sentry-ruby 5.1.0 → 5.4.2

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.
Files changed (65) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +11 -0
  3. data/.rspec +3 -0
  4. data/.yardopts +2 -0
  5. data/CHANGELOG.md +313 -0
  6. data/CODE_OF_CONDUCT.md +74 -0
  7. data/Gemfile +27 -0
  8. data/Makefile +4 -0
  9. data/README.md +8 -7
  10. data/Rakefile +13 -0
  11. data/bin/console +18 -0
  12. data/bin/setup +8 -0
  13. data/lib/sentry/background_worker.rb +72 -0
  14. data/lib/sentry/backtrace.rb +124 -0
  15. data/lib/sentry/breadcrumb/sentry_logger.rb +90 -0
  16. data/lib/sentry/breadcrumb.rb +70 -0
  17. data/lib/sentry/breadcrumb_buffer.rb +64 -0
  18. data/lib/sentry/client.rb +190 -0
  19. data/lib/sentry/configuration.rb +502 -0
  20. data/lib/sentry/core_ext/object/deep_dup.rb +61 -0
  21. data/lib/sentry/core_ext/object/duplicable.rb +155 -0
  22. data/lib/sentry/dsn.rb +53 -0
  23. data/lib/sentry/envelope.rb +96 -0
  24. data/lib/sentry/error_event.rb +38 -0
  25. data/lib/sentry/event.rb +178 -0
  26. data/lib/sentry/exceptions.rb +9 -0
  27. data/lib/sentry/hub.rb +220 -0
  28. data/lib/sentry/integrable.rb +26 -0
  29. data/lib/sentry/interface.rb +16 -0
  30. data/lib/sentry/interfaces/exception.rb +43 -0
  31. data/lib/sentry/interfaces/request.rb +144 -0
  32. data/lib/sentry/interfaces/single_exception.rb +57 -0
  33. data/lib/sentry/interfaces/stacktrace.rb +87 -0
  34. data/lib/sentry/interfaces/stacktrace_builder.rb +79 -0
  35. data/lib/sentry/interfaces/threads.rb +42 -0
  36. data/lib/sentry/linecache.rb +47 -0
  37. data/lib/sentry/logger.rb +20 -0
  38. data/lib/sentry/net/http.rb +115 -0
  39. data/lib/sentry/rack/capture_exceptions.rb +80 -0
  40. data/lib/sentry/rack.rb +5 -0
  41. data/lib/sentry/rake.rb +41 -0
  42. data/lib/sentry/redis.rb +90 -0
  43. data/lib/sentry/release_detector.rb +39 -0
  44. data/lib/sentry/scope.rb +295 -0
  45. data/lib/sentry/session.rb +35 -0
  46. data/lib/sentry/session_flusher.rb +90 -0
  47. data/lib/sentry/span.rb +226 -0
  48. data/lib/sentry/test_helper.rb +76 -0
  49. data/lib/sentry/transaction.rb +206 -0
  50. data/lib/sentry/transaction_event.rb +29 -0
  51. data/lib/sentry/transport/configuration.rb +25 -0
  52. data/lib/sentry/transport/dummy_transport.rb +21 -0
  53. data/lib/sentry/transport/http_transport.rb +175 -0
  54. data/lib/sentry/transport.rb +210 -0
  55. data/lib/sentry/utils/argument_checking_helper.rb +13 -0
  56. data/lib/sentry/utils/custom_inspection.rb +14 -0
  57. data/lib/sentry/utils/exception_cause_chain.rb +20 -0
  58. data/lib/sentry/utils/logging_helper.rb +26 -0
  59. data/lib/sentry/utils/real_ip.rb +84 -0
  60. data/lib/sentry/utils/request_id.rb +18 -0
  61. data/lib/sentry/version.rb +5 -0
  62. data/lib/sentry-ruby.rb +505 -0
  63. data/sentry-ruby-core.gemspec +23 -0
  64. data/sentry-ruby.gemspec +24 -0
  65. metadata +64 -16
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rake"
4
+ require "rake/task"
5
+
6
+ module Sentry
7
+ module Rake
8
+ module Application
9
+ # @api private
10
+ def display_error_message(ex)
11
+ Sentry.capture_exception(ex) do |scope|
12
+ task_name = top_level_tasks.join(' ')
13
+ scope.set_transaction_name(task_name)
14
+ scope.set_tag("rake_task", task_name)
15
+ end if Sentry.initialized? && !Sentry.configuration.skip_rake_integration
16
+
17
+ super
18
+ end
19
+ end
20
+
21
+ module Task
22
+ # @api private
23
+ def execute(args=nil)
24
+ return super unless Sentry.initialized? && Sentry.get_current_hub
25
+
26
+ super
27
+ end
28
+ end
29
+ end
30
+ end
31
+
32
+ # @api private
33
+ module Rake
34
+ class Application
35
+ prepend(Sentry::Rake::Application)
36
+ end
37
+
38
+ class Task
39
+ prepend(Sentry::Rake::Task)
40
+ end
41
+ end
@@ -0,0 +1,90 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sentry
4
+ # @api private
5
+ class Redis
6
+ OP_NAME = "db.redis.command"
7
+ LOGGER_NAME = :redis_logger
8
+
9
+ def initialize(commands, host, port, db)
10
+ @commands, @host, @port, @db = commands, host, port, db
11
+ end
12
+
13
+ def instrument
14
+ return yield unless Sentry.initialized?
15
+
16
+ record_span do
17
+ yield.tap do
18
+ record_breadcrumb
19
+ end
20
+ end
21
+ end
22
+
23
+ private
24
+
25
+ attr_reader :commands, :host, :port, :db
26
+
27
+ def record_span
28
+ return yield unless (transaction = Sentry.get_current_scope.get_transaction) && transaction.sampled
29
+
30
+ sentry_span = transaction.start_child(op: OP_NAME, start_timestamp: Sentry.utc_now.to_f)
31
+
32
+ yield.tap do
33
+ sentry_span.set_description(commands_description)
34
+ sentry_span.set_data(:server, server_description)
35
+ sentry_span.set_timestamp(Sentry.utc_now.to_f)
36
+ end
37
+ end
38
+
39
+ def record_breadcrumb
40
+ return unless Sentry.configuration.breadcrumbs_logger.include?(LOGGER_NAME)
41
+
42
+ Sentry.add_breadcrumb(
43
+ Sentry::Breadcrumb.new(
44
+ level: :info,
45
+ category: OP_NAME,
46
+ type: :info,
47
+ data: {
48
+ commands: parsed_commands,
49
+ server: server_description
50
+ }
51
+ )
52
+ )
53
+ end
54
+
55
+ def commands_description
56
+ parsed_commands.map do |statement|
57
+ statement.values.join(" ").strip
58
+ end.join(", ")
59
+ end
60
+
61
+ def parsed_commands
62
+ commands.map do |statement|
63
+ command, key, *arguments = statement
64
+
65
+ { command: command.to_s.upcase, key: key }.tap do |command_set|
66
+ command_set[:arguments] = arguments.join(" ") if Sentry.configuration.send_default_pii
67
+ end
68
+ end
69
+ end
70
+
71
+ def server_description
72
+ "#{host}:#{port}/#{db}"
73
+ end
74
+
75
+ module Client
76
+ def logging(commands, &block)
77
+ Sentry::Redis.new(commands, host, port, db).instrument do
78
+ super
79
+ end
80
+ end
81
+ end
82
+ end
83
+ end
84
+
85
+ if defined?(::Redis::Client)
86
+ Sentry.register_patch do
87
+ patch = Sentry::Redis::Client
88
+ Redis::Client.prepend(patch) unless Redis::Client.ancestors.include?(patch)
89
+ end
90
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sentry
4
+ # @api private
5
+ class ReleaseDetector
6
+ class << self
7
+ def detect_release(project_root:, running_on_heroku:)
8
+ detect_release_from_env ||
9
+ detect_release_from_git ||
10
+ detect_release_from_capistrano(project_root) ||
11
+ detect_release_from_heroku(running_on_heroku)
12
+ end
13
+
14
+ def detect_release_from_heroku(running_on_heroku)
15
+ return unless running_on_heroku
16
+ ENV['HEROKU_SLUG_COMMIT']
17
+ end
18
+
19
+ def detect_release_from_capistrano(project_root)
20
+ revision_file = File.join(project_root, 'REVISION')
21
+ revision_log = File.join(project_root, '..', 'revisions.log')
22
+
23
+ if File.exist?(revision_file)
24
+ File.read(revision_file).strip
25
+ elsif File.exist?(revision_log)
26
+ File.open(revision_log).to_a.last.strip.sub(/.*as release ([0-9]+).*/, '\1')
27
+ end
28
+ end
29
+
30
+ def detect_release_from_git
31
+ Sentry.sys_command("git rev-parse --short HEAD") if File.directory?(".git")
32
+ end
33
+
34
+ def detect_release_from_env
35
+ ENV['SENTRY_RELEASE']
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,295 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "sentry/breadcrumb_buffer"
4
+ require "etc"
5
+
6
+ module Sentry
7
+ class Scope
8
+ include ArgumentCheckingHelper
9
+
10
+ ATTRIBUTES = [:transaction_names, :contexts, :extra, :tags, :user, :level, :breadcrumbs, :fingerprint, :event_processors, :rack_env, :span, :session]
11
+
12
+ attr_reader(*ATTRIBUTES)
13
+
14
+ # @param max_breadcrumbs [Integer] the maximum number of breadcrumbs to be stored in the scope.
15
+ def initialize(max_breadcrumbs: nil)
16
+ @max_breadcrumbs = max_breadcrumbs
17
+ set_default_value
18
+ end
19
+
20
+ # Resets the scope's attributes to defaults.
21
+ # @return [void]
22
+ def clear
23
+ set_default_value
24
+ end
25
+
26
+ # Applies stored attributes and event processors to the given event.
27
+ # @param event [Event]
28
+ # @param hint [Hash] the hint data that'll be passed to event processors.
29
+ # @return [Event]
30
+ def apply_to_event(event, hint = nil)
31
+ event.tags = tags.merge(event.tags)
32
+ event.user = user.merge(event.user)
33
+ event.extra = extra.merge(event.extra)
34
+ event.contexts = contexts.merge(event.contexts)
35
+ event.transaction = transaction_name if transaction_name
36
+
37
+ if span
38
+ event.contexts[:trace] = span.get_trace_context
39
+ end
40
+
41
+ event.fingerprint = fingerprint
42
+ event.level = level
43
+ event.breadcrumbs = breadcrumbs
44
+ event.rack_env = rack_env if rack_env
45
+
46
+ unless @event_processors.empty?
47
+ @event_processors.each do |processor_block|
48
+ event = processor_block.call(event, hint)
49
+ end
50
+ end
51
+
52
+ event
53
+ end
54
+
55
+ # Adds the breadcrumb to the scope's breadcrumbs buffer.
56
+ # @param breadcrumb [Breadcrumb]
57
+ # @return [void]
58
+ def add_breadcrumb(breadcrumb)
59
+ breadcrumbs.record(breadcrumb)
60
+ end
61
+
62
+ # Clears the scope's breadcrumbs buffer
63
+ # @return [void]
64
+ def clear_breadcrumbs
65
+ set_new_breadcrumb_buffer
66
+ end
67
+
68
+ # @return [Scope]
69
+ def dup
70
+ copy = super
71
+ copy.breadcrumbs = breadcrumbs.dup
72
+ copy.contexts = contexts.deep_dup
73
+ copy.extra = extra.deep_dup
74
+ copy.tags = tags.deep_dup
75
+ copy.user = user.deep_dup
76
+ copy.transaction_names = transaction_names.deep_dup
77
+ copy.fingerprint = fingerprint.deep_dup
78
+ copy.span = span.deep_dup
79
+ copy.session = session.deep_dup
80
+ copy
81
+ end
82
+
83
+ # Updates the scope's data from a given scope.
84
+ # @param scope [Scope]
85
+ # @return [void]
86
+ def update_from_scope(scope)
87
+ self.breadcrumbs = scope.breadcrumbs
88
+ self.contexts = scope.contexts
89
+ self.extra = scope.extra
90
+ self.tags = scope.tags
91
+ self.user = scope.user
92
+ self.transaction_names = scope.transaction_names
93
+ self.fingerprint = scope.fingerprint
94
+ self.span = scope.span
95
+ end
96
+
97
+ # Updates the scope's data from the given options.
98
+ # @param contexts [Hash]
99
+ # @param extras [Hash]
100
+ # @param tags [Hash]
101
+ # @param user [Hash]
102
+ # @param level [String, Symbol]
103
+ # @param fingerprint [Array]
104
+ # @return [void]
105
+ def update_from_options(
106
+ contexts: nil,
107
+ extra: nil,
108
+ tags: nil,
109
+ user: nil,
110
+ level: nil,
111
+ fingerprint: nil
112
+ )
113
+ self.contexts.merge!(contexts) if contexts
114
+ self.extra.merge!(extra) if extra
115
+ self.tags.merge!(tags) if tags
116
+ self.user = user if user
117
+ self.level = level if level
118
+ self.fingerprint = fingerprint if fingerprint
119
+ end
120
+
121
+ # Sets the scope's rack_env attribute.
122
+ # @param env [Hash]
123
+ # @return [Hash]
124
+ def set_rack_env(env)
125
+ env = env || {}
126
+ @rack_env = env
127
+ end
128
+
129
+ # Sets the scope's span attribute.
130
+ # @param span [Span]
131
+ # @return [Span]
132
+ def set_span(span)
133
+ check_argument_type!(span, Span)
134
+ @span = span
135
+ end
136
+
137
+ # @!macro set_user
138
+ def set_user(user_hash)
139
+ check_argument_type!(user_hash, Hash)
140
+ @user = user_hash
141
+ end
142
+
143
+ # @!macro set_extras
144
+ def set_extras(extras_hash)
145
+ check_argument_type!(extras_hash, Hash)
146
+ @extra.merge!(extras_hash)
147
+ end
148
+
149
+ # Adds a new key-value pair to current extras.
150
+ # @param key [String, Symbol]
151
+ # @param value [Object]
152
+ # @return [Hash]
153
+ def set_extra(key, value)
154
+ set_extras(key => value)
155
+ end
156
+
157
+ # @!macro set_tags
158
+ def set_tags(tags_hash)
159
+ check_argument_type!(tags_hash, Hash)
160
+ @tags.merge!(tags_hash)
161
+ end
162
+
163
+ # Adds a new key-value pair to current tags.
164
+ # @param key [String, Symbol]
165
+ # @param value [Object]
166
+ # @return [Hash]
167
+ def set_tag(key, value)
168
+ set_tags(key => value)
169
+ end
170
+
171
+ # Updates the scope's contexts attribute by merging with the old value.
172
+ # @param contexts [Hash]
173
+ # @return [Hash]
174
+ def set_contexts(contexts_hash)
175
+ check_argument_type!(contexts_hash, Hash)
176
+ @contexts.merge!(contexts_hash) do |key, old, new|
177
+ old.merge(new)
178
+ end
179
+ end
180
+
181
+ # @!macro set_context
182
+ def set_context(key, value)
183
+ check_argument_type!(value, Hash)
184
+ set_contexts(key => value)
185
+ end
186
+
187
+ # Sets the scope's level attribute.
188
+ # @param level [String, Symbol]
189
+ # @return [void]
190
+ def set_level(level)
191
+ @level = level
192
+ end
193
+
194
+ # Appends a new transaction name to the scope.
195
+ # The "transaction" here does not refer to `Transaction` objects.
196
+ # @param transaction_name [String]
197
+ # @return [void]
198
+ def set_transaction_name(transaction_name)
199
+ @transaction_names << transaction_name
200
+ end
201
+
202
+ # Sets the currently active session on the scope.
203
+ # @param session [Session, nil]
204
+ # @return [void]
205
+ def set_session(session)
206
+ @session = session
207
+ end
208
+
209
+ # Returns current transaction name.
210
+ # The "transaction" here does not refer to `Transaction` objects.
211
+ # @return [String, nil]
212
+ def transaction_name
213
+ @transaction_names.last
214
+ end
215
+
216
+ # Returns the associated Transaction object.
217
+ # @return [Transaction, nil]
218
+ def get_transaction
219
+ span.transaction if span
220
+ end
221
+
222
+ # Returns the associated Span object.
223
+ # @return [Span, nil]
224
+ def get_span
225
+ span
226
+ end
227
+
228
+ # Sets the scope's fingerprint attribute.
229
+ # @param fingerprint [Array]
230
+ # @return [Array]
231
+ def set_fingerprint(fingerprint)
232
+ check_argument_type!(fingerprint, Array)
233
+
234
+ @fingerprint = fingerprint
235
+ end
236
+
237
+ # Adds a new event processor [Proc] to the scope.
238
+ # @param block [Proc]
239
+ # @return [void]
240
+ def add_event_processor(&block)
241
+ @event_processors << block
242
+ end
243
+
244
+ protected
245
+
246
+ # for duplicating scopes internally
247
+ attr_writer(*ATTRIBUTES)
248
+
249
+ private
250
+
251
+ def set_default_value
252
+ @contexts = { :os => self.class.os_context, :runtime => self.class.runtime_context }
253
+ @extra = {}
254
+ @tags = {}
255
+ @user = {}
256
+ @level = :error
257
+ @fingerprint = []
258
+ @transaction_names = []
259
+ @event_processors = []
260
+ @rack_env = {}
261
+ @span = nil
262
+ @session = nil
263
+ set_new_breadcrumb_buffer
264
+ end
265
+
266
+ def set_new_breadcrumb_buffer
267
+ @breadcrumbs = BreadcrumbBuffer.new(@max_breadcrumbs)
268
+ end
269
+
270
+ class << self
271
+ # @return [Hash]
272
+ def os_context
273
+ @os_context ||=
274
+ begin
275
+ uname = Etc.uname
276
+ {
277
+ name: uname[:sysname] || RbConfig::CONFIG["host_os"],
278
+ version: uname[:version],
279
+ build: uname[:release],
280
+ kernel_version: uname[:version]
281
+ }
282
+ end
283
+ end
284
+
285
+ # @return [Hash]
286
+ def runtime_context
287
+ @runtime_context ||= {
288
+ name: RbConfig::CONFIG["ruby_install_name"],
289
+ version: RUBY_DESCRIPTION || Sentry.sys_command("ruby -v")
290
+ }
291
+ end
292
+ end
293
+
294
+ end
295
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sentry
4
+ class Session
5
+ attr_reader :started, :status
6
+
7
+ # TODO-neel add :crashed after adding handled mechanism
8
+ STATUSES = %i(ok errored exited)
9
+ AGGREGATE_STATUSES = %i(errored exited)
10
+
11
+ def initialize
12
+ @started = Sentry.utc_now
13
+ @status = :ok
14
+ end
15
+
16
+ # TODO-neel add :crashed after adding handled mechanism
17
+ def update_from_exception(_exception = nil)
18
+ @status = :errored
19
+ end
20
+
21
+ def close
22
+ @status = :exited if @status == :ok
23
+ end
24
+
25
+ # truncate seconds from the timestamp since we only care about
26
+ # minute level granularity for aggregation
27
+ def aggregation_key
28
+ Time.utc(started.year, started.month, started.day, started.hour, started.min)
29
+ end
30
+
31
+ def deep_dup
32
+ dup
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,90 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sentry
4
+ class SessionFlusher
5
+ include LoggingHelper
6
+
7
+ FLUSH_INTERVAL = 60
8
+
9
+ def initialize(configuration, client)
10
+ @thread = nil
11
+ @exited = false
12
+ @client = client
13
+ @pending_aggregates = {}
14
+ @release = configuration.release
15
+ @environment = configuration.environment
16
+ @logger = configuration.logger
17
+
18
+ log_debug("[Sessions] Sessions won't be captured without a valid release") unless @release
19
+ end
20
+
21
+ def flush
22
+ return if @pending_aggregates.empty?
23
+ envelope = pending_envelope
24
+
25
+ Sentry.background_worker.perform do
26
+ @client.transport.send_envelope(envelope)
27
+ end
28
+
29
+ @pending_aggregates = {}
30
+ end
31
+
32
+ def add_session(session)
33
+ return if @exited
34
+ return unless @release
35
+
36
+ begin
37
+ ensure_thread
38
+ rescue ThreadError
39
+ log_debug("Session flusher thread creation failed")
40
+ @exited = true
41
+ return
42
+ end
43
+
44
+ return unless Session::AGGREGATE_STATUSES.include?(session.status)
45
+ @pending_aggregates[session.aggregation_key] ||= init_aggregates(session.aggregation_key)
46
+ @pending_aggregates[session.aggregation_key][session.status] += 1
47
+ end
48
+
49
+ def kill
50
+ log_debug("Killing session flusher")
51
+
52
+ @exited = true
53
+ @thread&.kill
54
+ end
55
+
56
+ private
57
+
58
+ def init_aggregates(aggregation_key)
59
+ aggregates = { started: aggregation_key.iso8601 }
60
+ Session::AGGREGATE_STATUSES.each { |k| aggregates[k] = 0 }
61
+ aggregates
62
+ end
63
+
64
+ def pending_envelope
65
+ envelope = Envelope.new
66
+
67
+ header = { type: 'sessions' }
68
+ payload = { attrs: attrs, aggregates: @pending_aggregates.values }
69
+
70
+ envelope.add_item(header, payload)
71
+ envelope
72
+ end
73
+
74
+ def attrs
75
+ { release: @release, environment: @environment }
76
+ end
77
+
78
+ def ensure_thread
79
+ return if @thread&.alive?
80
+
81
+ @thread = Thread.new do
82
+ loop do
83
+ sleep(FLUSH_INTERVAL)
84
+ flush
85
+ end
86
+ end
87
+ end
88
+
89
+ end
90
+ end