sentry-ruby 5.3.1 → 5.4.2

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 +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