sentry-ruby-core 5.1.1 → 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: bfc28e6f57d57de669447d555889196617f72ce09624b343cd284375021f60f5
4
- data.tar.gz: 5a6af91521df30a18ac1d021b2e59cd9e871653f1325cf6d65346e6c1dc91d7e
3
+ metadata.gz: c6a629d8aa998cef5638cd40e3dc9e0ad24770d0c31540b327c669e9fb84aedd
4
+ data.tar.gz: e373b601b401fddca9307a32ea1dda36bc4994209c68b19748e89f128a598e80
5
5
  SHA512:
6
- metadata.gz: fbe80e4c07f972d3babbf8c26c31cd97da84e91d77e86628a6931f35d7e66c83fb48461838d1a3e402361ee78dd9a3b6dfe849833f08a4573328d0eb4b5e66e1
7
- data.tar.gz: 0f081f57f89207ece5279bd77fa89d311c3d516231421ccf60f4b9c232065a606d80eacc53ab460f12e087aa7b2fadfc4cdd4636863cb58261cc4e58a605ad2c
6
+ metadata.gz: 2cfd2f1fe582578b74bc23610931d95bf7a86c35c696c658d55fb4f40ddfdd129c36bae415bc73efef20ecfb435f33ba72e3b46a96f536c0c61692d8c8cb34e2
7
+ data.tar.gz: f10f71edc8bef7a57426de73e0cf2a1aea855c89ec2900a065986d33f064216f7da74c0cf9f7dd41c08dcb18224a61c6c43bad67548a327ca593b73b526e3d8a
@@ -207,6 +207,10 @@ module Sentry
207
207
  # @return [Boolean]
208
208
  attr_accessor :send_client_reports
209
209
 
210
+ # Track sessions in request/response cycles automatically
211
+ # @return [Boolean]
212
+ attr_accessor :auto_session_tracking
213
+
210
214
  # these are not config options
211
215
  # @!visibility private
212
216
  attr_reader :errors, :gem_specs
@@ -261,6 +265,7 @@ module Sentry
261
265
  self.send_default_pii = false
262
266
  self.skip_rake_integration = false
263
267
  self.send_client_reports = true
268
+ self.auto_session_tracking = true
264
269
  self.trusted_proxies = []
265
270
  self.dsn = ENV['SENTRY_DSN']
266
271
  self.server_name = server_name_from_env
@@ -34,10 +34,6 @@ module Sentry
34
34
  @items << Item.new(headers, payload)
35
35
  end
36
36
 
37
- def to_s
38
- [JSON.generate(@headers), *@items.map(&:to_s)].join("\n")
39
- end
40
-
41
37
  def item_types
42
38
  @items.map(&:type)
43
39
  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
@@ -104,6 +105,8 @@ module Sentry
104
105
 
105
106
  return unless event
106
107
 
108
+ current_scope.session&.update_from_exception(event.exception)
109
+
107
110
  capture_event(event, **options, &block).tap do
108
111
  # mark the exception as captured so we can use this information to avoid duplicated capturing
109
112
  exception.instance_variable_set(Sentry::CAPTURED_SIGNATURE, true)
@@ -143,7 +146,6 @@ module Sentry
143
146
 
144
147
  event = current_client.capture_event(event, scope, hint)
145
148
 
146
-
147
149
  if event && configuration.debug
148
150
  configuration.log_debug(event.to_json_compatible)
149
151
  end
@@ -175,6 +177,30 @@ module Sentry
175
177
  configuration.background_worker_threads = original_background_worker_threads
176
178
  end
177
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
+
178
204
  private
179
205
 
180
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
@@ -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
 
data/lib/sentry/redis.rb CHANGED
@@ -3,8 +3,8 @@
3
3
  module Sentry
4
4
  # @api private
5
5
  class Redis
6
- OP_NAME ||= "db.redis.command"
7
- LOGGER_NAME ||= :redis_logger
6
+ OP_NAME = "db.redis.command"
7
+ LOGGER_NAME = :redis_logger
8
8
 
