module Squash::Ruby

Constants

CONFIGURATION_DEFAULTS

Default values for different configuration variables.

EXCEPTION_RESERVED_IVARS

Reserved instance variables that cannot be keys in a user-data hash.

JSON_NATIVE_TYPES

Types that are serialized directly to JSON, rather than to a hash of object information. Subclasses are not considered members of this array.

TOP_LEVEL_USER_DATA

Array of user-data fields that should be moved out of the user data to become top-level attributes. A Rails client library would expand this constant to include Rails-specific fields, for example.

Public Class Methods

add_user_data(user_data) { || ... } click to toggle source

Adds user data to any exception raised within a block of code, and re-raises the exception.

@param [Hash] user_data User data to add to an exception. @yield The code to run. @return The result of the block. @raise [ArgumentError] If `data` contains the keys `mesg` or `bt`.

# File lib/squash/ruby.rb, line 181
def self.add_user_data(user_data)
  raise ArgumentError, "Squash::Ruby.add_user_data must be called with a block" unless block_given?
  check_user_data user_data

  begin
    yield
  rescue Object => err
    user_data.each { |ivar, val| err.send :instance_variable_set, :"@#{ivar}", val }
    raise
  end
end
check_user_data(data) click to toggle source

@private

# File lib/squash/ruby.rb, line 253
def self.check_user_data(data)
  bad_ivars = EXCEPTION_RESERVED_IVARS.select { |name| data.keys.map { |k| k.to_s }.include? name }
  raise ArgumentError, "The following cannot be used as user-data keys: #{bad_ivars.join(', ')}" unless bad_ivars.empty?
end
configure(options) click to toggle source

Sets configuration options for the client from a hash. See the README for a list of configuration options. Subsequent calls will merge in new configuration options.

You must at a minimum specify the `:api_key` and `:environment` settings (see the README.md file).

@param [Hash] options Configuration options.

# File lib/squash/ruby.rb, line 248
def self.configure(options)
  @configuration = (@configuration || CONFIGURATION_DEFAULTS.dup).merge(options.inject({}) { |hsh, (k, v)| hsh[(k.to_sym rescue k)] = v; hsh })
end
fail_silently(exception_classes_or_options=nil, options=nil) { || ... } click to toggle source

@overload fail_silently(exception_classes=nil, options={})

Executes the block, suppressing and silently reporting any exceptions to
Squash. This allows you to ensure that a non-critical block of code
does not halt your application while still receiving exception
notifications in Squash.

The behavior of this method when Squash is disabled is dependent on the
`exception_behavior_when_disabled` configuration option.

@param [Array<Class>] exception_classes A list of exception classes to
  report silently. Exceptions _not_ of these classes (or their
  subclasses) will raise (and presumably be handled by Squash elsewhere
  in your code).
@param [Hash] options Additional options to pass to {.notify}.
@yield The code to suppress exceptions in.
@return The result of the block.

@raise [ArgumentError] If no block is provided.

# File lib/squash/ruby.rb, line 211
def self.fail_silently(exception_classes_or_options=nil, options=nil)
  raise ArgumentError, "Squash::Ruby.exception_classes must be called with a block" unless block_given?

  exception_classes = if options
                        exception_classes_or_options
                      else
                        if exception_classes_or_options.kind_of?(Hash) then
                          options = exception_classes_or_options
                          nil
                        else
                          exception_classes_or_options
                        end
                      end
  options           ||= {}

  exception_classes = [exception_classes] if exception_classes.kind_of?(Class)

  begin
    yield
  rescue Object => err
    if exception_classes.nil? || exception_classes.detect { |e| err.kind_of?(e) }
      Squash::Ruby.notify err, options
    else
      raise
    end
  end
end
ignore_exceptions(exception_classes=nil) { || ... } click to toggle source

Suppresses reporting of certain exceptions within a block of code. Any exceptions raised in the block will continue to be raised, however.

Let's take a few examples. If `exception_classes` is `[RangeError]`, then obviously any raised `RangeError`s will not be reported. If `StandardError` is raised, it will be reported, because it's a superclass of `RangeError`. If `FloatDomainError` is raised, it _will not_ be reported because it is a subclass of `RangeError`. Confusing? Sure, but I'm pretty sure this is the behavior most people would expect.

@param [Array<Class>] exception_classes A list of exception classes to

ignore. If not provided, ignores all exceptions raised in the block.

@yield The code to ignore exceptions in. @return The result of the block. @raise [ArgumentError] If no block is provided.

