sentry-ruby-core 5.0.2 → 5.2.0

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2f6a137330d349e7d0cfa5535bdf80f940266df9e01b4f134df6a49c475724fa
4
- data.tar.gz: 27d2fd1c95d69dc1cf6b01a2ca6c9c6e58fcaf2cba2a76706b6a26fe323f4599
3
+ metadata.gz: c6a629d8aa998cef5638cd40e3dc9e0ad24770d0c31540b327c669e9fb84aedd
4
+ data.tar.gz: e373b601b401fddca9307a32ea1dda36bc4994209c68b19748e89f128a598e80
5
5
  SHA512:
6
- metadata.gz: c6f8a78bd9bdb89ca6f710a036e1e4917891968e1de929da024dbebc12a5d86642a51e367b329b6a1c9f6c670af47d5bad0a8fd15804c3511f33edd5333ae789
7
- data.tar.gz: '0559dd91dcdb831e132d5e74d0680cdedbb3e07c83b1f3129e635eb868bc8fb5a84e27813276058d436709543ec45f602f3f89ababec9295b1b0208292ee77c7'
6
+ metadata.gz: 2cfd2f1fe582578b74bc23610931d95bf7a86c35c696c658d55fb4f40ddfdd129c36bae415bc73efef20ecfb435f33ba72e3b46a96f536c0c61692d8c8cb34e2
7
+ data.tar.gz: f10f71edc8bef7a57426de73e0cf2a1aea855c89ec2900a065986d33f064216f7da74c0cf9f7dd41c08dcb18224a61c6c43bad67548a327ca593b73b526e3d8a
data/Gemfile CHANGED
@@ -9,6 +9,7 @@ gem "rake", "~> 12.0"
9
9
  gem "rspec", "~> 3.0"
10
10
  gem "rspec-retry"
11
11
  gem "webmock"
12
+ gem "fakeredis"
12
13
  gem "timecop"
13
14
  gem 'simplecov'
14
15
  gem "simplecov-cobertura", "~> 1.4"
@@ -75,6 +75,7 @@ module Sentry
75
75
  # An array of breadcrumbs loggers to be used. Available options are:
76
76
  # - :sentry_logger
77
77
  # - :http_logger
78
+ # - :redis_logger
78
79
  #
79
80
  # And if you also use sentry-rails:
80
81
  # - :active_support_logger
@@ -206,6 +207,10 @@ module Sentry
206
207
  # @return [Boolean]
207
208
  attr_accessor :send_client_reports
208
209
 
210
+ # Track sessions in request/response cycles automatically
211
+ # @return [Boolean]
212
+ attr_accessor :auto_session_tracking
213
+
209
214
  # these are not config options
210
215
  # @!visibility private
211
216
  attr_reader :errors, :gem_specs
@@ -260,6 +265,7 @@ module Sentry
260
265
  self.send_default_pii = false
261
266
  self.skip_rake_integration = false
262
267
  self.send_client_reports = true
268
+ self.auto_session_tracking = true
263
269
  self.trusted_proxies = []
264
270
  self.dsn = ENV['SENTRY_DSN']
265
271
  self.server_name = server_name_from_env
@@ -3,24 +3,43 @@
3
3
  module Sentry
4
4
  # @api private
5
5
  class Envelope
6
- def initialize(headers)
6
+ class Item
7
+ attr_accessor :headers, :payload
8
+
9
+ def initialize(headers, payload)
10
+ @headers = headers
11
+ @payload = payload
12
+ end
13
+
14
+ def type
15
+ @headers[:type] || 'event'
16
+ end
17
+
18
+ def to_s
19
+ <<~ITEM
20
+ #{JSON.generate(@headers)}
21
+ #{JSON.generate(@payload)}
22
+ ITEM
23
+ end
24
+ end
25
+
26
+ attr_accessor :headers, :items
27
+
28
+ def initialize(headers = {})
7
29
  @headers = headers
