sentry-ruby 5.3.1 → 5.16.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +11 -0
- data/.rspec +2 -0
- data/.yardopts +2 -0
- data/CHANGELOG.md +313 -0
- data/Gemfile +26 -0
- data/Makefile +4 -0
- data/README.md +11 -8
- data/Rakefile +20 -0
- data/bin/console +18 -0
- data/bin/setup +8 -0
- data/lib/sentry/background_worker.rb +79 -0
- data/lib/sentry/backpressure_monitor.rb +75 -0
- data/lib/sentry/backtrace.rb +124 -0
- data/lib/sentry/baggage.rb +70 -0
- data/lib/sentry/breadcrumb/sentry_logger.rb +90 -0
- data/lib/sentry/breadcrumb.rb +76 -0
- data/lib/sentry/breadcrumb_buffer.rb +64 -0
- data/lib/sentry/check_in_event.rb +60 -0
- data/lib/sentry/client.rb +248 -0
- data/lib/sentry/configuration.rb +650 -0
- data/lib/sentry/core_ext/object/deep_dup.rb +61 -0
- data/lib/sentry/core_ext/object/duplicable.rb +155 -0
- data/lib/sentry/cron/configuration.rb +23 -0
- data/lib/sentry/cron/monitor_check_ins.rb +75 -0
- data/lib/sentry/cron/monitor_config.rb +53 -0
- data/lib/sentry/cron/monitor_schedule.rb +42 -0
- data/lib/sentry/dsn.rb +53 -0
- data/lib/sentry/envelope.rb +93 -0
- data/lib/sentry/error_event.rb +38 -0
- data/lib/sentry/event.rb +156 -0
- data/lib/sentry/exceptions.rb +9 -0
- data/lib/sentry/hub.rb +316 -0
- data/lib/sentry/integrable.rb +32 -0
- data/lib/sentry/interface.rb +16 -0
- data/lib/sentry/interfaces/exception.rb +43 -0
- data/lib/sentry/interfaces/request.rb +134 -0
- data/lib/sentry/interfaces/single_exception.rb +67 -0
- data/lib/sentry/interfaces/stacktrace.rb +87 -0
- data/lib/sentry/interfaces/stacktrace_builder.rb +79 -0
- data/lib/sentry/interfaces/threads.rb +42 -0
- data/lib/sentry/linecache.rb +47 -0
- data/lib/sentry/logger.rb +20 -0
- data/lib/sentry/net/http.rb +106 -0
- data/lib/sentry/profiler.rb +233 -0
- data/lib/sentry/propagation_context.rb +134 -0
- data/lib/sentry/puma.rb +32 -0
- data/lib/sentry/rack/capture_exceptions.rb +79 -0
- data/lib/sentry/rack.rb +5 -0
- data/lib/sentry/rake.rb +28 -0
- data/lib/sentry/redis.rb +108 -0
- data/lib/sentry/release_detector.rb +39 -0
- data/lib/sentry/scope.rb +360 -0
- data/lib/sentry/session.rb +33 -0
- data/lib/sentry/session_flusher.rb +90 -0
- data/lib/sentry/span.rb +273 -0
- data/lib/sentry/test_helper.rb +84 -0
- data/lib/sentry/transaction.rb +359 -0
- data/lib/sentry/transaction_event.rb +80 -0
- data/lib/sentry/transport/configuration.rb +98 -0
- data/lib/sentry/transport/dummy_transport.rb +21 -0
- data/lib/sentry/transport/http_transport.rb +206 -0
- data/lib/sentry/transport/spotlight_transport.rb +50 -0
- data/lib/sentry/transport.rb +225 -0
- data/lib/sentry/utils/argument_checking_helper.rb +19 -0
- data/lib/sentry/utils/custom_inspection.rb +14 -0
- data/lib/sentry/utils/encoding_helper.rb +22 -0
- data/lib/sentry/utils/exception_cause_chain.rb +20 -0
- data/lib/sentry/utils/logging_helper.rb +26 -0
- data/lib/sentry/utils/real_ip.rb +84 -0
- data/lib/sentry/utils/request_id.rb +18 -0
- data/lib/sentry/version.rb +5 -0
- data/lib/sentry-ruby.rb +580 -0
- data/sentry-ruby-core.gemspec +23 -0
- data/sentry-ruby.gemspec +24 -0
- metadata +75 -16
@@ -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
|
data/lib/sentry/span.rb
ADDED
@@ -0,0 +1,273 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "securerandom"
|
4
|
+
|
5
|
+
module Sentry
|
6
|
+
class Span
|
7
|
+
|
8
|
+
# We will try to be consistent with OpenTelemetry on this front going forward.
|
9
|
+
# https://develop.sentry.dev/sdk/performance/span-data-conventions/
|
10
|
+
module DataConventions
|
11
|
+
URL = "url"
|
12
|
+
HTTP_STATUS_CODE = "http.response.status_code"
|
13
|
+
HTTP_QUERY = "http.query"
|
14
|
+
HTTP_METHOD = "http.request.method"
|
15
|
+
|
16
|
+
# An identifier for the database management system (DBMS) product being used.
|
17
|
+
# Example: postgresql
|
18
|
+
DB_SYSTEM = "db.system"
|
19
|
+
|
20
|
+
# The name of the database being accessed.
|
21
|
+
# For commands that switch the database, this should be set to the target database
|
22
|
+
# (even if the command fails).
|
23
|
+
# Example: myDatabase
|
24
|
+
DB_NAME = "db.name"
|
25
|
+
|
26
|
+
# Name of the database host.
|
27
|
+
# Example: example.com
|
28
|
+
SERVER_ADDRESS = "server.address"
|
29
|
+
|
30
|
+
# Logical server port number
|
31
|
+
# Example: 80; 8080; 443
|
32
|
+
SERVER_PORT = "server.port"
|
33
|
+
|
34
|
+
# Physical server IP address or Unix socket address.
|
35
|
+
# Example: 10.5.3.2
|
36
|
+
SERVER_SOCKET_ADDRESS = "server.socket.address"
|
37
|
+
|
38
|
+
# Physical server port.
|
39
|
+
# Recommended: If different than server.port.
|
40
|
+
# Example: 16456
|
41
|
+
SERVER_SOCKET_PORT = "server.socket.port"
|
42
|
+
end
|
43
|
+
|
44
|
+
STATUS_MAP = {
|
45
|
+
400 => "invalid_argument",
|
46
|
+
401 => "unauthenticated",
|
47
|
+
403 => "permission_denied",
|
48
|
+
404 => "not_found",
|
49
|
+
409 => "already_exists",
|
50
|
+
429 => "resource_exhausted",
|
51
|
+
499 => "cancelled",
|
52
|
+
500 => "internal_error",
|
53
|
+
501 => "unimplemented",
|
54
|
+
503 => "unavailable",
|
55
|
+
504 => "deadline_exceeded"
|
56
|
+
}
|
57
|
+
|
58
|
+
# An uuid that can be used to identify a trace.
|
59
|
+
# @return [String]
|
60
|
+
attr_reader :trace_id
|
61
|
+
# An uuid that can be used to identify the span.
|
62
|
+
# @return [String]
|
63
|
+
attr_reader :span_id
|
64
|
+
# Span parent's span_id.
|
65
|
+
# @return [String]
|
66
|
+
attr_reader :parent_span_id
|
67
|
+
# Sampling result of the span.
|
68
|
+
# @return [Boolean, nil]
|
69
|
+
attr_reader :sampled
|
70
|
+
# Starting timestamp of the span.
|
71
|
+
# @return [Float]
|
72
|
+
attr_reader :start_timestamp
|
73
|
+
# Finishing timestamp of the span.
|
74
|
+
# @return [Float]
|
75
|
+
attr_reader :timestamp
|
76
|
+
# Span description
|
77
|
+
# @return [String]
|
78
|
+
attr_reader :description
|
79
|
+
# Span operation
|
80
|
+
# @return [String]
|
81
|
+
attr_reader :op
|
82
|
+
# Span status
|
83
|
+
# @return [String]
|
84
|
+
attr_reader :status
|
85
|
+
# Span tags
|
86
|
+
# @return [Hash]
|
87
|
+
attr_reader :tags
|
88
|
+
# Span data
|
89
|
+
# @return [Hash]
|
90
|
+
attr_reader :data
|
91
|
+
|
92
|
+
# The SpanRecorder the current span belongs to.
|
93
|
+
# SpanRecorder holds all spans under the same Transaction object (including the Transaction itself).
|
94
|
+
# @return [SpanRecorder]
|
95
|
+
attr_accessor :span_recorder
|
96
|
+
|
97
|
+
# The Transaction object the Span belongs to.
|
98
|
+
# Every span needs to be attached to a Transaction and their child spans will also inherit the same transaction.
|
99
|
+
# @return [Transaction]
|
100
|
+
attr_reader :transaction
|
101
|
+
|
102
|
+
def initialize(
|
103
|
+
transaction:,
|
104
|
+
description: nil,
|
105
|
+
op: nil,
|
106
|
+
status: nil,
|
107
|
+
trace_id: nil,
|
108
|
+
span_id: nil,
|
109
|
+
parent_span_id: nil,
|
110
|
+
sampled: nil,
|
111
|
+
start_timestamp: nil,
|
112
|
+
timestamp: nil
|
113
|
+
)
|
114
|
+
@trace_id = trace_id || SecureRandom.uuid.delete("-")
|
115
|
+
@span_id = span_id || SecureRandom.uuid.delete("-").slice(0, 16)
|
116
|
+
@parent_span_id = parent_span_id
|
117
|
+
@sampled = sampled
|
118
|
+
@start_timestamp = start_timestamp || Sentry.utc_now.to_f
|
119
|
+
@timestamp = timestamp
|
120
|
+
@description = description
|
121
|
+
@transaction = transaction
|
122
|
+
@op = op
|
123
|
+
@status = status
|
124
|
+
@data = {}
|
125
|
+
@tags = {}
|
126
|
+
end
|
127
|
+
|
128
|
+
# Finishes the span by adding a timestamp.
|
129
|
+
# @return [self]
|
130
|
+
def finish(end_timestamp: nil)
|
131
|
+
@timestamp = end_timestamp || @timestamp || Sentry.utc_now.to_f
|
132
|
+
self
|
133
|
+
end
|
134
|
+
|
135
|
+
# Generates a trace string that can be used to connect other transactions.
|
136
|
+
# @return [String]
|
137
|
+
def to_sentry_trace
|
138
|
+
sampled_flag = ""
|
139
|
+
sampled_flag = @sampled ? 1 : 0 unless @sampled.nil?
|
140
|
+
|
141
|
+
"#{@trace_id}-#{@span_id}-#{sampled_flag}"
|
142
|
+
end
|
143
|
+
|
144
|
+
# Generates a W3C Baggage header string for distributed tracing
|
145
|
+
# from the incoming baggage stored on the transaction.
|
146
|
+
# @return [String, nil]
|
147
|
+
def to_baggage
|
148
|
+
transaction.get_baggage&.serialize
|
149
|
+
end
|
150
|
+
|
151
|
+
# @return [Hash]
|
152
|
+
def to_hash
|
153
|
+
{
|
154
|
+
trace_id: @trace_id,
|
155
|
+
span_id: @span_id,
|
156
|
+
parent_span_id: @parent_span_id,
|
157
|
+
start_timestamp: @start_timestamp,
|
158
|
+
timestamp: @timestamp,
|
159
|
+
description: @description,
|
160
|
+
op: @op,
|
161
|
+
status: @status,
|
162
|
+
tags: @tags,
|
163
|
+
data: @data
|
164
|
+
}
|
165
|
+
end
|
166
|
+
|
167
|
+
# Returns the span's context that can be used to embed in an Event.
|
168
|
+
# @return [Hash]
|
169
|
+
def get_trace_context
|
170
|
+
{
|
171
|
+
trace_id: @trace_id,
|
172
|
+
span_id: @span_id,
|
173
|
+
parent_span_id: @parent_span_id,
|
174
|
+
description: @description,
|
175
|
+
op: @op,
|
176
|
+
status: @status
|
177
|
+
}
|
178
|
+
end
|
179
|
+
|
180
|
+
# Starts a child span with given attributes.
|
181
|
+
# @param attributes [Hash] the attributes for the child span.
|
182
|
+
def start_child(**attributes)
|
183
|
+
attributes = attributes.dup.merge(transaction: @transaction, trace_id: @trace_id, parent_span_id: @span_id, sampled: @sampled)
|
184
|
+
new_span = Span.new(**attributes)
|
185
|
+
new_span.span_recorder = span_recorder
|
186
|
+
|
187
|
+
if span_recorder
|
188
|
+
span_recorder.add(new_span)
|
189
|
+
end
|
190
|
+
|
191
|
+
new_span
|
192
|
+
end
|
193
|
+
|
194
|
+
# Starts a child span, yield it to the given block, and then finish the span after the block is executed.
|
195
|
+
# @example
|
196
|
+
# span.with_child_span do |child_span|
|
197
|
+
# # things happen here will be recorded in a child span
|
198
|
+
# end
|
199
|
+
#
|
200
|
+
# @param attributes [Hash] the attributes for the child span.
|
201
|
+
# @param block [Proc] the action to be recorded in the child span.
|
202
|
+
# @yieldparam child_span [Span]
|
203
|
+
def with_child_span(**attributes, &block)
|
204
|
+
child_span = start_child(**attributes)
|
205
|
+
|
206
|
+
yield(child_span)
|
207
|
+
|
208
|
+
child_span.finish
|
209
|
+
rescue
|
210
|
+
child_span.set_http_status(500)
|
211
|
+
child_span.finish
|
212
|
+
raise
|
213
|
+
end
|
214
|
+
|
215
|
+
def deep_dup
|
216
|
+
dup
|
217
|
+
end
|
218
|
+
|
219
|
+
# Sets the span's operation.
|
220
|
+
# @param op [String] operation of the span.
|
221
|
+
def set_op(op)
|
222
|
+
@op = op
|
223
|
+
end
|
224
|
+
|
225
|
+
# Sets the span's description.
|
226
|
+
# @param description [String] description of the span.
|
227
|
+
def set_description(description)
|
228
|
+
@description = description
|
229
|
+
end
|
230
|
+
|
231
|
+
|
232
|
+
# Sets the span's status.
|
233
|
+
# @param satus [String] status of the span.
|
234
|
+
def set_status(status)
|
235
|
+
@status = status
|
236
|
+
end
|
237
|
+
|
238
|
+
# Sets the span's finish timestamp.
|
239
|
+
# @param timestamp [Float] finished time in float format (most precise).
|
240
|
+
def set_timestamp(timestamp)
|
241
|
+
@timestamp = timestamp
|
242
|
+
end
|
243
|
+
|
244
|
+
# Sets the span's status with given http status code.
|
245
|
+
# @param status_code [String] example: "500".
|
246
|
+
def set_http_status(status_code)
|
247
|
+
status_code = status_code.to_i
|
248
|
+
set_data(DataConventions::HTTP_STATUS_CODE, status_code)
|
249
|
+
|
250
|
+
status =
|
251
|
+
if status_code >= 200 && status_code < 299
|
252
|
+
"ok"
|
253
|
+
else
|
254
|
+
STATUS_MAP[status_code]
|
255
|
+
end
|
256
|
+
set_status(status)
|
257
|
+
end
|
258
|
+
|
259
|
+
# Inserts a key-value pair to the span's data payload.
|
260
|
+
# @param key [String, Symbol]
|
261
|
+
# @param value [Object]
|
262
|
+
def set_data(key, value)
|
263
|
+
@data[key] = value
|
264
|
+
end
|
265
|
+
|
266
|
+
# Sets a tag to the span.
|
267
|
+
# @param key [String, Symbol]
|
268
|
+
# @param value [String]
|
269
|
+
def set_tag(key, value)
|
270
|
+
@tags[key] = value
|
271
|
+
end
|
272
|
+
end
|
273
|
+
end
|
@@ -0,0 +1,84 @@
|
|
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
|
+
dummy_config = Sentry.configuration.dup
|
18
|
+
# configure dummy DSN, so the events will not be sent to the actual service
|
19
|
+
dummy_config.dsn = DUMMY_DSN
|
20
|
+
# set transport to DummyTransport, so we can easily intercept the captured events
|
21
|
+
dummy_config.transport.transport_class = Sentry::DummyTransport
|
22
|
+
# make sure SDK allows sending under the current environment
|
23
|
+
dummy_config.enabled_environments << dummy_config.environment unless dummy_config.enabled_environments.include?(dummy_config.environment)
|
24
|
+
# disble async event sending
|
25
|
+
dummy_config.background_worker_threads = 0
|
26
|
+
|
27
|
+
# user can overwrite some of the configs, with a few exceptions like:
|
28
|
+
# - include_local_variables
|
29
|
+
# - auto_session_tracking
|
30
|
+
block&.call(dummy_config)
|
31
|
+
|
32
|
+
# the base layer's client should already use the dummy config so nothing will be sent by accident
|
33
|
+
base_client = Sentry::Client.new(dummy_config)
|
34
|
+
Sentry.get_current_hub.bind_client(base_client)
|
35
|
+
# create a new layer so mutations made to the testing scope or configuration could be simply popped later
|
36
|
+
Sentry.get_current_hub.push_scope
|
37
|
+
test_client = Sentry::Client.new(dummy_config.dup)
|
38
|
+
Sentry.get_current_hub.bind_client(test_client)
|
39
|
+
end
|
40
|
+
|
41
|
+
# Clears all stored events and envelopes.
|
42
|
+
# It should be called **after** every test case.
|
43
|
+
# @return [void]
|
44
|
+
def teardown_sentry_test
|
45
|
+
return unless Sentry.initialized?
|
46
|
+
|
47
|
+
# pop testing layer created by `setup_sentry_test`
|
48
|
+
# but keep the base layer to avoid nil-pointer errors
|
49
|
+
# TODO: find a way to notify users if they somehow popped the test layer before calling this method
|
50
|
+
if Sentry.get_current_hub.instance_variable_get(:@stack).size > 1
|
51
|
+
Sentry.get_current_hub.pop_scope
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
# @return [Transport]
|
56
|
+
def sentry_transport
|
57
|
+
Sentry.get_current_client.transport
|
58
|
+
end
|
59
|
+
|
60
|
+
# Returns the captured event objects.
|
61
|
+
# @return [Array<Event>]
|
62
|
+
def sentry_events
|
63
|
+
sentry_transport.events
|
64
|
+
end
|
65
|
+
|
66
|
+
# Returns the captured envelope objects.
|
67
|
+
# @return [Array<Envelope>]
|
68
|
+
def sentry_envelopes
|
69
|
+
sentry_transport.envelopes
|
70
|
+
end
|
71
|
+
|
72
|
+
# Returns the last captured event object.
|
73
|
+
# @return [Event, nil]
|
74
|
+
def last_sentry_event
|
75
|
+
sentry_events.last
|
76
|
+
end
|
77
|
+
|
78
|
+
# Extracts SDK's internal exception container (not actual exception objects) from an given event.
|
79
|
+
# @return [Array<Sentry::SingleExceptionInterface>]
|
80
|
+
def extract_sentry_exceptions(event)
|
81
|
+
event&.exception&.values || []
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|