# File lib/squash/ruby.rb, line 161
def self.ignore_exceptions(exception_classes=nil)
  raise ArgumentError, "Squash::Ruby.ignore_exceptions must be called with a block" unless block_given?
  exception_classes = [exception_classes] if exception_classes.kind_of?(Class)

  begin
    yield
  rescue Object => err
    err.instance_variable_set(:@_squash_do_not_report, true) if exception_classes.nil? || exception_classes.map { |e| e.ancestors }.flatten.include?(err.class)
    raise
  end
end
notify(exception, user_data={}) click to toggle source

Notifies Squash of an exception. The behavior of this method when Squash is disabled is dependent on the `exception_behavior_when_disabled` configuration option.

@param [Object] exception The exception. Must at least duck-type an

`Exception` subclass.

@param [Hash] user_data Any additional context-specific information about

the exception.

@return [true, false] Whether the exception was reported to Squash. (Some

exceptions are ignored and not reported to Squash.)

@raise [StandardError] If Squash has not yet been fully configured (see

{.configure}).
# File lib/squash/ruby.rb, line 70
def self.notify(exception, user_data={})
  if configuration(:disabled)
    case configuration(:exception_behavior_when_disabled)
      when 'log', :log
        failsafe_log '[Squash::Ruby.notify]', "Exception raised: #{exception.to_s}\n" +
                                                exception.backtrace.map { |l| "  #{l}" }.join("\n")
      when 'raise', :raise
        raise exception
      else
        # ignore
    end
    return false
  else
    raise "The :api_key configuration is required" unless configuration(:api_key)
    raise "The :api_host configuration is required" unless configuration(:api_host)
    raise "The :environment configuration is required" unless configuration(:environment)
  end

  begin
    blob = self.generate_exception(exception, user_data)
    return false if blob.nil?

    self.transmit_exception(blob)
    return true
  rescue Object => nested_error
    raise if configuration(:disable_failsafe)
    failsafe_handler exception, nested_error
    :failsafe # a perfect example of http://thedailywtf.com/Articles/What_Is_Truth_0x3f_.aspx
  end
end
record(exception_class_or_message, message_or_options=nil, data=nil) click to toggle source

Raises an exception and immediately catches it and sends it to Squash. The exception is then eaten. This is meant to be used as a hackneyed form of event logging. You can pass in any user data you wish to record with the event.

It should be emphasized that Squash is not a logging system, and there are far more appropriate products for this kind of thing, but this method is here nonetheless.

@overload record(exception_class, message, user_data={})

Specify both the exception class and the message.
@param [Class] exception_class The exception class to raise.
@param [String] message The exception message.
@param [Hash] user_data Additional information to give to {.notify}.

@overload record(message, user_data={})

Specify only the message. The exception class will be `StandardError`.
@param [String] message The exception message.
@param [Hash] user_data Additional information to give to {.notify}.
# File lib/squash/ruby.rb, line 120
def self.record(exception_class_or_message, message_or_options=nil, data=nil)
  if message_or_options && data
    exception_class = exception_class_or_message
    message         = message_or_options
  elsif message_or_options.kind_of?(String)
    message         = message_or_options
    exception_class = exception_class_or_message
  elsif message_or_options.kind_of?(Hash)
    data            = message_or_options
    message         = exception_class_or_message
    exception_class = StandardError
  elsif message_or_options.nil?
    message         = exception_class_or_message
    exception_class = StandardError
  else
    raise ArgumentError
  end

  begin
    raise exception_class, message
  rescue exception_class => error
    notify error, data || {}
  end
end

Protected Class Methods

generate_exception(exception, user_data={}) click to toggle source

Generates an exception JSON blob for use with {.transmit_exception}. These methods are separated so that the JSON blobs can be generated separately from the actual transmission of exceptions, allowing each task to be optimized separately.

@param [Object] exception The exception. Must at least duck-type an

`Exception` subclass.

@param [Hash] user_data Any additional context-specific information about

the exception.

@return [String, nil] The JSON data to transmit to Squash. If `nil`,