8
30
  @items = []
9
31
  end
10
32
 
11
33
  def add_item(headers, payload)
12
- @items << [headers, payload]
34
+ @items << Item.new(headers, payload)
13
35
  end
14
36
 
15
- def to_s
16
- payload = @items.map do |item_headers, item_payload|
17
- <<~ENVELOPE
18
- #{JSON.generate(item_headers)}
19
- #{JSON.generate(item_payload)}
20
- ENVELOPE
21
- end.join("\n")
37
+ def item_types
38
+ @items.map(&:type)
39
+ end
22
40
 
23
- "#{JSON.generate(@headers)}\n#{payload}"
41
+ def event_id
42
+ @headers[:event_id]
24
43
  end
25
44
  end
26
45
  end
data/lib/sentry/event.rb CHANGED
@@ -23,6 +23,7 @@ module Sentry
23
23
  WRITER_ATTRIBUTES = SERIALIZEABLE_ATTRIBUTES - %i(type timestamp level)
24
24
 
25
25
  MAX_MESSAGE_SIZE_IN_BYTES = 1024 * 8
26
+ MAX_SERIALIZED_PAYLOAD_SIZE = 1024 * 200
26
27
 
27
28
  SKIP_INSPECTION_ATTRIBUTES = [:@modules, :@stacktrace_builder, :@send_default_pii, :@trusted_proxies, :@rack_env_whitelist]
28
29
 
data/lib/sentry/hub.rb CHANGED
@@ -2,6 +2,7 @@
2
2
 
3
3
  require "sentry/scope"
4
4
  require "sentry/client"
5
+ require "sentry/session"
5
6
 
6
7
  module Sentry
7
8
  class Hub
@@ -94,6 +95,8 @@ module Sentry
94
95
  def capture_exception(exception, **options, &block)
95
96
  check_argument_type!(exception, ::Exception)
96
97
 
98
+ return if Sentry.exception_captured?(exception)
99
+
97
100
  return unless current_client
98
101
 
99
102
  options[:hint] ||= {}
@@ -102,7 +105,12 @@ module Sentry
102
105
 
103
106
  return unless event
104
107
 
105
- capture_event(event, **options, &block)
108
+ current_scope.session&.update_from_exception(event.exception)
109
+
110
+ capture_event(event, **options, &block).tap do
111
+ # mark the exception as captured so we can use this information to avoid duplicated capturing
112
+ exception.instance_variable_set(Sentry::CAPTURED_SIGNATURE, true)
113
+ end
106
114
  end
107
115
 
108
116
  def capture_message(message, **options, &block)
@@ -138,7 +146,6 @@ module Sentry
138
146
 
139
147
  event = current_client.capture_event(event, scope, hint)
140
148
 
141
-
142
149
  if event && configuration.debug
143
150
  configuration.log_debug(event.to_json_compatible)
144
151
  end
@@ -170,6 +177,30 @@ module Sentry
170
177
  configuration.background_worker_threads = original_background_worker_threads
171
178
  end
172
179
 
180
+ def start_session
181
+ return unless current_scope
182
+ current_scope.set_session(Session.new)
183
+ end
184
+
185
+ def end_session
186
+ return unless current_scope
187
+ session = current_scope.session
188
+ current_scope.set_session(nil)
189
+
190
+ return unless session
191
+ session.close
192
+ Sentry.session_flusher.add_session(session)
193
+ end
194
+
195
+ def with_session_tracking(&block)
196
+ return yield unless configuration.auto_session_tracking
197
+
198
+ start_session
199
+ yield
200
+ ensure
201
+ end_session
202
+ end
203
+
173
204
  private
174
205
 
175
206
  def current_layer
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+ require "set"
2
3
 
3
4
  module Sentry
4
5
  class ExceptionInterface < Interface
@@ -62,7 +62,7 @@ module Sentry
62
62
  self.url = request.scheme && request.url.split('?').first
