sentry-ruby-core 5.1.1 → 5.3.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: 03d233c6e1bd55ddc4044c7a23b6bc74047eec17a8c16d5f33f768065641259c
4
+ data.tar.gz: bec07f880bff192b6ef71aaefdf9dc9ff2ff072a539f94ed5c4fd0398ad1146a
5
5
  SHA512:
6
- metadata.gz: fbe80e4c07f972d3babbf8c26c31cd97da84e91d77e86628a6931f35d7e66c83fb48461838d1a3e402361ee78dd9a3b6dfe849833f08a4573328d0eb4b5e66e1
7
- data.tar.gz: 0f081f57f89207ece5279bd77fa89d311c3d516231421ccf60f4b9c232065a606d80eacc53ab460f12e087aa7b2fadfc4cdd4636863cb58261cc4e58a605ad2c
6
+ metadata.gz: c3d90a9b888db8a396e260dccf5df3a38d4f153c9bc2b31b92fc206009553fec2d3d0fe47bb1a871b001372fcba8013fee4e16b60405588db4ce3984f56d675e
7
+ data.tar.gz: 2ae1db7dee78487fec06b1cb1c6fb2dfa77852ae20b0c28b6c2f0e0b964f728fdea40a8261495439ef6e193182a0f7dc3196dc1bd75368abcf2d78852310361a
data/Gemfile CHANGED
@@ -1,4 +1,5 @@
1
1
  source "https://rubygems.org"
2
+ git_source(:github) { |name| "https://github.com/#{name}.git" }
2
3
 
3
4
  gem "sentry-ruby-core", path: "./"
4
5
  gem "sentry-ruby", path: "./"
data/README.md CHANGED
@@ -29,7 +29,7 @@ If you're using `sentry-raven`, we recommend you to migrate to this new SDK. You
29
29
 
30
30
  ## Requirements
31
31
 
32
- We test on Ruby 2.4, 2.5, 2.6, 2.7, and 3.0 at the latest patchlevel/teeny version. We also support JRuby 9.0.
32
+ We test on Ruby 2.4, 2.5, 2.6, 2.7, 3.0, and 3.1 at the latest patchlevel/teeny version. We also support JRuby 9.0.
33
33
 
34
34
  If you use self-hosted Sentry, please also make sure its version is above `20.6.0`.
35
35
 
@@ -29,7 +29,7 @@ module Sentry
29
29
  log_debug("config.background_worker_threads is set to 0, all events will be sent synchronously")
30
30
  Concurrent::ImmediateExecutor.new
31
31
  else
32
- log_debug("initialized a background worker with #{@number_of_threads} threads")
32
+ log_debug("Initializing the background worker with #{@number_of_threads} threads")
33
33
 
