Module: PostHog::ExceptionCapture Private

Defined in:
lib/posthog/exception_capture.rb

Overview

This module is part of a private API. You should avoid using this module if possible, as it may be removed or be changed in the future.

Builds PostHog exception payloads from Ruby exception objects.

Constant Summary collapse

RUBY_INPUT_FORMAT =

This constant is part of a private API. You should avoid using this constant if possible, as it may be removed or be changed in the future.

/
  ^ \s* (?: [a-zA-Z]: | uri:classloader: )? ([^:]+ | <.*>):
  (\d+)
  (?: :in\s('|`)(?:([\w:]+)\#)?([^']+)')?$
/x
MAX_CHAINED_EXCEPTIONS =

This constant is part of a private API. You should avoid using this constant if possible, as it may be removed or be changed in the future.

Maximum number of exceptions extracted from a single cause chain.

50
DEFAULT_MECHANISM =

This constant is part of a private API. You should avoid using this constant if possible, as it may be removed or be changed in the future.

{ 'type' => 'generic', 'handled' => true }.freeze

Class Method Summary collapse

Class Method Details

.add_context_lines(frame, file_path, lineno, context_size = 5) ⇒ void

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

This method returns an undefined value.

Parameters:

  • frame (Hash)
  • file_path (String)
  • lineno (Integer)
  • context_size (Integer) (defaults to: 5)


210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
# File 'lib/posthog/exception_capture.rb', line 210

def self.add_context_lines(frame, file_path, lineno, context_size = 5)
  lines = File.readlines(file_path)
  return if lines.empty?

  return unless lineno.positive? && lineno <= lines.length

  pre_context_start = [lineno - context_size, 1].max
  post_context_end = [lineno + context_size, lines.length].min

  frame['context_line'] = lines[lineno - 1].chomp

  frame['pre_context'] = lines[(pre_context_start - 1)...(lineno - 1)].map(&:chomp) if pre_context_start < lineno

  frame['post_context'] = lines[lineno...(post_context_end)].map(&:chomp) if post_context_end > lineno
rescue StandardError
  # Silently ignore file read errors
end

.build_exception_list(value, mechanism: nil) ⇒ Array<Hash>?

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Builds the $exception_list payload for an exception, walking its cause chain outermost-first (wrapper first, root cause last).

Parameters:

  • value (Exception, String, Object)

    Exception input to parse.

  • mechanism (Hash, nil) (defaults to: nil)

    Mechanism applied to the outermost exception, e.g. { 'type' => 'rails', 'handled' => false }. Chained causes are tagged with { 'type' => 'chained', ... } and parent linkage.

Returns:

  • (Array<Hash>, nil)

    Parsed exception payloads, or nil when the input is unsupported.



38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
# File 'lib/posthog/exception_capture.rb', line 38

def self.build_exception_list(value, mechanism: nil)
  root_mechanism = DEFAULT_MECHANISM.merge(mechanism || {})

  exceptions = []
  seen = {}.compare_by_identity
  current = value

  while current && exceptions.length < MAX_CHAINED_EXCEPTIONS && !seen.key?(current)
    parsed = build_parsed_exception(current, mechanism: chain_mechanism(root_mechanism, exceptions.length))
    break if parsed.nil?

    exceptions << parsed
    seen[current] = true
    current = current.respond_to?(:cause) ? current.cause : nil
  end

  exceptions.empty? ? nil : exceptions
end

.build_parsed_exception(value, mechanism: nil) ⇒ Hash?

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Returns Parsed exception payload, or nil when the input is unsupported.

Parameters:

  • value (Exception, String, Object)

    Exception input to parse.

  • mechanism (Hash, nil) (defaults to: nil)

    Mechanism describing how the exception was captured.

Returns:

  • (Hash, nil)

    Parsed exception payload, or nil when the input is unsupported.



74
75
76
77
78
79
# File 'lib/posthog/exception_capture.rb', line 74

def self.build_parsed_exception(value, mechanism: nil)
  title, message, backtrace = coerce_exception_input(value)
  return nil if title.nil?

  build_single_exception_from_data(title, message, backtrace, mechanism: mechanism)
end

.build_single_exception_from_data(title, message, backtrace, mechanism: nil) ⇒ Hash

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Parameters:

  • title (String)
  • message (String, nil)
  • backtrace (Array<String>, nil)
  • mechanism (Hash, nil) (defaults to: nil)

Returns:

  • (Hash)


86
87
88
89
90
91
92
93
# File 'lib/posthog/exception_capture.rb', line 86

def self.build_single_exception_from_data(title, message, backtrace, mechanism: nil)
  {
    'type' => title,
    'value' => message || '',
    'mechanism' => DEFAULT_MECHANISM.merge(mechanism || {}),
    'stacktrace' => build_stacktrace(backtrace)
  }
end

.build_stacktrace(backtrace) ⇒ Hash?

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Parameters:

  • backtrace (Array<String>, nil)

Returns:

  • (Hash, nil)


97
98
99
100
101
102
103
104
105
106
107
108
109
110
# File 'lib/posthog/exception_capture.rb', line 97

def self.build_stacktrace(backtrace)
  return nil unless backtrace && !backtrace.empty?

  root = project_root
  roots = dependency_roots(root)
  frames = backtrace.first(50).map do |line|
    parse_backtrace_line(line, project_root: root, dependency_roots: roots)
  end.compact.reverse

  {
    'type' => 'raw',
    'frames' => frames
  }
end

