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,226 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "securerandom"
4
+
5
+ module Sentry
6
+ class Span
7
+ STATUS_MAP = {
8
+ 400 => "invalid_argument",
9
+ 401 => "unauthenticated",
10
+ 403 => "permission_denied",
11
+ 404 => "not_found",
12
+ 409 => "already_exists",
13
+ 429 => "resource_exhausted",
14
+ 499 => "cancelled",
15
+ 500 => "internal_error",
16
+ 501 => "unimplemented",
17
+ 503 => "unavailable",
18
+ 504 => "deadline_exceeded"
19
+ }
20
+
21
+ # An uuid that can be used to identify a trace.
22
+ # @return [String]
23
+ attr_reader :trace_id
24
+ # An uuid that can be used to identify the span.
25
+ # @return [String]
26
+ attr_reader :span_id
27
+ # Span parent's span_id.
28
+ # @return [String]
29
+ attr_reader :parent_span_id
30
+ # Sampling result of the span.
31
+ # @return [Boolean, nil]
32
+ attr_reader :sampled
33
+ # Starting timestamp of the span.
34
+ # @return [Float]
35
+ attr_reader :start_timestamp
36
+ # Finishing timestamp of the span.
37
+ # @return [Float]
38
+ attr_reader :timestamp
39
+ # Span description
40
+ # @return [String]
41
+ attr_reader :description
42
+ # Span operation
43
+ # @return [String]
44
+ attr_reader :op
45
+ # Span status
46
+ # @return [String]
47
+ attr_reader :status
48
+ # Span tags
49
+ # @return [Hash]
50
+ attr_reader :tags
51
+ # Span data
52
+ # @return [Hash]
53
+ attr_reader :data
54
+
55
+ # The SpanRecorder the current span belongs to.
56
+ # SpanRecorder holds all spans under the same Transaction object (including the Transaction itself).
57
+ # @return [SpanRecorder]
58
+ attr_accessor :span_recorder
59
+
60
+ # The Transaction object the Span belongs to.
61
+ # Every span needs to be attached to a Transaction and their child spans will also inherit the same transaction.
62
+ # @return [Transaction]
63
+ attr_accessor :transaction
64
+
65
+ def initialize(
66
+ description: nil,
67
+ op: nil,
68
+ status: nil,
69
+ trace_id: nil,
70
+ parent_span_id: nil,
71
+ sampled: nil,
72
+ start_timestamp: nil,
73
+ timestamp: nil
74
+ )
75
+ @trace_id = trace_id || SecureRandom.uuid.delete("-")
76
+ @span_id = SecureRandom.hex(8)
77
+ @parent_span_id = parent_span_id
78
+ @sampled = sampled
79
+ @start_timestamp = start_timestamp || Sentry.utc_now.to_f
80
+ @timestamp = timestamp
81
+ @description = description
82
+ @op = op
83
+ @status = status
84
+ @data = {}
85
+ @tags = {}
86
+ end
87
+
88
+ # Finishes the span by adding a timestamp.
89
+ # @return [self]
90
+ def finish
91
+ # already finished
92
+ return if @timestamp
93
+
94
+ @timestamp = Sentry.utc_now.to_f
95
+ self
96
+ end
97
+
98
+ # Generates a trace string that can be used to connect other transactions.
99
+ # @return [String]
100
+ def to_sentry_trace
101
+ sampled_flag = ""
102
+ sampled_flag = @sampled ? 1 : 0 unless @sampled.nil?
103
+
104
+ "#{@trace_id}-#{@span_id}-#{sampled_flag}"
105
+ end
106
+
107
+ # @return [Hash]
108
+ def to_hash
109
+ {
110
+ trace_id: @trace_id,
111
+ span_id: @span_id,
112
+ parent_span_id: @parent_span_id,
113
+ start_timestamp: @start_timestamp,
114
+ timestamp: @timestamp,
115
+ description: @description,
116
+ op: @op,
117
+ status: @status,
118
+ tags: @tags,
119
+ data: @data
120
+ }
121
+ end
122
+
123
+ # Returns the span's context that can be used to embed in an Event.
124
+ # @return [Hash]
125
+ def get_trace_context
126
+ {
127
+ trace_id: @trace_id,
128
+ span_id: @span_id,
129
+ parent_span_id: @parent_span_id,
130
+ description: @description,
131
+ op: @op,
132
+ status: @status
133
+ }
134
+ end
135
+
136
+ # Starts a child span with given attributes.
137
+ # @param attributes [Hash] the attributes for the child span.
138
+ def start_child(**attributes)
139
+ attributes = attributes.dup.merge(trace_id: @trace_id, parent_span_id: @span_id, sampled: @sampled)
140
+ new_span = Span.new(**attributes)
141
+ new_span.transaction = transaction
142
+ new_span.span_recorder = span_recorder
143
+
144
+ if span_recorder
145
+ span_recorder.add(new_span)
146
+ end
147
+
148
+ new_span
149
+ end
150
+
151
+ # Starts a child span, yield it to the given block, and then finish the span after the block is executed.
152
+ # @example
153
+ # span.with_child_span do |child_span|
154
+ # # things happen here will be recorded in a child span
155
+ # end
156
+ #
157
+ # @param attributes [Hash] the attributes for the child span.
158
+ # @param block [Proc] the action to be recorded in the child span.
159
+ # @yieldparam child_span [Span]
160
+ def with_child_span(**attributes, &block)
161
+ child_span = start_child(**attributes)
162
+
163
+ yield(child_span)
164
+
165
+ child_span.finish
166
+ end
167
+
168
+ def deep_dup
169
+ dup
170
+ end
171
+
172
+ # Sets the span's operation.
173
+ # @param op [String] operation of the span.
174
+ def set_op(op)
175
+ @op = op
176
+ end
177
+
178
+ # Sets the span's description.
179
+ # @param description [String] description of the span.
180
+ def set_description(description)
181
+ @description = description
182
+ end
183
+
184
+
185
+ # Sets the span's status.
186
+ # @param satus [String] status of the span.
187
+ def set_status(status)
188
+ @status = status
189
+ end
190
+
191
+ # Sets the span's finish timestamp.
192
+ # @param timestamp [Float] finished time in float format (most precise).
193
+ def set_timestamp(timestamp)
194
+ @timestamp = timestamp
195
+ end
196
+
197
+ # Sets the span's status with given http status code.
198
+ # @param status_code [String] example: "500".
199
+ def set_http_status(status_code)
200
+ status_code = status_code.to_i
201
+ set_data("status_code", status_code)
202
+
203
+ status =
204
+ if status_code >= 200 && status_code < 299
205
+ "ok"
206
+ else
207
+ STATUS_MAP[status_code]
208
+ end
209
+ set_status(status)
210
+ end
211
+
212
+ # Inserts a key-value pair to the span's data payload.
213
+ # @param key [String, Symbol]
214
+ # @param value [Object]
215
+ def set_data(key, value)
216
+ @data[key] = value
217
+ end
218
+
219
+ # Sets a tag to the span.
220
+ # @param key [String, Symbol]
221
+ # @param value [String]
222
+ def set_tag(key, value)
223
+ @tags[key] = value
224
+ end
225
+ end
226
+ end
@@ -0,0 +1,76 @@
1
+ module Sentry
2
+ module TestHelper
3
+ DUMMY_DSN = 'http://12345:67890@sentry.localdomain/sentry/42'
4
+
5
+ # Alters the existing SDK configuration with test-suitable options. Mainly:
6
+ # - Sets a dummy DSN instead of `nil` or an actual DSN.
7
+ # - Sets the transport to DummyTransport, which allows easy access to the captured events.
8
+ # - Disables background worker.
9
+ # - Makes sure the SDK is enabled under the current environment ("test" in most cases).
10
+ #
11
+ # It should be called **before** every test case.
12
+ #
13
+ # @yieldparam config [Configuration]
14
+ # @return [void]
15
+ def setup_sentry_test(&block)
16
+ raise "please make sure the SDK is initialized for testing" unless Sentry.initialized?
17
+ copied_config = Sentry.configuration.dup
18
+ # configure dummy DSN, so the events will not be sent to the actual service
19
+ copied_config.dsn = DUMMY_DSN
20
+ # set transport to DummyTransport, so we can easily intercept the captured events
21
+ copied_config.transport.transport_class = Sentry::DummyTransport
22
+ # make sure SDK allows sending under the current environment
23
+ copied_config.enabled_environments << copied_config.environment unless copied_config.enabled_environments.include?(copied_config.environment)
24
+ # disble async event sending
25
+ copied_config.background_worker_threads = 0
26
+
27
+ # user can overwrite some of the configs, with a few exceptions like:
28
+ # - capture_exception_frame_locals
29
+ # - auto_session_tracking
30
+ block&.call(copied_config)
31
+
32
+ test_client = Sentry::Client.new(copied_config)
33
+ Sentry.get_current_hub.bind_client(test_client)
34
+ end
35
+
36
+ # Clears all stored events and envelopes.
37
+ # It should be called **after** every test case.
38
+ # @return [void]
39
+ def teardown_sentry_test
40
+ return unless Sentry.initialized?
41
+
42
+ sentry_transport.events = []
43
+ sentry_transport.envelopes = []
44
+ end
45
+
46
+ # @return [Transport]
47
+ def sentry_transport
48
+ Sentry.get_current_client.transport
49
+ end
50
+
51
+ # Returns the captured event objects.
52
+ # @return [Array<Event>]
53
+ def sentry_events
54
+ sentry_transport.events
55
+ end
56
+
57
+ # Returns the captured envelope objects.
58
+ # @return [Array<Envelope>]
59
+ def sentry_envelopes
60
+ sentry_transport.envelopes
61
+ end
62
+
63
+ # Returns the last captured event object.
64
+ # @return [Event, nil]
65
+ def last_sentry_event
66
+ sentry_events.last
67
+ end
68
+
69
+ # Extracts SDK's internal exception container (not actual exception objects) from an given event.
70
+ # @return [Array<Sentry::SingleExceptionInterface>]
71
+ def extract_sentry_exceptions(event)
72
+ event&.exception&.values || []
73
+ end
74
+ end
75
+ end
76
+
@@ -0,0 +1,206 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sentry
4
+ class Transaction < Span
5
+ SENTRY_TRACE_REGEXP = Regexp.new(
6
+ "^[ \t]*" + # whitespace
7
+ "([0-9a-f]{32})?" + # trace_id
8
+ "-?([0-9a-f]{16})?" + # span_id
9
+ "-?([01])?" + # sampled
10
+ "[ \t]*$" # whitespace
11
+ )
12
+ UNLABELD_NAME = "<unlabeled transaction>".freeze
13
+ MESSAGE_PREFIX = "[Tracing]"
14
+
15
+ include LoggingHelper
16
+
17
+ # The name of the transaction.
18
+ # @return [String]
19
+ attr_reader :name
20
+
21
+ # The sampling decision of the parent transaction, which will be considered when making the current transaction's sampling decision.
22
+ # @return [String]
23
+ attr_reader :parent_sampled
24
+
25
+ # @deprecated Use Sentry.get_current_hub instead.
26
+ attr_reader :hub
27
+
28
+ # @deprecated Use Sentry.configuration instead.
29
+ attr_reader :configuration
30
+
31
+ # @deprecated Use Sentry.logger instead.
32
+ attr_reader :logger
33
+
34
+ def initialize(name: nil, parent_sampled: nil, hub:, **options)
35
+ super(**options)
36
+
37
+ @name = name
38
+ @parent_sampled = parent_sampled
39
+ @transaction = self
40
+ @hub = hub
41
+ @configuration = hub.configuration # to be removed
42
+ @tracing_enabled = hub.configuration.tracing_enabled?
43
+ @traces_sampler = hub.configuration.traces_sampler
44
+ @traces_sample_rate = hub.configuration.traces_sample_rate
45
+ @logger = hub.configuration.logger
46
+ init_span_recorder
47
+ end
48
+
49
+ # Initalizes a Transaction instance with a Sentry trace string from another transaction (usually from an external request).
50
+ #
51
+ # The original transaction will become the parent of the new Transaction instance. And they will share the same `trace_id`.
52
+ #
53
+ # The child transaction will also store the parent's sampling decision in its `parent_sampled` attribute.
54
+ # @param sentry_trace [String] the trace string from the previous transaction.
55
+ # @param hub [Hub] the hub that'll be responsible for sending this transaction when it's finished.
56
+ # @param options [Hash] the options you want to use to initialize a Transaction instance.
57
+ # @return [Transaction, nil]
58
+ def self.from_sentry_trace(sentry_trace, hub: Sentry.get_current_hub, **options)
59
+ return unless hub.configuration.tracing_enabled?
60
+ return unless sentry_trace
61
+
62
+ match = SENTRY_TRACE_REGEXP.match(sentry_trace)
63
+ return if match.nil?
64
+ trace_id, parent_span_id, sampled_flag = match[1..3]
65
+
66
+ parent_sampled =
67
+ if sampled_flag.nil?
68
+ nil
69
+ else
70
+ sampled_flag != "0"
71
+ end
72
+
73
+ new(trace_id: trace_id, parent_span_id: parent_span_id, parent_sampled: parent_sampled, hub: hub, **options)
74
+ end
75
+
76
+ # @return [Hash]
77
+ def to_hash
78
+ hash = super
79
+ hash.merge!(name: @name, sampled: @sampled, parent_sampled: @parent_sampled)
80
+ hash
81
+ end
82
+
83
+ # @return [Transaction]
84
+ def deep_dup
85
+ copy = super
86
+ copy.init_span_recorder(@span_recorder.max_length)
87
+
88
+ @span_recorder.spans.each do |span|
89
+ # span_recorder's first span is the current span, which should not be added to the copy's spans
90
+ next if span == self
91
+ copy.span_recorder.add(span.dup)
92
+ end
93
+
94
+ copy
95
+ end
96
+
97
+ # Sets initial sampling decision of the transaction.
98
+ # @param sampling_context [Hash] a context Hash that'll be passed to `traces_sampler` (if provided).
99
+ # @return [void]
100
+ def set_initial_sample_decision(sampling_context:)
101
+ unless @tracing_enabled
102
+ @sampled = false
103
+ return
104
+ end
105
+
106
+ return unless @sampled.nil?
107
+
108
+ sample_rate =
109
+ if @traces_sampler.is_a?(Proc)
110
+ @traces_sampler.call(sampling_context)
111
+ elsif !sampling_context[:parent_sampled].nil?
112
+ sampling_context[:parent_sampled]
113
+ else
114
+ @traces_sample_rate
115
+ end
116
+
117
+ transaction_description = generate_transaction_description
118
+
119
+ unless [true, false].include?(sample_rate) || (sample_rate.is_a?(Numeric) && sample_rate >= 0.0 && sample_rate <= 1.0)
120
+ @sampled = false
121
+ log_warn("#{MESSAGE_PREFIX} Discarding #{transaction_description} because of invalid sample_rate: #{sample_rate}")
122
+ return
123
+ end
124
+
125
+ if sample_rate == 0.0 || sample_rate == false
126
+ @sampled = false
127
+ log_debug("#{MESSAGE_PREFIX} Discarding #{transaction_description} because traces_sampler returned 0 or false")
128
+ return
129
+ end
130
+
131
+ if sample_rate == true
132
+ @sampled = true
133
+ else
134
+ @sampled = Random.rand < sample_rate
135
+ end
136
+
137
+ if @sampled
138
+ log_debug("#{MESSAGE_PREFIX} Starting #{transaction_description}")
139
+ else
140
+ log_debug(
141
+ "#{MESSAGE_PREFIX} Discarding #{transaction_description} because it's not included in the random sample (sampling rate = #{sample_rate})"
142
+ )
143
+ end
144
+ end
145
+
146
+ # Finishes the transaction's recording and send it to Sentry.
147
+ # @param hub [Hub] the hub that'll send this transaction. (Deprecated)
148
+ # @return [TransactionEvent]
149
+ def finish(hub: nil)
150
+ if hub
151
+ log_warn(
152
+ <<~MSG
153
+ Specifying a different hub in `Transaction#finish` will be deprecated in version 5.0.
154
+ Please use `Hub#start_transaction` with the designated hub.
155
+ MSG
156
+ )
157
+ end
158
+
159
+ hub ||= @hub
160
+
161
+ super() # Span#finish doesn't take arguments
162
+
163
+ if @name.nil?
164
+ @name = UNLABELD_NAME
165
+ end
166
+
167
+ if @sampled
168
+ event = hub.current_client.event_from_transaction(self)
169
+ hub.capture_event(event)
170
+ else
171
+ hub.current_client.transport.record_lost_event(:sample_rate, 'transaction')
172
+ end
173
+ end
174
+
175
+ protected
176
+
177
+ def init_span_recorder(limit = 1000)
178
+ @span_recorder = SpanRecorder.new(limit)
179
+ @span_recorder.add(self)
180
+ end
181
+
182
+ private
183
+
184
+ def generate_transaction_description
185
+ result = op.nil? ? "" : "<#{@op}> "
186
+ result += "transaction"
187
+ result += " <#{@name}>" if @name
188
+ result
189
+ end
190
+
191
+ class SpanRecorder
192
+ attr_reader :max_length, :spans
193
+
194
+ def initialize(max_length)
195
+ @max_length = max_length
196
+ @spans = []
197
+ end
198
+
199
+ def add(span)
200
+ if @spans.count < @max_length
201
+ @spans << span
202
+ end
203
+ end
204
+ end
205
+ end
206
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sentry
4
+ # TransactionEvent represents events that carry transaction data (type: "transaction").
5
+ class TransactionEvent < Event
6
+ TYPE = "transaction"
7
+
8
+ # @return [<Array[Span]>]
9
+ attr_accessor :spans
10
+
11
+ # @return [Float, nil]
12
+ attr_reader :start_timestamp
13
+
14
+ # Sets the event's start_timestamp.
15
+ # @param time [Time, Float]
16
+ # @return [void]
17
+ def start_timestamp=(time)
18
+ @start_timestamp = time.is_a?(Time) ? time.to_f : time
19
+ end
20
+
21
+ # @return [Hash]
22
+ def to_hash
23
+ data = super
24
+ data[:spans] = @spans.map(&:to_hash) if @spans
25
+ data[:start_timestamp] = @start_timestamp
26
+ data
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sentry
4
+ class Transport
5
+ class Configuration
6
+ attr_accessor :timeout, :open_timeout, :proxy, :ssl, :ssl_ca_file, :ssl_verification, :encoding
7
+ attr_reader :transport_class
8
+
9
+ def initialize
10
+ @ssl_verification = true
11
+ @open_timeout = 1
12
+ @timeout = 2
13
+ @encoding = HTTPTransport::GZIP_ENCODING
14
+ end
15
+
16
+ def transport_class=(klass)
17
+ unless klass.is_a?(Class)
18
+ raise Sentry::Error.new("config.transport.transport_class must a class. got: #{klass.class}")
19
+ end
20
+
21
+ @transport_class = klass
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sentry
4
+ class DummyTransport < Transport
5
+ attr_accessor :events, :envelopes
6
+
7
+ def initialize(*)
8
+ super
9
+ @events = []
10
+ @envelopes = []
11
+ end
12
+
13
+ def send_event(event)
14
+ @events << event
15
+ end
16
+
17
+ def send_envelope(envelope)
18
+ @envelopes << envelope
19
+ end
20
+ end
21
+ end