no request should be made to Squash.
# File lib/squash/ruby.rb, line 272
def self.generate_exception(exception, user_data={})
  occurred = Time.now

  return nil if configuration(:disabled)
  unless exception.respond_to?(:backtrace)
    failsafe_log 'notify', "Tried to pass notify something other than an exception: #{exception.inspect}"
    return nil
  end
  unless exception.backtrace
    failsafe_log 'notify', "Tried to pass notify an exception with no backtrace: #{exception}"
    return nil
  end

  exception, parents = unroll(exception)
  return nil if ignored?(exception, user_data)
  check_user_data user_data

  hsh = exception_info_hash(exception, occurred, user_data, parents)
  hsh.inject({}) { |h, (k, v)| h[k.to_s] = v; h }.to_json
end
http_transmit(url, headers, body) click to toggle source

Posts an exception or deploy notification to the API endpoint. Only POST requests are supported. This method is used internally only. It is documented so that, in the event you wish to use an alternative HTTP library (other than `Net::HTTP`), you can override this method.

This method will make a `POST` request to the given URL. The request will contain the given headers and body. It should not eat any exceptions relating to HTTP connectivity issues.

There is support for the Linux idiom of configuring a proxy server by setting the environment variables `http_proxy` and `https_proxy`.

Your implementation should also respect the value of the `transmit_timeout` configuration, which is accessible using `configuration(:transmit_timeout)`.

@param [String] url The URL to post to. Could be an HTTP or HTTPS URL. @param [Hash<String, String>] headers The request headers.

`Content-Type: application/json` is added by default.

@param [String] body The request body. @return [true, false] Whether or not the response was successful.

# File lib/squash/ruby.rb, line 325
def self.http_transmit(url, headers, body)
  uri = URI.parse(url)

  no_proxy = if ENV['no_proxy']
               ENV['no_proxy'].split(',').map(&:strip).any? { |ext| uri.host[-ext.length, ext.length] == ext }
             else
               false
             end

  http = if uri.scheme == 'https' && ENV['https_proxy'] && !no_proxy
           proxy = URI.parse(ENV['https_proxy'])
           Net::HTTP::Proxy(proxy.host, proxy.port, proxy.user, proxy.password).new(uri.host, uri.port)
         elsif uri.scheme == 'http' && ENV['http_proxy'] && !no_proxy
           proxy = URI.parse(ENV['http_proxy'])
           Net::HTTP::Proxy(proxy.host, proxy.port, proxy.user, proxy.password).new(uri.host, uri.port)
         else
           Net::HTTP.new(uri.host, uri.port)
         end

  http_options(uri).each { |k, v| http.send :"#{k}=", v }

  timeout = configuration(:open_timeout) + configuration(:transmit_timeout)
  configuration(:timeout_protection).call(timeout) do
    http.start do |http|
      request = Net::HTTP::Post.new(uri.request_uri)
      request.add_field 'Content-Type', 'application/json'
      headers.each { |k, v| request.add_field k, v }
      request.body = body
      response     = http.request request
      if response.kind_of?(Net::HTTPSuccess)
        return true
      else
        self.failsafe_log 'http_transmit', "Response from server: #{response.code}"
        return false
      end
    end
  end
end
notify_deploy(env, revision, from_host) click to toggle source

Notifies Squash of a new deploy. Squash will then determine which bug fixes have been deployed and then mark those bugs as fix-deployed.

@param [String] env The name of the environment that was deployed. @param [String] revision The repository revision that was deployed. @param [String] from_host The hostname of the computer that performed the

deploy.

@raise [StandardError] If an invalid response is received from the HTTP

request.
# File lib/squash/ruby.rb, line 374
def self.notify_deploy(env, revision, from_host)
  return if configuration(:disabled)

  success = http_transmit(
      configuration(:api_host) + configuration(:deploy_path),
      {},
      {
          'project'     => {'api_key' => configuration(:api_key)},
          'environment' => {'name' => env},
          'deploy'      => {
              'deployed_at' => Time.now,
              'revision'    => revision,
              'hostname'    => from_host
          }
      }.to_json
  )
  $stderr.puts "[Squash] Bad response; see failsafe log" unless success
rescue Timeout::Error
  $stderr.puts "[Squash] Timeout when trying to notify of the deploy"
end
transmit_exception(blob) click to toggle source

Transmits to Squash exception data generated by {.generate_exception}.

@param [String] blob Exception data. @return [true, false] Whether or not the response was successful. @see .generate_exception

# File lib/squash/ruby.rb, line 299
def self.transmit_exception(blob)
  http_transmit configuration(:api_host) + configuration(:notify_path), {}, blob
end
value_filter(value) click to toggle source

@abstract

