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 +4 -4
- data/lib/posthog/client.rb +7 -4
- data/lib/posthog/exception_capture.rb +123 -16
- data/lib/posthog/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 187ed0ab3a49d0137a8a5516792d00eb6baf5242ed6665df879563656a6541ac
|
|
4
|
+
data.tar.gz: cf463f7135031a7d6975bac3eb72ad48ead37729c577738f80fe03e83a6c8f9c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 5f0906fded5049de9de8638ffee0345e7db4bee9a73387fcc602247a9e62193e011fa89358a1001d8818ec216116aec3750548e0c056a6488a7c3360784da038
|
|
7
|
+
data.tar.gz: 5d7c036bfccdb96b1343287a7349c5ab95ec7184b88db2189f9ba4d805756e050edc3f14533517b79a30377253649ceb1ccecf95a0f9950fdc28812cf18ec629
|
data/lib/posthog/client.rb
CHANGED
|
@@ -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
|
-
|
|
321
|
+
exception_list = ExceptionCapture.build_exception_list(exception, mechanism: mechanism)
|
|
319
322
|
|
|
320
|
-
return if
|
|
323
|
+
return if exception_list.nil?
|
|
321
324
|
|
|
322
|
-
properties = { '$exception_list' =>
|
|
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' =>
|
|
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.
|
|
92
|
-
|
|
93
|
-
|
|
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]
|
data/lib/posthog/version.rb
CHANGED