posthog-ruby 3.15.3 → 3.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 36a73ef4ffe63b9e2c722441bd8d3c5732e8e162ea1431bdc68bc861b2c8a452
4
- data.tar.gz: cf1f040865ce0df9fc7c68d0cac9f0903114b959b1b162d83afecfbdfe8697d1
3
+ metadata.gz: 187ed0ab3a49d0137a8a5516792d00eb6baf5242ed6665df879563656a6541ac
4
+ data.tar.gz: cf463f7135031a7d6975bac3eb72ad48ead37729c577738f80fe03e83a6c8f9c
5
5
  SHA512:
6
- metadata.gz: 5214a1df75497519ac214d1134549a9d41e98bb16ca7e82df800fca062957ecc17c382472902cd7a8161fe4a6a92786c2fe243ec10663dbbaeb7ae1ddd54562f
7
- data.tar.gz: 3adf9801fe9b1a64427d8c7a0fd4abbc15b78c77fa80566e2be23bc97ff9d3d9808be9ba0513f71d00b814ae9e70da532e82495ce3c4bf763b00ac1adf30b076
6
+ metadata.gz: 5f0906fded5049de9de8638ffee0345e7db4bee9a73387fcc602247a9e62193e011fa89358a1001d8818ec216116aec3750548e0c056a6488a7c3360784da038
7
+ data.tar.gz: 5d7c036bfccdb96b1343287a7349c5ab95ec7184b88db2189f9ba4d805756e050edc3f14533517b79a30377253649ceb1ccecf95a0f9950fdc28812cf18ec629
@@ -311,15 +311,18 @@ module PostHog
311
311
  # @param flags [PostHog::FeatureFlagEvaluations, nil] A snapshot returned by {#evaluate_flags}.
312
312
  # Forwarded to the inner {#capture} call so the captured `$exception` event carries the
313
313
  # same `$feature/<key>` and `$active_feature_flags` properties as the snapshot.
314
+ # @param mechanism [Hash, nil] How the exception was captured, e.g.
315
+ # `{ 'type' => 'rails', 'handled' => false }` for automatic integrations.
316
+ # Defaults to `{ 'type' => 'generic', 'handled' => true }` for manual captures.
314
317
  # @return [Boolean, nil] Whether the exception event was queued or sent, or nil if the input could not be parsed.
315
- def capture_exception(exception, distinct_id = nil, additional_properties = {}, flags: nil)
318
+ def capture_exception(exception, distinct_id = nil, additional_properties = {}, flags: nil, mechanism: nil)
316
319
  return false if @disabled
317
320
 
318
- exception_info = ExceptionCapture.build_parsed_exception(exception)
321
+ exception_list = ExceptionCapture.build_exception_list(exception, mechanism: mechanism)
319
322
 
320
- return if exception_info.nil?
323
+ return if exception_list.nil?
321
324
 
322
- properties = { '$exception_list' => [exception_info] }
325
+ properties = { '$exception_list' => exception_list }
323
326
  properties.merge!(additional_properties) if additional_properties && !additional_properties.empty?
324
327
 