Override this method to filter sensitive information from any data that Squash intends to add to an occurrence. This method receives every object that Squash is about to serialize – instance variables, user data, (for Rails) sessions, params, etc., just before serialization and transmission.

This method gives you the opportunity to alter the object before serialization, for example to remove sensitive information. It's probably a good idea to clone the object, modify it, and then return the clone, so that the original object remains unmodified.

The base implementation returns `value` unmodified.

@param value A value that is about to be serialized for transmission. @return The object to serialize (filtered as necessary). May be a different

object.
# File lib/squash/ruby.rb, line 413
def self.value_filter(value) value end

Private Class Methods

check_size(value) click to toggle source
# File lib/squash/ruby.rb, line 619
def self.check_size(value)
  if configuration(:max_variable_size)
    value.each do |format, representation|
      if representation.bytesize > configuration(:max_variable_size)
        value[format] = '[exceeded maximum variable size]'
      end
    end
  else
    value
  end
end
client_name() click to toggle source

@private

# File lib/squash/ruby.rb, line 738
def self.client_name() 'ruby' end
configuration(key) click to toggle source
# File lib/squash/ruby.rb, line 425
def self.configuration(key)
  (@configuration || CONFIGURATION_DEFAULTS)[key] || CONFIGURATION_DEFAULTS[key]
end
constantize(class_name) click to toggle source
# File lib/squash/ruby.rb, line 638
def self.constantize(class_name)
  return class_name if class_name.kind_of?(Class)

  parts    = class_name.split('::').reject { |i| i.empty? }
  constant = Object
  while parts.any? do
    begin
      constant = constant.const_get(parts.shift)
    rescue NameError
      return nil
    end
  end
  constant
end
current_revision() click to toggle source

@private

# File lib/squash/ruby.rb, line 686
def self.current_revision
  revision = if configuration(:revision)
               configuration(:revision)
             elsif configuration(:revision_file)
               File.read(configuration(:revision_file)).chomp.strip
             else
               if configuration(:mirrored_repository)
                 head_file = File.join(configuration(:repository_root), 'HEAD')
               else
                 head_file = File.join(configuration(:repository_root), '.git', 'HEAD')
               end
               if File.exist?(head_file)
                 rev = File.read(head_file).chomp.strip
                 if rev =~ /^ref: (.+?)$/
                   rev      = nil # in case we need to shell
                   ref      = $1
                   ref_file = File.join(configuration(:repository_root), '.git', ref)
                   if File.exist?(ref_file)
                     rev = File.read(ref_file).chomp.strip
                   elsif File.exist?(File.join(configuration(:repository_root), '.git', 'packed-refs'))
                     revs = File.join(configuration(:repository_root), '.git', 'packed-refs')
                     File.open(revs) do |f|
                       f.each_line do |line|
                         next if line[0, 1] == '#'
                         next if line[0, 1] == '^'
                         next unless line.chomp[-(ref.length)..-1] == ref
                         rev = line[0, 40]
                         break
                       end
                     end
                   end
                 end
                 rev ||= `git rev-parse #{ref}`.strip # shell as a last resort
                 rev
               else
                 raise "You must set the :revision_file configuration if the code is not running in a Git checkout"
               end
             end
  raise "Unknown Git revision #{revision.inspect}" unless revision =~ /^[0-9a-f]{40}$/
  revision
end
environment_data() click to toggle source

@private

# File lib/squash/ruby.rb, line 654
def self.environment_data
  {
      'pid'       => Process.pid,
      'hostname'  => Socket.gethostname,
      'env_vars'  => ENV.inject({}) { |hsh, (k, v)| hsh[k.to_s] = valueify(v); hsh },
      'arguments' => ARGV.join(' ')
  }
end
exception_info_hash(exception, occurred, user_data, parents) click to toggle source
# File lib/squash/ruby.rb, line 448
def self.exception_info_hash(exception, occurred, user_data, parents)
  top_level_user_data = Hash.new
  user_data.delete_if do |key, value|
    if TOP_LEVEL_USER_DATA.include?(key.to_s)
      top_level_user_data[key.to_s] = valueify(value, true)
      true
    else
      false
    end
  end

  environment_data.merge(top_level_user_data).merge(
      'class_name'        => exception.class.to_s,
      'message'           => exception.to_s,
      'backtraces'        => [{
                                  'name'      => "Active Thread/Fiber",
                                  'faulted'   => true,
                                  'backtrace' => prepare_backtrace(exception.backtrace)
                              }],
      'occurred_at'       => occurred,
      'revision'          => current_revision,

      'environment'       => configuration(:environment).to_s,
      'api_key'           => configuration(:api_key).to_s,
      'client'            => client_name,

      'ivars'             => instance_variable_hash(exception),
      'user_data'         => valueify(user_data, true),

      'parent_exceptions' => parents.nil? ? nil : parents.map do |parent|
        {'class_name'  => parent.class.to_s,
         'message'     => parent.to_s,
         'backtraces'  => [{
                               'name'      => "Active Thread/Fiber",
                               'faulted'   => true,
                               'backtrace' => prepare_backtrace(parent.backtrace)
                           }],
         'association' => 'original_exception',
         'ivars'       => instance_variable_hash(parent)}
      end
  )