63
63
  self.method = request.request_method
64
64
 
65
- self.headers = filter_and_format_headers(env)
65
+ self.headers = filter_and_format_headers(env, send_default_pii)
66
66
  self.env = filter_and_format_env(env, rack_env_whitelist)
67
67
  end
68
68
 
@@ -81,13 +81,14 @@ module Sentry
81
81
  e.message
82
82
  end
83
83
 
84
- def filter_and_format_headers(env)
84
+ def filter_and_format_headers(env, send_default_pii)
85
85
  env.each_with_object({}) do |(key, value), memo|
86
86
  begin
87
87
  key = key.to_s # rack env can contain symbols
88
88
  next memo['X-Request-Id'] ||= Utils::RequestId.read_from(env) if Utils::RequestId::REQUEST_ID_HEADERS.include?(key)
89
89
  next if is_server_protocol?(key, value, env["SERVER_PROTOCOL"])
90
90
  next if is_skippable_header?(key)
91
+ next if key == "HTTP_AUTHORIZATION" && !send_default_pii
91
92
 
92
93
  # Rack stores headers as HTTP_WHAT_EVER, we need What-Ever
93
94
  key = key.sub(/^HTTP_/, "")
@@ -6,7 +6,8 @@ module Sentry
6
6
  # @api private
7
7
  module Net
8
8
  module HTTP
9
- OP_NAME = "net.http"
9
+ OP_NAME = "http.client"
10
+ BREADCRUMB_CATEGORY = "net.http"
10
11
 
11
12
  # To explain how the entire thing works, we need to know how the original Net::HTTP#request works
12
13
  # Here's part of its definition. As you can see, it usually calls itself inside a #start block
@@ -53,7 +54,7 @@ module Sentry
53
54
 