325
328
  event_data = {
@@ -22,27 +22,72 @@ module PostHog
22
22
  (?: :in\s('|`)(?:([\w:]+)\#)?([^']+)')?$
23
23
  /x
24
24
 
25
+ # Maximum number of exceptions extracted from a single `cause` chain.
26
+ MAX_CHAINED_EXCEPTIONS = 50
27
+
28
+ DEFAULT_MECHANISM = { 'type' => 'generic', 'handled' => true }.freeze
29
+
30
+ # Builds the `$exception_list` payload for an exception, walking its
31
+ # `cause` chain outermost-first (wrapper first, root cause last).
32
+ #
33
+ # @param value [Exception, String, Object] Exception input to parse.
34
+ # @param mechanism [Hash, nil] Mechanism applied to the outermost exception,
35
+ # e.g. `{ 'type' => 'rails', 'handled' => false }`. Chained causes are
36
+ # tagged with `{ 'type' => 'chained', ... }` and parent linkage.
37
+ # @return [Array<Hash>, nil] Parsed exception payloads, or nil when the input is unsupported.
38
+ def self.build_exception_list(value, mechanism: nil)
39
+ root_mechanism = DEFAULT_MECHANISM.merge(mechanism || {})
40
+
41
+ exceptions = []
42
+ seen = {}.compare_by_identity
43
+ current = value
44
+
45
+ while current && exceptions.length < MAX_CHAINED_EXCEPTIONS && !seen.key?(current)
46
+ parsed = build_parsed_exception(current, mechanism: chain_mechanism(root_mechanism, exceptions.length))
47
+ break if parsed.nil?
48
+
49
+ exceptions << parsed
50
+ seen[current] = true
51
+ current = current.respond_to?(:cause) ? current.cause : nil
52
+ end
53
+
54
+ exceptions.empty? ? nil : exceptions
55
+ end
56
+
57
+ # @param root_mechanism [Hash] Mechanism of the outermost exception.
58
+ # @param exception_id [Integer] Zero-based position in the cause chain.
59
+ # @return [Hash]
60
+ def self.chain_mechanism(root_mechanism, exception_id)
61
+ mechanism = root_mechanism.merge('exception_id' => exception_id)
62
+ return mechanism if exception_id.zero?
63
+
64
+ mechanism.merge(
65
+ 'type' => 'chained',
66
+ 'source' => 'cause',
67
+ 'parent_id' => exception_id - 1
68
+ )
69
+ end
70
+
25
71
  # @param value [Exception, String, Object] Exception input to parse.
72
+ # @param mechanism [Hash, nil] Mechanism describing how the exception was captured.
26
73
  # @return [Hash, nil] Parsed exception payload, or nil when the input is unsupported.
27
- def self.build_parsed_exception(value)
74
+ def self.build_parsed_exception(value, mechanism: nil)
28
75
  title, message, backtrace = coerce_exception_input(value)
29
76
  return nil if title.nil?
30
77
 
31
- build_single_exception_from_data(title, message, backtrace)
78
+ build_single_exception_from_data(title, message, backtrace, mechanism: mechanism)
32
79
  end
33
80
 
34
81
  # @param title [String]
35
82
  # @param message [String, nil]
36
83
  # @param backtrace [Array<String>, nil]
84
+ # @param mechanism [Hash, nil]
37
85
  # @return [Hash]
38
- def self.build_single_exception_from_data(title, message, backtrace)
86
+ def self.build_single_exception_from_data(title, message, backtrace, mechanism: nil)
39
87
  {
40
88
  'type' => title,
41
89
  'value' => message || '',
42
- 'mechanism' => {
43
- 'type' => 'generic',
44
- 'handled' => true
45
- },
90
+ 'mechanism' => DEFAULT_MECHANISM.merge(mechanism || {}),
46
91
  'stacktrace' => build_stacktrace(backtrace)
47
92
  }
48
93
  end
@@ -52,8 +97,10 @@ module PostHog
52
97
  def self.build_stacktrace(backtrace)
53
98
  return nil unless backtrace && !backtrace.empty?
54
99
 
100
+ root = project_root
101
+ roots = dependency_roots(root)
55
102
  frames = backtrace.first(50).map do |line|
56
- parse_backtrace_line(line)
103
+ parse_backtrace_line(line, project_root: root, dependency_roots: roots)
57
104
  end.compact.reverse
58
105
 
59
106
  {
@@ -63,8 +110,10 @@ module PostHog
63
110
  end
64
111
 
65
112
  # @param line [String]
113
+ # @param project_root [String, nil] Project root used to derive project-relative filenames.
114
+ # @param dependency_roots [Array<String>, nil] Cached gem and stdlib roots.
66
115
  # @return [Hash, nil]
67
- def self.parse_backtrace_line(line)
116
+ def self.parse_backtrace_line(line, project_root: self.project_root, dependency_roots: nil)
68
117
  match = line.match(RUBY_INPUT_FORMAT)
69
118
  return nil unless match
70
119
 
@@ -73,11 +122,11 @@ module PostHog
73
122
  method_name = match[5]
74
123
 
75
124
  frame = {
76
- 'filename' => File.basename(file),
125
+ 'filename' => frame_filename(file, project_root),
77
126
  'abs_path' => file,
78
127
  'lineno' => lineno,
79
128
  'function' => method_name,
80
- 'in_app' => !gem_path?(file),
129
+ 'in_app' => !gem_path?(file, dependency_roots || self.dependency_roots(project_root)),
81
130
  'platform' => 'ruby'
82
131
  }
83
132
 
@@ -86,13 +135,71 @@ module PostHog
86
135
  frame
87
136
  end
88
137
 
138
+ # Root directory the application runs from, used to derive stable
139
+ # project-relative filenames from per-host deploy paths
140
+ # (e.g. `/app/releases/20240101/...`).
141
+ #
142
+ # @return [String]
143
+ def self.project_root
144
+ if defined?(::Rails) && ::Rails.respond_to?(:root) && ::Rails.root
145
+ ::Rails.root.to_s
146
+ else
147
+ Dir.pwd
148
+ end
149
+ rescue StandardError
150
+ Dir.pwd
151
+ end
152
+
153
+ # Stable filename used for fingerprinting: the project-relative path when
154
+ # the file lives inside the project root, its basename otherwise. The raw
155
+ # absolute path is kept separately in `abs_path`.
156
+ #
157
+ # @param path [String]
158
+ # @param project_root [String, nil]
159
+ # @return [String]
160
+ def self.frame_filename(path, project_root)
161
+ if project_root && !project_root.empty? && path_within?(path, project_root)
162
+ relative = path[project_root.length..]
163
+ relative = relative[1..] while relative.start_with?(File::SEPARATOR)
164
+ return relative unless relative.empty?
165
+ end
166
+
167
+ File.basename(path)
168
+ end
169
+
170
+ # Whether the path belongs to an installed gem or the Ruby standard library,
171
+ # based on gem install locations rather than path substrings.
172
+ #
173
+ # @param path [String]
174
+ # @param dependency_roots [Array<String>]
175
+ # @return [Boolean]
176
+ def self.gem_path?(path, dependency_roots = self.dependency_roots)
177
+ dependency_roots.any? { |root| path_within?(path, root) }
178
+ end
179
+
180
+ # @param project_root [String]
181
+ # @return [Array<String>] Directories containing installed gems and the Ruby stdlib.
182
+ def self.dependency_roots(project_root = self.project_root)
183
+ roots = []
184
+ if defined?(Gem)
185
+ roots.concat(Gem.path) if Gem.respond_to?(:path)
186
+ roots << Gem.default_dir if Gem.respond_to?(:default_dir)
187
+ roots.concat(Gem.loaded_specs.each_value.map(&:full_gem_path)) if Gem.respond_to?(:loaded_specs)
188
+ end
189
+ roots << RbConfig::CONFIG['rubylibprefix'] if defined?(RbConfig)
190
+ # The project itself can be a loaded spec (e.g. a gem developed in place,
191
+ # or a Rails engine); its frames are still application code.
192
+ roots.compact.reject(&:empty?).uniq - [project_root]
193
+ rescue StandardError
194
+ []
195
+ end
196
+
89
197
  # @param path [String]
198
+ # @param root [String]
90
199
  # @return [Boolean]
91
- def self.gem_path?(path)
92
- path.include?('/gems/') ||
93
- path.include?('/ruby/') ||
94
- path.include?('/.rbenv/') ||
95
- path.include?('/.rvm/')
200
+ def self.path_within?(path, root)
201
+ root = root.chomp(File::SEPARATOR)
202
+ path == root || path.start_with?("#{root}#{File::SEPARATOR}")
96
203
  end
97
204
 
98
205
  # @param frame [Hash]
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module PostHog
4
- VERSION = '3.15.3'
4
+ VERSION = '3.17.0'
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: posthog-ruby
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.15.3
4
+ version: 3.17.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - ''