sentry-ruby-core 5.0.2 → 5.2.0

Sign up to get free protection for your applications and to get access to all the features.
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