54
55
  crumb = Sentry::Breadcrumb.new(
55
56
  level: :info,
56
- category: OP_NAME,
57
+ category: BREADCRUMB_CATEGORY,
57
58
  type: :info,
58
59
  data: {
59
60
  status: res.code.to_i,
@@ -14,30 +14,32 @@ module Sentry
14
14
  Sentry.clone_hub_to_current_thread
15
15
 
16
16
  Sentry.with_scope do |scope|
17
- scope.clear_breadcrumbs
18
- scope.set_transaction_name(env["PATH_INFO"]) if env["PATH_INFO"]
19
- scope.set_rack_env(env)
20
-
21
- transaction = start_transaction(env, scope)
22
- scope.set_span(transaction) if transaction
23
-
24
- begin
25
- response = @app.call(env)
26
- rescue Sentry::Error
27
- finish_transaction(transaction, 500)
28
- raise # Don't capture Sentry errors
29
- rescue Exception => e
30
- capture_exception(e)
31
- finish_transaction(transaction, 500)
32
- raise
17
+ Sentry.with_session_tracking do
18
+ scope.clear_breadcrumbs
19
+ scope.set_transaction_name(env["PATH_INFO"]) if env["PATH_INFO"]
20
+ scope.set_rack_env(env)
21
+
22
+ transaction = start_transaction(env, scope)
23
+ scope.set_span(transaction) if transaction
24
+
25
+ begin
26
+ response = @app.call(env)
27
+ rescue Sentry::Error
28
+ finish_transaction(transaction, 500)
29
+ raise # Don't capture Sentry errors
30
+ rescue Exception => e
31
+ capture_exception(e)
32
+ finish_transaction(transaction, 500)
33
+ raise
34
+ end
35
+
36
+ exception = collect_exception(env)
37
+ capture_exception(exception) if exception
38
+
39
+ finish_transaction(transaction, response[0])
40
+
41
+ response
33
42
  end
34
-
35
- exception = collect_exception(env)
36
- capture_exception(exception) if exception
37
-
38
- finish_transaction(transaction, response[0])
39
-
40
- response
41
43
  end
42
44
  end
43
45
 
@@ -59,7 +61,7 @@ module Sentry
59
61
  sentry_trace = env["HTTP_SENTRY_TRACE"]
60
62
  options = { name: scope.transaction_name, op: transaction_op }
61
63
  transaction = Sentry::Transaction.from_sentry_trace(sentry_trace, **options) if sentry_trace
62
- Sentry.start_transaction(transaction: transaction, **options)
64
+ Sentry.start_transaction(transaction: transaction, custom_sampling_context: { env: env }, **options)
63
65
  end
64
66
 
65
67
 
@@ -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
data/lib/sentry/scope.rb CHANGED
@@ -7,7 +7,7 @@ module Sentry
7
7
  class Scope
8
8
  include ArgumentCheckingHelper
9
9
 
10
- ATTRIBUTES = [:transaction_names, :contexts, :extra, :tags, :user, :level, :breadcrumbs, :fingerprint, :event_processors, :rack_env, :span]
10
+ ATTRIBUTES = [:transaction_names, :contexts, :extra, :tags, :user, :level, :breadcrumbs, :fingerprint, :event_processors, :rack_env, :span, :session]
11
11
 
12
12
  attr_reader(*ATTRIBUTES)
13
13
 
@@ -76,6 +76,7 @@ module Sentry
76
76
  copy.transaction_names = transaction_names.deep_dup
77
77
  copy.fingerprint = fingerprint.deep_dup
78
78
  copy.span = span.deep_dup
79
+ copy.session = session.deep_dup
79
80
  copy
80
81
  end
81
82
 
@@ -173,7 +174,7 @@ module Sentry
173
174
  def set_contexts(contexts_hash)
174
175
  check_argument_type!(contexts_hash, Hash)
175
176
  @contexts.merge!(contexts_hash) do |key, old, new|
176
- new.merge(old)
177
+ old.merge(new)
177
178
  end
178
179
  end
179
180
 
@@ -198,6 +199,13 @@ module Sentry
198
199
  @transaction_names << transaction_name
199
200
  end
200
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
+
201
209
  # Returns current transaction name.
202
210
  # The "transaction" here does not refer to `Transaction` objects.
203
211
  # @return [String, nil]
@@ -251,6 +259,7 @@ module Sentry
251
259
  @event_processors = []
252
260
  @rack_env = {}
253
261
  @span = nil
262
+ @session = nil
254
263
  set_new_breadcrumb_buffer
255
264
  end
256
265
 
@@ -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,79 @@
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
+ @client = client
12
+ @pending_aggregates = {}
13
+ @release = configuration.release
14
+ @environment = configuration.environment
15
+ @logger = configuration.logger
16
+
17
+ log_debug("[Sessions] Sessions won't be captured without a valid release") unless @release
18
+ end
19
+
20
+ def flush
21
+ return if @pending_aggregates.empty?
22
+ envelope = pending_envelope
23
+
24
+ Sentry.background_worker.perform do
25
+ @client.transport.send_envelope(envelope)
26
+ end
27
+
28
+ @pending_aggregates = {}
29
+ end
30
+
31
+ def add_session(session)
32
+ return unless @release
33
+
34
+ ensure_thread
35
+
36
+ return unless Session::AGGREGATE_STATUSES.include?(session.status)
37
+ @pending_aggregates[session.aggregation_key] ||= init_aggregates(session.aggregation_key)
38
+ @pending_aggregates[session.aggregation_key][session.status] += 1
39
+ end
40
+
41
+ def kill
42
+ @thread&.kill
43
+ end
44
+
45
+ private
46
+
47
+ def init_aggregates(aggregation_key)
48
+ aggregates = { started: aggregation_key.iso8601 }
49
+ Session::AGGREGATE_STATUSES.each { |k| aggregates[k] = 0 }
50
+ aggregates
51
+ end
52
+
53
+ def pending_envelope
54
+ envelope = Envelope.new
55
+
56
+ header = { type: 'sessions' }
57
+ payload = { attrs: attrs, aggregates: @pending_aggregates.values }
58
+
59
+ envelope.add_item(header, payload)
60
+ envelope
61
+ end
62
+
63
+ def attrs
64
+ { release: @release, environment: @environment }
65
+ end
66
+
67
+ def ensure_thread
68
+ return if @thread&.alive?
69
+
70
+ @thread = Thread.new do
71
+ loop do
72
+ sleep(FLUSH_INTERVAL)
73
+ flush
74
+ end
75
+ end
76
+ end
77
+
78
+ end
79
+ end
@@ -164,13 +164,12 @@ module Sentry
164
164
  @name = UNLABELD_NAME
165
165
  end
166
166
 
167
- unless @sampled || @parent_sampled
167
+ if @sampled
168
+ event = hub.current_client.event_from_transaction(self)
169
+ hub.capture_event(event)
170
+ else
168
171
  hub.current_client.transport.record_lost_event(:sample_rate, 'transaction')
169
- return
170
172
  end
171
-
172
- event = hub.current_client.event_from_transaction(self)
173
- hub.capture_event(event)
174
173
  end
175
174
 
176
175
  protected
@@ -2,15 +2,20 @@
2
2
 
3
3
  module Sentry
4
4
  class DummyTransport < Transport
5
- attr_accessor :events
5
+ attr_accessor :events, :envelopes
6
6
 
7
7
  def initialize(*)
8
8
  super
9
9
  @events = []
10
+ @envelopes = []
10
11
  end
11
12
 
12
13
  def send_event(event)
13
14
  @events << event
14
15
  end
16
+
17
+ def send_envelope(envelope)
18
+ @envelopes << envelope
19
+ end
15
20
  end
16
21
  end
@@ -46,23 +46,54 @@ module Sentry
46
46
  end
47
47
 
48
48
  def send_event(event)
49
- event_hash = event.to_hash
50
- item_type = get_item_type(event_hash)
49
+ envelope = envelope_from_event(event)
50
+ send_envelope(envelope)
51
51
 
52
- if is_rate_limited?(item_type)
53
- log_info("Envelope [#{item_type}] not sent: rate limiting")
54
- record_lost_event(:ratelimit_backoff, item_type)
52
+ event
53
+ end
54
+
55
+ def send_envelope(envelope)
56
+ reject_rate_limited_items(envelope)
55
57
 
56
- return
58
+ return if envelope.items.empty?
59
+
60
+ data, serialized_items = serialize_envelope(envelope)
61
+
62
+ if data
63
+ log_info("[Transport] Sending envelope with items [#{serialized_items.map(&:type).join(', ')}] #{envelope.event_id} to Sentry")
64
+ send_data(data)
57
65
  end
66
+ end
58
67
 
59
- encoded_data = encode(event)
68
+ def serialize_envelope(envelope)
69
+ serialized_items = []
70
+ serialized_results = []
60
71
 
61
- return nil unless encoded_data
72
+ envelope.items.each do |item|
73
+ result = item.to_s
62
74
 
63
- send_data(encoded_data)
75
+ if result.bytesize > Event::MAX_SERIALIZED_PAYLOAD_SIZE
76
+ item.payload.delete(:breadcrumbs)
77
+ result = item.to_s
78
+ end
64
79
 
65
- event
80
+ if result.bytesize > Event::MAX_SERIALIZED_PAYLOAD_SIZE
81
+ size_breakdown = item.payload.map do |key, value|
82
+ "#{key}: #{JSON.generate(value).bytesize}"
83
+ end.join(", ")
84
+
85
+ log_debug("Envelope item [#{item.type}] is still oversized without breadcrumbs: {#{size_breakdown}}")
86
+
87
+ next
88
+ end
89
+
90
+ serialized_results << result
91
+ serialized_items << item
92
+ end
93
+
94
+ data = [JSON.generate(envelope.headers), *serialized_results].join("\n") unless serialized_results.empty?
95
+
96
+ [data, serialized_items]
66
97
  end
67
98
 
68
99
  def is_rate_limited?(item_type)
@@ -71,6 +102,8 @@ module Sentry
71
102
  case item_type
72
103
  when "transaction"
73
104
  @rate_limits["transaction"]
105
+ when "sessions"
106
+ @rate_limits["session"]
74
107
  else
75
108
  @rate_limits["error"]
76
109
  end
@@ -106,7 +139,7 @@ module Sentry
106
139
  'Sentry ' + fields.map { |key, value| "#{key}=#{value}" }.join(', ')
107
140
  end
108
141
 
109
- def encode(event)
142
+ def envelope_from_event(event)
110
143
  # Convert to hash
111
144
  event_payload = event.to_hash
112
145
  event_id = event_payload[:event_id] || event_payload["event_id"]
@@ -129,9 +162,7 @@ module Sentry
129
162
  client_report_headers, client_report_payload = fetch_pending_client_report
130
163
  envelope.add_item(client_report_headers, client_report_payload) if client_report_headers
131
164
 
132
- log_info("Sending envelope [#{item_type}] #{event_id} to Sentry")
133
-
134
- envelope.to_s
165
+ envelope
135
166
  end
136
167
 
137
168
  def record_lost_event(reason, item_type)
@@ -173,6 +204,19 @@ module Sentry
173
204
 
174
205
  [item_header, item_payload]
175
206
  end
207
+
208
+ def reject_rate_limited_items(envelope)
209
+ envelope.items.reject! do |item|
210
+ if is_rate_limited?(item.type)
211
+ log_info("[Transport] Envelope item [#{item.type}] not sent: rate limiting")
212
+ record_lost_event(:ratelimit_backoff, item.type)
213
+
214
+ true
215
+ else
216
+ false
217
+ end
218
+ end
219
+ end
176
220
  end
177
221
  end
178
222
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Sentry
4
- VERSION = "5.0.2"
4
+ VERSION = "5.2.0"
5
5
  end
data/lib/sentry-ruby.rb CHANGED
@@ -17,6 +17,7 @@ require "sentry/span"
17
17
  require "sentry/transaction"
18
18
  require "sentry/hub"
19
19
  require "sentry/background_worker"
20
+ require "sentry/session_flusher"
20
21
 
21
22
  [
22
23
  "sentry/rake",
@@ -31,6 +32,8 @@ end
31
32
  module Sentry
32
33
  META = { "name" => "sentry.ruby", "version" => Sentry::VERSION }.freeze
33
34
 
35
+ CAPTURED_SIGNATURE = :@__sentry_captured
36
+
34
37
  LOGGER_PROGNAME = "sentry".freeze
35
38
 
36
39
  SENTRY_TRACE_HEADER_NAME = "sentry-trace".freeze
@@ -59,6 +62,10 @@ module Sentry
59
62
  # @return [BackgroundWorker]
60
63
  attr_accessor :background_worker
61
64
 
65
+ # @!attribute [r] session_flusher
66
+ # @return [SessionFlusher]
67
+ attr_reader :session_flusher
68
+
62
69
  ##### Patch Registration #####
63
70
 
64
71
  # @!visibility private
@@ -111,9 +118,17 @@ module Sentry
111
118
 
112
119
  # @!method configuration
113
120
  # @!macro configuration
121
+ def configuration
122
+ return unless initialized?
123
+ get_current_client.configuration
124
+ end
125
+
114
126
  # @!method send_event
115
127
  # @!macro send_event
116
- def_delegators :get_current_client, :configuration, :send_event
128
+ def send_event(*args)
129
+ return unless initialized?
130
+ get_current_client.send_event(*args)
131
+ end
117
132
 
118
133
  # @!macro [new] set_extras
119
134
  # Updates the scope's extras attribute by merging with the old value.
@@ -135,13 +150,31 @@ module Sentry
135
150
 
136
151
  # @!method set_tags
137
152
  # @!macro set_tags
153
+ def set_tags(*args)
154
+ return unless initialized?
155
+ get_current_scope.set_tags(*args)
156
+ end
157
+
138
158
  # @!method set_extras
139
159
  # @!macro set_extras
160
+ def set_extras(*args)
161
+ return unless initialized?
162
+ get_current_scope.set_extras(*args)
163
+ end
164
+
140
165
  # @!method set_user
141
166
  # @!macro set_user
167
+ def set_user(*args)
168
+ return unless initialized?
169
+ get_current_scope.set_user(*args)
170
+ end
171
+
142
172
  # @!method set_context
143
173
  # @!macro set_context
144
- def_delegators :get_current_scope, :set_tags, :set_extras, :set_user, :set_context
174
+ def set_context(*args)
175
+ return unless initialized?
176
+ get_current_scope.set_context(*args)
177
+ end
145
178
 
146
179
  ##### Main APIs #####
147
180
 
@@ -161,11 +194,18 @@ module Sentry
161
194
  @main_hub = hub
162
195
  @background_worker = Sentry::BackgroundWorker.new(config)
163
196
 
197
+ @session_flusher = if config.auto_session_tracking
198
+ Sentry::SessionFlusher.new(config, client)
199
+ else
200
+ nil
201
+ end
202
+
164
203
  if config.capture_exception_frame_locals
165
204
  exception_locals_tp.enable
166
205
  end
167
206
 
168
207
  at_exit do
208
+ @session_flusher&.kill
169
209
  @background_worker.shutdown
170
210
  end
171
211
  end
@@ -201,7 +241,8 @@ module Sentry
201
241
  #
202
242
  # @return [Breadcrumb, nil]
203
243
  def add_breadcrumb(breadcrumb, **options)
204
- get_current_hub&.add_breadcrumb(breadcrumb, **options)
244
+ return unless initialized?
245
+ get_current_hub.add_breadcrumb(breadcrumb, **options)
205
246
  end
206
247
 
207
248
  # Returns the current active hub.
@@ -221,14 +262,16 @@ module Sentry
221
262
  # Returns the current active client.
222
263
  # @return [Client, nil]
223
264
  def get_current_client
224
- get_current_hub&.current_client
265
+ return unless initialized?
266
+ get_current_hub.current_client
225
267
  end
226
268
 
227
269
  # Returns the current active scope.
228
270
  #
229
271
  # @return [Scope, nil]
230
272
  def get_current_scope
231
- get_current_hub&.current_scope
273
+ return unless initialized?
274
+ get_current_hub.current_scope
232
275
  end
233
276
 
234
277
  # Clones the main thread's active hub and stores it to the current thread.
@@ -250,7 +293,8 @@ module Sentry
250
293
  # @yieldparam scope [Scope]
251
294
  # @return [void]
252
295
  def configure_scope(&block)
253
- get_current_hub&.configure_scope(&block)
296
+ return unless initialized?
297
+ get_current_hub.configure_scope(&block)
254
298
  end
255
299
 
256
300
  # Takes a block and yields a temporary scope.
@@ -274,7 +318,28 @@ module Sentry
274
318
  # @yieldparam scope [Scope]
275
319
  # @return [void]
276
320
  def with_scope(&block)
277
- get_current_hub&.with_scope(&block)
321
+ return unless initialized?
322
+ get_current_hub.with_scope(&block)
323
+ end
324
+
325
+ # Wrap a given block with session tracking.
326
+ # Aggregate sessions in minutely buckets will be recorded
327
+ # around this block and flushed every minute.
328
+ #
329
+ # @example
330
+ # Sentry.with_session_tracking do
331
+ # a = 1 + 1 # new session recorded with :exited status
332
+ # end
333
+ #
334
+ # Sentry.with_session_tracking do
335
+ # 1 / 0
336
+ # rescue => e
337
+ # Sentry.capture_exception(e) # new session recorded with :errored status
338
+ # end
339
+ # @return [void]
340
+ def with_session_tracking(&block)
341
+ return yield unless initialized?
342
+ get_current_hub.with_session_tracking(&block)
278
343
  end
279
344
 
280
345
  # Takes an exception and reports it to Sentry via the currently active hub.
@@ -282,7 +347,8 @@ module Sentry
282
347
  # @yieldparam scope [Scope]
283
348
  # @return [Event, nil]
284
349
  def capture_exception(exception, **options, &block)
285
- get_current_hub&.capture_exception(exception, **options, &block)
350
+ return unless initialized?
351
+ get_current_hub.capture_exception(exception, **options, &block)
286
352
  end
287
353
 
288
354
  # Takes a message string and reports it to Sentry via the currently active hub.
@@ -290,30 +356,41 @@ module Sentry
290
356
  # @yieldparam scope [Scope]
291
357
  # @return [Event, nil]
292
358
  def capture_message(message, **options, &block)
293
- get_current_hub&.capture_message(message, **options, &block)
359
+ return unless initialized?
360
+ get_current_hub.capture_message(message, **options, &block)
294
361
  end
295
362
 
296
363
  # Takes an instance of Sentry::Event and dispatches it to the currently active hub.
297
364
  #
298
365
  # @return [Event, nil]
299
366
  def capture_event(event)
300
- get_current_hub&.capture_event(event)
367
+ return unless initialized?
368
+ get_current_hub.capture_event(event)
301
369
  end
302
370
 
303
371
  # Takes or initializes a new Sentry::Transaction and makes a sampling decision for it.
304
372
  #
305
373
  # @return [Transaction, nil]
306
374
  def start_transaction(**options)
307
- get_current_hub&.start_transaction(**options)
375
+ return unless initialized?
376
+ get_current_hub.start_transaction(**options)
308
377
  end
309
378
 
310
379
  # Returns the id of the lastly reported Sentry::Event.
311
380
  #
312
381
  # @return [String, nil]
313
382
  def last_event_id
314
- get_current_hub&.last_event_id
383
+ return unless initialized?
384
+ get_current_hub.last_event_id
315
385
  end
316
386
 
387
+ # Checks if the exception object has been captured by the SDK.
388
+ #
389
+ # @return [Boolean]
390
+ def exception_captured?(exc)
391
+ return false unless initialized?
392
+ !!exc.instance_variable_get(CAPTURED_SIGNATURE)
393
+ end
317
394
 
318
395
  ##### Helpers #####
319
396
 
@@ -344,3 +421,4 @@ end
344
421
 
345
422
  # patches
346
423
  require "sentry/net/http"
424
+ require "sentry/redis"
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: sentry-ruby-core
3
3
  version: !ruby/object:Gem::Version
4
- version: 5.0.2
4
+ version: 5.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sentry Team
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2022-01-31 00:00:00.000000000 Z
11
+ date: 2022-03-08 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: concurrent-ruby
@@ -73,8 +73,11 @@ files:
73
73
  - lib/sentry/rack.rb
74
74
  - lib/sentry/rack/capture_exceptions.rb
75
75
  - lib/sentry/rake.rb
76
+ - lib/sentry/redis.rb
76
77
  - lib/sentry/release_detector.rb
77
78
  - lib/sentry/scope.rb
79
+ - lib/sentry/session.rb
80
+ - lib/sentry/session_flusher.rb
78
81
  - lib/sentry/span.rb
79
82
  - lib/sentry/transaction.rb
80
83
  - lib/sentry/transaction_event.rb