end
failsafe_handler(original_error, nested_error) click to toggle source

@private

# File lib/squash/ruby.rb, line 664
def self.failsafe_handler(original_error, nested_error)
  log_entries = [
      "#{Time.now.to_s} - Original error: (#{original_error.class.to_s}) #{original_error.to_s}",
      (original_error.backtrace || []).map { |l| '  ' + l }.join("\n"),
      "#{Time.now.to_s} - Error raised when reporting original error: (#{nested_error.class.to_s}) #{nested_error.to_s}",
      nested_error.backtrace.map { |l| '  ' + l }.join("\n"),
      "--- END SQUASH FAILSAFE ERROR ---"
  ]
  log_entries.each { |message| self.failsafe_log 'failsafe_handler', message }
end
failsafe_log(tag, message) click to toggle source

@private

# File lib/squash/ruby.rb, line 676
def self.failsafe_log(tag, message)
  File.open(configuration(:failsafe_log), 'a') do |f|
    f.puts "#{Time.now.to_s}\t[#{tag}]\t#{message}"
  end
rescue Object => err
  $stderr.puts "Couldn't write to failsafe log (#{err.to_s}); writing to stderr instead."
  $stderr.puts "#{Time.now.to_s}\t[#{tag}]\t#{message}"
end
http_options(uri) click to toggle source
# File lib/squash/ruby.rb, line 417
def self.http_options(uri)
  options = {use_ssl:      uri.scheme == 'https',
             open_timeout: configuration(:transmit_timeout),
             read_timeout: configuration(:transmit_timeout)}
  options[:verify_mode] = OpenSSL::SSL::VERIFY_NONE if configuration(:skip_ssl_verification)
  options
end
ignored?(exception, user_data) click to toggle source
# File lib/squash/ruby.rb, line 429
def self.ignored?(exception, user_data)
  return true if exception.instance_variable_get(:@_squash_do_not_report)

  return true if Array(configuration(:ignored_exception_classes)).map do |class_name|
    constantize class_name
  end.compact.any? { |klass| exception.kind_of?(klass) }

  return true if configuration(:ignored_exception_messages).any? do |class_name, ignored_messages|
    ignored_messages = Array(ignored_messages).map { |str| str.kind_of?(String) ? Regexp.compile(str) : str }
    (klass = constantize(class_name)) && exception.kind_of?(klass) && ignored_messages.any? { |msg| exception.to_s =~ msg }
  end

  return true if Array(configuration(:ignored_exception_procs)).any? do |proc|
    proc.call(exception, user_data)
  end

  return false
end
instance_variable_hash(object) click to toggle source
# File lib/squash/ruby.rb, line 631
def self.instance_variable_hash(object)
  object.instance_variables.inject({}) do |hsh, cur|
    hsh[cur.to_s[1..-1]] = valueify(object.send(:instance_variable_get, cur.to_sym))
    hsh
  end
