sentry-ruby 5.4.0 → 5.4.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (64) 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/Rakefile +13 -0
  10. data/bin/console +18 -0
  11. data/bin/setup +8 -0
  12. data/lib/sentry/background_worker.rb +72 -0
  13. data/lib/sentry/backtrace.rb +124 -0
  14. data/lib/sentry/breadcrumb/sentry_logger.rb +90 -0
  15. data/lib/sentry/breadcrumb.rb +70 -0
  16. data/lib/sentry/breadcrumb_buffer.rb +64 -0
  17. data/lib/sentry/client.rb +190 -0
  18. data/lib/sentry/configuration.rb +502 -0
  19. data/lib/sentry/core_ext/object/deep_dup.rb +61 -0
  20. data/lib/sentry/core_ext/object/duplicable.rb +155 -0
  21. data/lib/sentry/dsn.rb +53 -0
  22. data/lib/sentry/envelope.rb +96 -0
  23. data/lib/sentry/error_event.rb +38 -0
  24. data/lib/sentry/event.rb +178 -0
  25. data/lib/sentry/exceptions.rb +9 -0
  26. data/lib/sentry/hub.rb +220 -0
  27. data/lib/sentry/integrable.rb +26 -0
  28. data/lib/sentry/interface.rb +16 -0
  29. data/lib/sentry/interfaces/exception.rb +43 -0
  30. data/lib/sentry/interfaces/request.rb +144 -0
  31. data/lib/sentry/interfaces/single_exception.rb +57 -0
  32. data/lib/sentry/interfaces/stacktrace.rb +87 -0
  33. data/lib/sentry/interfaces/stacktrace_builder.rb +79 -0
  34. data/lib/sentry/interfaces/threads.rb +42 -0
  35. data/lib/sentry/linecache.rb +47 -0
  36. data/lib/sentry/logger.rb +20 -0
  37. data/lib/sentry/net/http.rb +115 -0
  38. data/lib/sentry/rack/capture_exceptions.rb +80 -0
  39. data/lib/sentry/rack.rb +5 -0
  40. data/lib/sentry/rake.rb +41 -0
  41. data/lib/sentry/redis.rb +90 -0
  42. data/lib/sentry/release_detector.rb +39 -0
  43. data/lib/sentry/scope.rb +295 -0
  44. data/lib/sentry/session.rb +35 -0
  45. data/lib/sentry/session_flusher.rb +90 -0
  46. data/lib/sentry/span.rb +226 -0
  47. data/lib/sentry/test_helper.rb +76 -0
  48. data/lib/sentry/transaction.rb +206 -0
  49. data/lib/sentry/transaction_event.rb +29 -0
  50. data/lib/sentry/transport/configuration.rb +25 -0
  51. data/lib/sentry/transport/dummy_transport.rb +21 -0
  52. data/lib/sentry/transport/http_transport.rb +175 -0
  53. data/lib/sentry/transport.rb +210 -0
  54. data/lib/sentry/utils/argument_checking_helper.rb +13 -0
  55. data/lib/sentry/utils/custom_inspection.rb +14 -0
  56. data/lib/sentry/utils/exception_cause_chain.rb +20 -0
  57. data/lib/sentry/utils/logging_helper.rb +26 -0
  58. data/lib/sentry/utils/real_ip.rb +84 -0
  59. data/lib/sentry/utils/request_id.rb +18 -0
  60. data/lib/sentry/version.rb +5 -0
  61. data/lib/sentry-ruby.rb +505 -0
  62. data/sentry-ruby-core.gemspec +23 -0
  63. data/sentry-ruby.gemspec +24 -0
  64. metadata +63 -1
@@ -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