.chain_mechanism(root_mechanism, exception_id) ⇒ Hash

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Parameters:

  • root_mechanism (Hash)

    Mechanism of the outermost exception.

  • exception_id (Integer)

    Zero-based position in the cause chain.

Returns:

  • (Hash)


60
61
62
63
64
65
66
67
68
69
# File 'lib/posthog/exception_capture.rb', line 60

def self.chain_mechanism(root_mechanism, exception_id)
  mechanism = root_mechanism.merge('exception_id' => exception_id)
  return mechanism if exception_id.zero?

  mechanism.merge(
    'type' => 'chained',
    'source' => 'cause',
    'parent_id' => exception_id - 1
  )
end

.coerce_exception_input(value) ⇒ Array

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Returns Three-item array of title, message, and backtrace.

Parameters:

  • value (Exception, String, Object)

Returns:

  • (Array)

    Three-item array of title, message, and backtrace.



230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
# File 'lib/posthog/exception_capture.rb', line 230

def self.coerce_exception_input(value)
  if value.is_a?(String)
    title = 'Error'
    message = value
    backtrace = nil
  elsif value.respond_to?(:backtrace) && value.respond_to?(:message)
    title = value.class.to_s
    message = value.message || ''
    backtrace = value.backtrace
  else
    return [nil, nil, nil]
  end

  [title, message, backtrace]
end

.dependency_roots(project_root = self.project_root) ⇒ Array<String>

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Returns Directories containing installed gems and the Ruby stdlib.

Parameters:

  • project_root (String) (defaults to: self.project_root)

Returns:

  • (Array<String>)

    Directories containing installed gems and the Ruby stdlib.



182
183
184
185
186
187
188
189
190
191
192
193
194
195
# File 'lib/posthog/exception_capture.rb', line 182

def self.dependency_roots(project_root = self.project_root)
  roots = []
  if defined?(Gem)
    roots.concat(Gem.path) if Gem.respond_to?(:path)
    roots << Gem.default_dir if Gem.respond_to?(:default_dir)
    roots.concat(Gem.loaded_specs.each_value.map(&:full_gem_path)) if Gem.respond_to?(:loaded_specs)
  end
  roots << RbConfig::CONFIG['rubylibprefix'] if defined?(RbConfig)
  # The project itself can be a loaded spec (e.g. a gem developed in place,
  # or a Rails engine); its frames are still application code.
  roots.compact.reject(&:empty?).uniq - [project_root]
rescue StandardError
  []
end

.frame_filename(path, project_root) ⇒ String

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Stable filename used for fingerprinting: the project-relative path when the file lives inside the project root, its basename otherwise. The raw absolute path is kept separately in abs_path.

Parameters:

  • path (String)
  • project_root (String, nil)

Returns:

  • (String)


160
161
162
163
164
165
166
167
168
# File 'lib/posthog/exception_capture.rb', line 160

def self.frame_filename(path, project_root)
  if project_root && !project_root.empty? && path_within?(path, project_root)
    relative = path[project_root.length..]
    relative = relative[1..] while relative.start_with?(File::SEPARATOR)
    return relative unless relative.empty?
  end

  File.basename(path)
end

.gem_path?(path, dependency_roots = self.dependency_roots) ⇒ Boolean

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Whether the path belongs to an installed gem or the Ruby standard library, based on gem install locations rather than path substrings.

Parameters:

  • path (String)
  • dependency_roots (Array<String>) (defaults to: self.dependency_roots)

Returns:

  • (Boolean)


176
177
178
# File 'lib/posthog/exception_capture.rb', line 176

def self.gem_path?(path, dependency_roots = self.dependency_roots)
  dependency_roots.any? { |root| path_within?(path, root) }
end

.parse_backtrace_line(line, project_root: self.project_root, dependency_roots: nil) ⇒ Hash?

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Parameters:

  • line (String)
  • project_root (String, nil) (defaults to: self.project_root)

    Project root used to derive project-relative filenames.

  • dependency_roots (Array<String>, nil) (defaults to: nil)

    Cached gem and stdlib roots.

Returns:

  • (Hash, nil)


116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
# File 'lib/posthog/exception_capture.rb', line 116

def self.parse_backtrace_line(line, project_root: self.project_root, dependency_roots: nil)
  match = line.match(RUBY_INPUT_FORMAT)
  return nil unless match

  file = match[1]
  lineno = match[2].to_i
  method_name = match[5]

  frame = {
    'filename' => frame_filename(file, project_root),
    'abs_path' => file,
    'lineno' => lineno,
    'function' => method_name,
    'in_app' => !gem_path?(file, dependency_roots || self.dependency_roots(project_root)),
    'platform' => 'ruby'
  }

  add_context_lines(frame, file, lineno) if frame['in_app'] && File.exist?(file)

  frame
end

.path_within?(path, root) ⇒ Boolean

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Parameters:

  • path (String)
  • root (String)

Returns:

  • (Boolean)


200
201
202
203
# File 'lib/posthog/exception_capture.rb', line 200

def self.path_within?(path, root)
  root = root.chomp(File::SEPARATOR)
  path == root || path.start_with?("#{root}#{File::SEPARATOR}")
end

.project_rootString

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Root directory the application runs from, used to derive stable project-relative filenames from per-host deploy paths (e.g. /app/releases/20240101/...).

Returns:

  • (String)


143
144
145
146
147
148
149
150
151
# File 'lib/posthog/exception_capture.rb', line 143

def self.project_root
  if defined?(::Rails) && ::Rails.respond_to?(:root) && ::Rails.root
    ::Rails.root.to_s
  else
    Dir.pwd
  end
rescue StandardError
  Dir.pwd
end