end
prepare_backtrace(bt) click to toggle source
# File lib/squash/ruby.rb, line 491
def self.prepare_backtrace(bt)
  if defined?(JRuby)
    bt.map(&:strip).map do |element|
      if element =~ /^((?:[a-z0-9_$]+\.)*(?:[a-z0-9_$]+))\.(\w+)\((\w+.java):(\d+)\)$/i
        # special JRuby backtrace element of the form "org.jruby.RubyHash$27.visit(RubyHash.java:1646)"
        {
            'type'   => 'obfuscated',
            'file'   => $3,
            'line'   => $4.to_i,
            'symbol' => $2,
            'class'  => $1
        }
      elsif element =~ /^(.+?)\.(\w+)\(Native Method\)$/
        {
            'type'   => 'java_native',
            'symbol' => $2,
            'class'  => $1
        }
      elsif element =~ /^rubyjit[$.](.+?)\$\$(\w+?[?!]?)_[0-9A-F]{40}.+?__(?:file|ensure)__\.call\(.+\)$/
        {
            'type'   => 'jruby_block',
            'class'  => $1,
            'symbol' => $2
        }
      elsif element =~ /^rubyjit[$.](.+?)\$\$(\w+?[?!]?)_[0-9A-F]{40}.+?__(?:file|ensure)__\((.+?):(\d+)\)$/
        {
            'file'   => $3,
            'line'   => $4.to_i,
            'symbol' => "#{$1}##{$2}"
        }
      elsif element =~ /^.+\.call\(.+?(\w+)\.gen\)$/
        {
            'type' => 'asm_invoker',
            'file' => $1 + '.gen'
        }
      elsif element =~ /^rubyjit[$.](.+?)\$\$(\w+?[?!]?)_[0-9A-F]{40}.+?__(?:file|ensure)__\((.+?)\)$/
        {
            'file'   => $3,
            'type'   => 'jruby_noline',
            'symbol' => "#{$1}##{$2}"
        }
      else
        if element.include?(' at ')
          method, fileline = element.split(' at ')
          method.lstrip!
          file, line = fileline.split(':')
        else
          file, line, method = element.split(':')
          if method =~ /^in `(.+)'$/
            method = $1
          end
          method = nil if method && method.empty?
        end
        line = line.to_i
        line = nil if line < 1
        if method =~ /^in `(.+)'$/
          method = $1
        end
        method = nil if method && method.empty?

        # it could still be a java backtrace, even if it's not the special format
        if file[-5, 5] == '.java'
          {
              'type'   => 'obfuscated',
              'file'   => file.split('/').last,
              'line'   => line,
              'symbol' => method,
              'class'  => file.sub(/\.java$/, '').gsub('/', '.')
          }
        else
          # ok now we're sure it's a ruby backtrace
          file.slice! 0, configuration(:project_root).length + 1 if file[0, configuration(:project_root).length + 1] == configuration(:project_root) + '/'
          {
              'file'   => file,
              'line'   => line,
              'symbol' => method
          }
        end
      end
    end
  else
    bt.map do |element|
      file, line, method = element.split(':')
      line               = line.to_i
      line = nil if line < 1

      file.slice! 0, configuration(:project_root).length + 1 if file[0, configuration(:project_root).length + 1] == configuration(:project_root) + '/'

      if method =~ /^in `(.+)'$/
        method = $1
      end
      method = nil if method && method.empty?
      {
          'file'   => file,
          'line'   => line,
          'symbol' => method
      }
    end
  end
end
timeout_protection(timeout, &block) click to toggle source

@private

# File lib/squash/ruby.rb, line 741
def self.timeout_protection(timeout, &block)
  if defined?(SystemTimer)
    SystemTimer.timeout_after(timeout, &block)
  else
    Timeout.timeout(timeout, &block)
  end
end
unroll(exception) click to toggle source

@private

# File lib/squash/ruby.rb, line 729
def self.unroll(exception)
  if exception.respond_to?(:original_exception)
    [exception.original_exception, [exception]]
  else
    [exception, nil]
  end
end
valueify(instance, elements_only=false) click to toggle source
# File lib/squash/ruby.rb, line 592
def self.valueify(instance, elements_only=false)
  if JSON_NATIVE_TYPES.any? { |klass| instance.class == klass }
    instance
  elsif instance.kind_of?(Hash) && elements_only
    instance.inject({}) { |hsh, (k, v)| hsh[k.to_s] = valueify(v); hsh }
  elsif instance.kind_of?(Array) && elements_only
    instance.map { |i| valueify(i) }
  else
    filtered       = value_filter(instance)
    yaml           = begin filtered.to_yaml; rescue Exception; nil end
    json           = begin filtered.to_json; rescue Exception; nil end
    inspect_result = begin filtered.inspect; rescue Exception => e; "[#{e.class}: #{e} raised when calling #inspect]" end
    to_s_result    = begin filtered.to_s; rescue Exception => e; "[#{e.class}: #{e} raised when calling #to_s]" end

    serialized = {
        'language'   => 'ruby',
        'class_name' => filtered.class.to_s,
        'inspect'    => inspect_result,
        'yaml'       => yaml,
        'json'       => json,
        'to_s'       => to_s_result
    }

    elements_only ? serialized : check_size(serialized)
  end
end