9
9
  def initialize(commands, host, port, db)
10
10
  @commands, @host, @port, @db = commands, host, port, db
@@ -60,9 +60,11 @@ module Sentry
60
60
 
61
61
  def parsed_commands
62
62
  commands.map do |statement|
63
- command, key, *_values = statement
63
+ command, key, *arguments = statement
64
64
 
65
- { command: command.to_s.upcase, key: key }
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
66
68
  end
67
69
  end
68
70
 
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
 
@@ -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
@@ -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
@@ -57,8 +57,43 @@ module Sentry
57
57
 
58
58
  return if envelope.items.empty?
59
59
 
60
- log_info("[Transport] Sending envelope with items [#{envelope.item_types.join(', ')}] #{envelope.event_id} to Sentry")
61
- send_data(envelope.to_s)
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)
65
+ end
66
+ end
67
+
68
+ def serialize_envelope(envelope)
69
+ serialized_items = []
70
+ serialized_results = []
71
+
72
+ envelope.items.each do |item|
73
+ result = item.to_s
74
+
75
+ if result.bytesize > Event::MAX_SERIALIZED_PAYLOAD_SIZE
76
+ item.payload.delete(:breadcrumbs)
77
+ result = item.to_s
78
+ end
79
+
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]
62
97
  end
63
98
 
64
99
  def is_rate_limited?(item_type)
@@ -67,6 +102,8 @@ module Sentry
67
102
  case item_type
68
103
  when "transaction"
69
104
  @rate_limits["transaction"]
105
+ when "sessions"
106
+ @rate_limits["session"]
70
107
  else
71
108
  @rate_limits["error"]
72
109
  end
@@ -125,7 +162,6 @@ module Sentry
125
162
  client_report_headers, client_report_payload = fetch_pending_client_report
126
163
  envelope.add_item(client_report_headers, client_report_payload) if client_report_headers
127
164
 
128
-
129
165
  envelope
130
166
  end
131
167
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Sentry
4
- VERSION = "5.1.1"
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",
@@ -61,6 +62,10 @@ module Sentry
61
62
  # @return [BackgroundWorker]
62
63
  attr_accessor :background_worker
63
64
 
65
+ # @!attribute [r] session_flusher
66
+ # @return [SessionFlusher]
67
+ attr_reader :session_flusher
68
+
64
69
  ##### Patch Registration #####
65
70
 
66
71
  # @!visibility private
@@ -189,11 +194,18 @@ module Sentry
189
194
  @main_hub = hub
190
195
  @background_worker = Sentry::BackgroundWorker.new(config)
191
196
 
197
+ @session_flusher = if config.auto_session_tracking
198
+ Sentry::SessionFlusher.new(config, client)
199
+ else
200
+ nil
201
+ end
202
+
192
203
  if config.capture_exception_frame_locals
193
204
  exception_locals_tp.enable
194
205
  end
195
206
 
196
207
  at_exit do
208
+ @session_flusher&.kill
197
209
  @background_worker.shutdown
198
210
  end
199
211
  end
@@ -310,6 +322,26 @@ module Sentry
310
322
  get_current_hub.with_scope(&block)
311
323
  end
312
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)
343
+ end
344
+
313
345
  # Takes an exception and reports it to Sentry via the currently active hub.
314
346
  #
315
347
  # @yieldparam scope [Scope]
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.1.1
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-02-24 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
@@ -76,6 +76,8 @@ files:
76
76
  - lib/sentry/redis.rb
77
77
  - lib/sentry/release_detector.rb
78
78
  - lib/sentry/scope.rb
79
+ - lib/sentry/session.rb
80
+ - lib/sentry/session_flusher.rb
79
81
  - lib/sentry/span.rb
80
82
  - lib/sentry/transaction.rb
81
83
  - lib/sentry/transaction_event.rb