34
34
  executor = Concurrent::ThreadPoolExecutor.new(
35
35
  min_threads: 0,
@@ -59,6 +59,7 @@ module Sentry
59
59
  end
60
60
 
61
61
  def shutdown
62
+ log_debug("Shutting down background worker")
62
63
  @shutdown_callback&.call
63
64
  end
64
65
 
data/lib/sentry/client.rb CHANGED
@@ -80,9 +80,10 @@ module Sentry
80
80
 
81
81
  integration_meta = Sentry.integrations[hint[:integration]]
82
82
 
83
- Event.new(configuration: configuration, integration_meta: integration_meta).tap do |event|
83
+ ErrorEvent.new(configuration: configuration, integration_meta: integration_meta).tap do |event|
84
84
  event.add_exception_interface(exception)
85
85
  event.add_threads_interface(crashed: true)
86
+ event.level = :error
86
87
  end
87
88
  end
88
89
 
@@ -94,8 +95,9 @@ module Sentry
94
95
  return unless @configuration.sending_allowed?
95
96
 
96
97
  integration_meta = Sentry.integrations[hint[:integration]]
97
- event = Event.new(configuration: configuration, integration_meta: integration_meta, message: message)
98
+ event = ErrorEvent.new(configuration: configuration, integration_meta: integration_meta, message: message)
98
99
  event.add_threads_interface(backtrace: backtrace || caller)
100
+ event.level = :error
99
101
  event
100
102
  end
101
103
 
@@ -133,7 +135,7 @@ module Sentry
133
135
 
134
136
  event
135
137
  rescue => e
136
- loggable_event_type = (event_type || "event").capitalize
138
+ loggable_event_type = event_type.capitalize
137
139
  log_error("#{loggable_event_type} sending failed", e, debug: configuration.debug)
138
140
 
139
141
  event_info = Event.get_log_message(event.to_hash)
@@ -174,8 +176,7 @@ module Sentry
174
176
  async_block.call(event_hash)
175
177
  end
176
178
  rescue => e
177
- loggable_event_type = event_hash["type"] || "event"
178
- log_error("Async #{loggable_event_type} sending failed", e, debug: configuration.debug)
179
+ log_error("Async #{event_hash["type"]} sending failed", e, debug: configuration.debug)
179
180
  send_event(event, hint)
180
181
  end
181
182
  end
@@ -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
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sentry
4
+ # ErrorEvent represents error or normal message events.
5
+ class ErrorEvent < Event
6
+ # @return [ExceptionInterface]
7
+ attr_reader :exception
8
+
9
+ # @return [ThreadsInterface]
10
+ attr_reader :threads
11
+
12
+ # @return [Hash]
13
+ def to_hash
14
+ data = super
15
+ data[:threads] = threads.to_hash if threads
16
+ data[:exception] = exception.to_hash if exception
17
+ data
18
+ end
19
+
20
+ # @!visibility private
21
+ def add_threads_interface(backtrace: nil, **options)
22
+ @threads = ThreadsInterface.build(
23
+ backtrace: backtrace,
24
+ stacktrace_builder: @stacktrace_builder,
25
+ **options
26
+ )
27
+ end
28
+
29
+ # @!visibility private
30
+ def add_exception_interface(exception)
31
+ if exception.respond_to?(:sentry_context)
32
+ @extra.merge!(exception.sentry_context)
33
+ end
34
+
35
+ @exception = Sentry::ExceptionInterface.build(exception: exception, stacktrace_builder: @stacktrace_builder)
36
+ end
37
+ end
38
+ end
data/lib/sentry/event.rb CHANGED
@@ -9,7 +9,10 @@ require 'sentry/utils/request_id'
9
9
  require 'sentry/utils/custom_inspection'
10
10
 
11
11
  module Sentry
12
+ # This is an abstract class that defines the shared attributes of an event.
13
+ # Please don't use it directly. The user-facing classes are its child classes.
12
14
  class Event
15
+ TYPE = "event"
13
16
  # These are readable attributes.
14
17
  SERIALIZEABLE_ATTRIBUTES = %i(
15
18
  event_id level timestamp
@@ -23,6 +26,7 @@ module Sentry
23
26
  WRITER_ATTRIBUTES = SERIALIZEABLE_ATTRIBUTES - %i(type timestamp level)
24
27
 
25
28
  MAX_MESSAGE_SIZE_IN_BYTES = 1024 * 8
29
+ MAX_SERIALIZED_PAYLOAD_SIZE = 1024 * 200
26
30
 
27
31
  SKIP_INSPECTION_ATTRIBUTES = [:@modules, :@stacktrace_builder, :@send_default_pii, :@trusted_proxies, :@rack_env_whitelist]
28
32
 
@@ -34,12 +38,6 @@ module Sentry
34
38
  # @return [RequestInterface]
35
39
  attr_reader :request
36
40
 
37
- # @return [ExceptionInterface]
38
- attr_reader :exception
39
-
40
- # @return [ThreadsInterface]
41
- attr_reader :threads
42
-
43
41
  # @param configuration [Configuration]
44
42
  # @param integration_meta [Hash, nil]
45
43
  # @param message [String, nil]
@@ -48,6 +46,7 @@ module Sentry
48
46
  @event_id = SecureRandom.uuid.delete("-")
49
47
  @timestamp = Sentry.utc_now.iso8601
50
48
  @platform = :ruby
49
+ @type = self.class::TYPE
51
50
  @sdk = integration_meta || Sentry.sdk_meta
52
51
 
53
52
  @user = {}
@@ -70,8 +69,6 @@ module Sentry
70
69
  @rack_env_whitelist = configuration.rack_env_whitelist
71
70
 
72
71
  @message = (message || "").byteslice(0..MAX_MESSAGE_SIZE_IN_BYTES)
73
-
74
- self.level = :error
75
72
  end
76
73
 
77
74
  class << self
@@ -145,9 +142,6 @@ module Sentry
145
142
  data = serialize_attributes
146
143
  data[:breadcrumbs] = breadcrumbs.to_hash if breadcrumbs
147
144
  data[:request] = request.to_hash if request
148
- data[:exception] = exception.to_hash if exception
149
- data[:threads] = threads.to_hash if threads
150
-
151
145
  data
152
146
  end
153
147
 
@@ -156,24 +150,6 @@ module Sentry
156
150
  JSON.parse(JSON.generate(to_hash))
157
151
  end
158
152
 
159
- # @!visibility private
160
- def add_threads_interface(backtrace: nil, **options)
161
- @threads = ThreadsInterface.build(
162
- backtrace: backtrace,
163
- stacktrace_builder: @stacktrace_builder,
164
- **options
165
- )
166
- end
167
-
168
- # @!visibility private
169
- def add_exception_interface(exception)
170
- if exception.respond_to?(:sentry_context)
171
- @extra.merge!(exception.sentry_context)
172
- end
173
-
174
- @exception = Sentry::ExceptionInterface.build(exception: exception, stacktrace_builder: @stacktrace_builder)
175
- end
176
-
177
153
  private
178
154
 
179
155
  def add_request_interface(env)
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,12 +146,11 @@ 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
150
152
 
151
- @last_event_id = event&.event_id
153
+ @last_event_id = event&.event_id unless event.is_a?(Sentry::TransactionEvent)
152
154
  event
153
155
  end
154
156
 
@@ -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
@@ -74,11 +74,11 @@ module Sentry
74
74
  end
75
75
 
76
76
  def start_sentry_span
77
- return unless Sentry.initialized? && transaction = Sentry.get_current_scope.get_transaction
77
+ return unless Sentry.initialized? && span = Sentry.get_current_scope.get_span
78
78
  return if from_sentry_sdk?
79
- return if transaction.sampled == false
79
+ return if span.sampled == false
80
80
 
81
- transaction.start_child(op: OP_NAME, start_timestamp: Sentry.utc_now.to_f)
81
+ span.start_child(op: OP_NAME, start_timestamp: Sentry.utc_now.to_f)
82
82
  end
83
83
 
84
84
  def finish_sentry_span(sentry_span)
@@ -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,80 @@
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
+ log_debug("Killing session flusher")
43
+ @thread&.kill
44
+ end
45
+
46
+ private
47
+
48
+ def init_aggregates(aggregation_key)
49
+ aggregates = { started: aggregation_key.iso8601 }
50
+ Session::AGGREGATE_STATUSES.each { |k| aggregates[k] = 0 }
51
+ aggregates
52
+ end
53
+
54
+ def pending_envelope
55
+ envelope = Envelope.new
56
+
57
+ header = { type: 'sessions' }
58
+ payload = { attrs: attrs, aggregates: @pending_aggregates.values }
59
+
60
+ envelope.add_item(header, payload)
61
+ envelope
62
+ end
63
+
64
+ def attrs
65
+ { release: @release, environment: @environment }
66
+ end
67
+
68
+ def ensure_thread
69
+ return if @thread&.alive?
70
+
71
+ @thread = Thread.new do
72
+ loop do
73
+ sleep(FLUSH_INTERVAL)
74
+ flush
75
+ end
76
+ end
77
+ end
78
+
79
+ end
80
+ end
@@ -1,31 +1,15 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Sentry
4
+ # TransactionEvent represents events that carry transaction data (type: "transaction").
4
5
  class TransactionEvent < Event
5
6
  TYPE = "transaction"
6
7
 
7
- SERIALIZEABLE_ATTRIBUTES = %i(
8
- event_id level timestamp start_timestamp
9
- release environment server_name modules
10
- user tags contexts extra
11
- transaction platform sdk type
12
- )
13
-
14
- WRITER_ATTRIBUTES = SERIALIZEABLE_ATTRIBUTES - %i(type timestamp start_timestamp level)
15
-
16
- attr_writer(*WRITER_ATTRIBUTES)
17
- attr_reader(*SERIALIZEABLE_ATTRIBUTES)
18
-
19
8
  # @return [<Array[Span]>]
20
9
  attr_accessor :spans
21
10
 
22
- # @param configuration [Configuration]
23
- # @param integration_meta [Hash, nil]
24
- # @param message [String, nil]
25
- def initialize(configuration:, integration_meta: nil, message: nil)
26
- super
27
- @type = TYPE
28
- end
11
+ # @return [Float, nil]
12
+ attr_reader :start_timestamp
29
13
 
30
14
  # Sets the event's start_timestamp.
31
15
  # @param time [Time, Float]
@@ -38,6 +22,7 @@ module Sentry
38
22
  def to_hash
39
23
  data = super
40
24
  data[:spans] = @spans.map(&:to_hash) if @spans
25
+ data[:start_timestamp] = @start_timestamp
41
26
  data
42
27
  end
43
28
  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
@@ -130,7 +130,7 @@ module Sentry
130
130
  server = URI(@dsn.server)
131
131
 
132
132
  connection =
133
- if proxy = @transport_configuration.proxy
133
+ if proxy = normalize_proxy(@transport_configuration.proxy)
134
134
  ::Net::HTTP.new(server.hostname, server.port, proxy[:uri].hostname, proxy[:uri].port, proxy[:user], proxy[:password])
135
135
  else
136
136
  ::Net::HTTP.new(server.hostname, server.port, nil)
@@ -148,6 +148,20 @@ module Sentry
148
148
  connection
149
149
  end
150
150
 
151
+ def normalize_proxy(proxy)
152
+ return proxy unless proxy
153
+
154
+ case proxy
155
+ when String
156
+ uri = URI(proxy)
157
+ { uri: uri, user: uri.user, password: uri.password }
158
+ when URI
159
+ { uri: proxy, user: proxy.user, password: proxy.password }
160
+ when Hash
161
+ proxy
162
+ end
163
+ end
164
+
151
165
  def ssl_configuration
152
166
  configuration = {
153
167
  verify: @transport_configuration.ssl_verification,
@@ -57,8 +57,48 @@ 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
+ if item.payload.key?(:breadcrumbs)
77
+ item.payload.delete(:breadcrumbs)
78
+ elsif item.payload.key?("breadcrumbs")
79
+ item.payload.delete("breadcrumbs")
80
+ end
81
+
82
+ result = item.to_s
83
+ end
84
+
85
+ if result.bytesize > Event::MAX_SERIALIZED_PAYLOAD_SIZE
86
+ size_breakdown = item.payload.map do |key, value|
87
+ "#{key}: #{JSON.generate(value).bytesize}"
88
+ end.join(", ")
89
+
90
+ log_debug("Envelope item [#{item.type}] is still oversized without breadcrumbs: {#{size_breakdown}}")
91
+
92
+ next
93
+ end
94
+
95
+ serialized_results << result
96
+ serialized_items << item
97
+ end
98
+
99
+ data = [JSON.generate(envelope.headers), *serialized_results].join("\n") unless serialized_results.empty?
100
+
101
+ [data, serialized_items]
62
102
  end
63
103
 
64
104
  def is_rate_limited?(item_type)
@@ -67,6 +107,8 @@ module Sentry
67
107
  case item_type
68
108
  when "transaction"
69
109
  @rate_limits["transaction"]
110
+ when "sessions"
111
+ @rate_limits["session"]
70
112
  else
71
113
  @rate_limits["error"]
72
114
  end
@@ -106,7 +148,7 @@ module Sentry
106
148
  # Convert to hash
107
149
  event_payload = event.to_hash
108
150
  event_id = event_payload[:event_id] || event_payload["event_id"]
109
- item_type = get_item_type(event_payload)
151
+ item_type = event_payload[:type] || event_payload["type"]
110
152
 
111
153
  envelope = Envelope.new(
112
154
  {
@@ -125,7 +167,6 @@ module Sentry
125
167
  client_report_headers, client_report_payload = fetch_pending_client_report
126
168
  envelope.add_item(client_report_headers, client_report_payload) if client_report_headers
127
169
 
128
-
129
170
  envelope
130
171
  end
131
172
 
@@ -133,16 +174,11 @@ module Sentry
133
174
  return unless @send_client_reports
134
175
  return unless CLIENT_REPORT_REASONS.include?(reason)
135
176
 
136
- item_type ||= 'event'
137
177
  @discarded_events[[reason, item_type]] += 1
138
178
  end
139
179
 
140
180
  private
141
181
 
142
- def get_item_type(event_hash)
143
- event_hash[:type] || event_hash["type"] || "event"
144
- end
145
-
146
182
  def fetch_pending_client_report
147
183
  return nil unless @send_client_reports
148
184
  return nil if @last_client_report_sent > Time.now - CLIENT_REPORT_INTERVAL
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Sentry
4
- VERSION = "5.1.1"
4
+ VERSION = "5.3.0"
5
5
  end
data/lib/sentry-ruby.rb CHANGED
@@ -12,11 +12,13 @@ require "sentry/utils/logging_helper"
12
12
  require "sentry/configuration"
13
13
  require "sentry/logger"
14
14
  require "sentry/event"
15
+ require "sentry/error_event"
15
16
  require "sentry/transaction_event"
16
17
  require "sentry/span"
17
18
  require "sentry/transaction"
18
19
  require "sentry/hub"
19
20
  require "sentry/background_worker"
21
+ require "sentry/session_flusher"
20
22
 
21
23
  [
22
24
  "sentry/rake",
@@ -61,6 +63,10 @@ module Sentry
61
63
  # @return [BackgroundWorker]
62
64
  attr_accessor :background_worker
63
65
 
66
+ # @!attribute [r] session_flusher
67
+ # @return [SessionFlusher]
68
+ attr_reader :session_flusher
69
+
64
70
  ##### Patch Registration #####
65
71
 
66
72
  # @!visibility private
@@ -94,6 +100,14 @@ module Sentry
94
100
  # @param name [String] name of the integration
95
101
  # @param version [String] version of the integration
96
102
  def register_integration(name, version)
103
+ if initialized?
104
+ logger.warn(LOGGER_PROGNAME) do
105
+ <<~MSG
106
+ Integration '#{name}' is loaded after the SDK is initialized, which can cause unexpected behavior. Please make sure all integrations are loaded before SDK initialization.
107
+ MSG
108
+ end
109
+ end
110
+
97
111
  meta = { name: "sentry.ruby.#{name}", version: version }.freeze
98
112
  integrations[name.to_s] = meta
99
113
  end
@@ -189,11 +203,18 @@ module Sentry
189
203
  @main_hub = hub
190
204
  @background_worker = Sentry::BackgroundWorker.new(config)
191
205
 
206
+ @session_flusher = if config.auto_session_tracking
207
+ Sentry::SessionFlusher.new(config, client)
208
+ else
209
+ nil
210
+ end
211
+
192
212
  if config.capture_exception_frame_locals
193
213
  exception_locals_tp.enable
194
214
  end
195
215
 
196
216
  at_exit do
217
+ @session_flusher&.kill
197
218
  @background_worker.shutdown
198
219
  end
199
220
  end
@@ -310,6 +331,26 @@ module Sentry
310
331
  get_current_hub.with_scope(&block)
311
332
  end
312
333
 
334
+ # Wrap a given block with session tracking.
335
+ # Aggregate sessions in minutely buckets will be recorded
336
+ # around this block and flushed every minute.
337
+ #
338
+ # @example
339
+ # Sentry.with_session_tracking do
340
+ # a = 1 + 1 # new session recorded with :exited status
341
+ # end
342
+ #
343
+ # Sentry.with_session_tracking do
344
+ # 1 / 0
345
+ # rescue => e
346
+ # Sentry.capture_exception(e) # new session recorded with :errored status
347
+ # end
348
+ # @return [void]
349
+ def with_session_tracking(&block)
350
+ return yield unless initialized?
351
+ get_current_hub.with_session_tracking(&block)
352
+ end
353
+
313
354
  # Takes an exception and reports it to Sentry via the currently active hub.
314
355
  #
315
356
  # @yieldparam scope [Scope]
@@ -344,6 +385,40 @@ module Sentry
344
385
  get_current_hub.start_transaction(**options)
345
386
  end
346
387
 
388
+ # Records the block's execution as a child of the current span.
389
+ # If the current scope doesn't have a span, the block would still be executed but the yield param will be nil.
390
+ # @param attributes [Hash] attributes for the child span.
391
+ # @yieldparam child_span [Span, nil]
392
+ # @return yield result
393
+ #
394
+ # @example
395
+ # Sentry.with_child_span(op: "my operation") do |child_span|
396
+ # child_span.set_data(operation_data)
397
+ # child_span.set_description(operation_detail)
398
+ # # result will be returned
399
+ # end
400
+ #
401
+ def with_child_span(**attributes, &block)
402
+ current_span = get_current_scope.get_span
403
+
404
+ if current_span
405
+ result = nil
406
+
407
+ begin
408
+ current_span.with_child_span(**attributes) do |child_span|
409
+ get_current_scope.set_span(child_span)
410
+ result = yield(child_span)
411
+ end
412
+ ensure
413
+ get_current_scope.set_span(current_span)
414
+ end
415
+
416
+ result
417
+ else
418
+ yield(nil)
419
+ end
420
+ end
421
+
347
422
  # Returns the id of the lastly reported Sentry::Event.
348
423
  #
349
424
  # @return [String, nil]
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.3.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-04-27 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: concurrent-ruby
@@ -56,6 +56,7 @@ files:
56
56
  - lib/sentry/core_ext/object/duplicable.rb
57
57
  - lib/sentry/dsn.rb
58
58
  - lib/sentry/envelope.rb
59
+ - lib/sentry/error_event.rb
59
60
  - lib/sentry/event.rb
60
61
  - lib/sentry/exceptions.rb
61
62
  - lib/sentry/hub.rb
@@ -76,6 +77,8 @@ files:
76
77
  - lib/sentry/redis.rb
77
78
  - lib/sentry/release_detector.rb
78
79
  - lib/sentry/scope.rb
80
+ - lib/sentry/session.rb
81
+ - lib/sentry/session_flusher.rb
79
82
  - lib/sentry/span.rb
80
83
  - lib/sentry/transaction.rb
81
84
  - lib/sentry/transaction_event.rb