sentry-ruby 5.18.2 → 5.28.1

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.
Files changed (78) hide show
  1. checksums.yaml +4 -4
  2. data/.rspec +3 -1
  3. data/Gemfile +9 -3
  4. data/README.md +12 -7
  5. data/Rakefile +9 -11
  6. data/bin/console +1 -0
  7. data/lib/sentry/attachment.rb +40 -0
  8. data/lib/sentry/background_worker.rb +2 -3
  9. data/lib/sentry/backpressure_monitor.rb +1 -1
  10. data/lib/sentry/backtrace.rb +7 -8
  11. data/lib/sentry/baggage.rb +7 -7
  12. data/lib/sentry/breadcrumb/sentry_logger.rb +6 -6
  13. data/lib/sentry/breadcrumb.rb +5 -4
  14. data/lib/sentry/check_in_event.rb +5 -4
  15. data/lib/sentry/client.rb +121 -19
  16. data/lib/sentry/configuration.rb +207 -38
  17. data/lib/sentry/core_ext/object/deep_dup.rb +1 -1
  18. data/lib/sentry/cron/monitor_check_ins.rb +6 -4
  19. data/lib/sentry/cron/monitor_config.rb +1 -1
  20. data/lib/sentry/debug_structured_logger.rb +94 -0
  21. data/lib/sentry/dsn.rb +35 -3
  22. data/lib/sentry/envelope/item.rb +88 -0
  23. data/lib/sentry/envelope.rb +2 -85
  24. data/lib/sentry/event.rb +13 -8
  25. data/lib/sentry/excon/middleware.rb +77 -0
  26. data/lib/sentry/excon.rb +10 -0
  27. data/lib/sentry/faraday.rb +77 -0
  28. data/lib/sentry/graphql.rb +1 -1
  29. data/lib/sentry/hub.rb +57 -4
  30. data/lib/sentry/interfaces/mechanism.rb +1 -1
  31. data/lib/sentry/interfaces/request.rb +6 -6
  32. data/lib/sentry/interfaces/single_exception.rb +3 -3
  33. data/lib/sentry/interfaces/stacktrace.rb +3 -1
  34. data/lib/sentry/interfaces/stacktrace_builder.rb +15 -2
  35. data/lib/sentry/linecache.rb +3 -3
  36. data/lib/sentry/log_event.rb +219 -0
  37. data/lib/sentry/log_event_buffer.rb +75 -0
  38. data/lib/sentry/logger.rb +1 -1
  39. data/lib/sentry/metrics/aggregator.rb +13 -13
  40. data/lib/sentry/metrics/configuration.rb +12 -2
  41. data/lib/sentry/metrics/set_metric.rb +2 -2
  42. data/lib/sentry/metrics/timing.rb +8 -0
  43. data/lib/sentry/metrics.rb +27 -15
  44. data/lib/sentry/net/http.rb +17 -38
  45. data/lib/sentry/profiler/helpers.rb +46 -0
  46. data/lib/sentry/profiler.rb +27 -57
  47. data/lib/sentry/propagation_context.rb +60 -23
  48. data/lib/sentry/rack/capture_exceptions.rb +2 -2
  49. data/lib/sentry/rack.rb +2 -2
  50. data/lib/sentry/rake.rb +2 -2
  51. data/lib/sentry/release_detector.rb +4 -4
  52. data/lib/sentry/rspec.rb +91 -0
  53. data/lib/sentry/scope.rb +27 -2
  54. data/lib/sentry/session_flusher.rb +10 -6
  55. data/lib/sentry/span.rb +17 -4
  56. data/lib/sentry/std_lib_logger.rb +55 -0
  57. data/lib/sentry/structured_logger.rb +138 -0
  58. data/lib/sentry/test_helper.rb +45 -1
  59. data/lib/sentry/threaded_periodic_worker.rb +3 -3
  60. data/lib/sentry/transaction.rb +41 -13
  61. data/lib/sentry/transaction_event.rb +5 -3
  62. data/lib/sentry/transport/debug_transport.rb +70 -0
  63. data/lib/sentry/transport/dummy_transport.rb +1 -0
  64. data/lib/sentry/transport/http_transport.rb +21 -18
  65. data/lib/sentry/transport.rb +26 -11
  66. data/lib/sentry/utils/env_helper.rb +21 -0
  67. data/lib/sentry/utils/http_tracing.rb +74 -0
  68. data/lib/sentry/utils/logging_helper.rb +10 -3
  69. data/lib/sentry/utils/real_ip.rb +1 -1
  70. data/lib/sentry/utils/sample_rand.rb +97 -0
  71. data/lib/sentry/utils/uuid.rb +13 -0
  72. data/lib/sentry/vernier/output.rb +89 -0
  73. data/lib/sentry/vernier/profiler.rb +132 -0
  74. data/lib/sentry/version.rb +1 -1
  75. data/lib/sentry-ruby.rb +106 -12
  76. data/sentry-ruby-core.gemspec +3 -1
  77. data/sentry-ruby.gemspec +4 -2
  78. metadata +28 -12
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sentry
4
+ # Ruby Logger support Add commentMore actions
5
+ # intercepts any logger instance and send the log to Sentry too.
6
+ module StdLibLogger
7
+ SEVERITY_MAP = {
8
+ 0 => :debug,
9
+ 1 => :info,
10
+ 2 => :warn,
11
+ 3 => :error,
12
+ 4 => :fatal
13
+ }.freeze
14
+
15
+ ORIGIN = "auto.log.ruby.std_logger"
16
+
17
+ def add(severity, message = nil, progname = nil, &block)
18
+ result = super
19
+
20
+ return unless Sentry.initialized? && Sentry.get_current_hub
21
+
22
+ # Only process logs that meet or exceed the logger's level
23
+ return result if severity < level
24
+
25
+ # exclude sentry SDK logs -- to prevent recursive log action,
26
+ # do not process internal logs again
27
+ if message.nil? && progname != Sentry::Logger::PROGNAME
28
+
29
+ # handle different nature of Ruby Logger class:
30
+ # inspo from Sentry::Breadcrumb::SentryLogger
31
+ if block_given?
32
+ message = yield
33
+ else
34
+ message = progname
35
+ end
36
+
37
+ message = message.to_s.strip
38
+
39
+ if !message.nil? && message != Sentry::Logger::PROGNAME && method = SEVERITY_MAP[severity]
40
+ Sentry.logger.send(method, message, origin: ORIGIN)
41
+ end
42
+ end
43
+
44
+ result
45
+ end
46
+ end
47
+ end
48
+
49
+ Sentry.register_patch(:logger) do |config|
50
+ if config.enable_logs
51
+ ::Logger.prepend(Sentry::StdLibLogger)
52
+ else
53
+ config.sdk_logger.warn(":logger patch enabled but `enable_logs` is turned off - skipping applying patch")
54
+ end
55
+ end
@@ -0,0 +1,138 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sentry
4
+ # The StructuredLogger class implements Sentry's SDK telemetry logs protocol.
5
+ # It provides methods for logging messages at different severity levels and
6
+ # sending them to Sentry with structured data.
7
+ #
8
+ # This class follows the Sentry Logs Protocol as defined in:
9
+ # https://develop.sentry.dev/sdk/telemetry/logs/
10
+ #
11
+ # @example Basic usage
12
+ # Sentry.logger.info("User logged in", user_id: 123)
13
+ #
14
+ # @example With structured data
15
+ # Sentry.logger.warn("API request failed",
16
+ # status_code: 404,
17
+ # endpoint: "/api/users",
18
+ # request_id: "abc-123"
19
+ # )
20
+ #
21
+ # @example With a message template
22
+ # # Using positional parameters
23
+ # Sentry.logger.info("User %s logged in", ["Jane Doe"])
24
+ #
25
+ # # Using hash parameters
26
+ # Sentry.logger.info("User %{name} logged in", name: "Jane Doe")
27
+ #
28
+ # # Using hash parameters and extra attributes
29
+ # Sentry.logger.info("User %{name} logged in", name: "Jane Doe", user_id: 312)
30
+ #
31
+ # @see https://develop.sentry.dev/sdk/telemetry/logs/ Sentry SDK Telemetry Logs Protocol
32
+ class StructuredLogger
33
+ # Severity number mapping for log levels according to the Sentry Logs Protocol
34
+ # @see https://develop.sentry.dev/sdk/telemetry/logs/#log-severity-number
35
+ LEVELS = {
36
+ trace: 1,
37
+ debug: 5,
38
+ info: 9,
39
+ warn: 13,
40
+ error: 17,
41
+ fatal: 21
42
+ }.freeze
43
+
44
+ # @return [Configuration] The Sentry configuration
45
+ # @!visibility private
46
+ attr_reader :config
47
+
48
+ # Initializes a new StructuredLogger instance
49
+ #
50
+ # @param config [Configuration] The Sentry configuration
51
+ def initialize(config)
52
+ @config = config
53
+ end
54
+
55
+ # Logs a message at TRACE level
56
+ #
57
+ # @param message [String] The log message
58
+ # @param parameters [Array] Array of values to replace template parameters in the message
59
+ # @param attributes [Hash] Additional attributes to include with the log
60
+ #
61
+ # @return [LogEvent, nil] The created log event or nil if logging is disabled
62
+ def trace(message, parameters = [], **attributes)
63
+ log(__method__, message, parameters: parameters, **attributes)
64
+ end
65
+
66
+ # Logs a message at DEBUG level
67
+ #
68
+ # @param message [String] The log message
69
+ # @param parameters [Array] Array of values to replace template parameters in the message
70
+ # @param attributes [Hash] Additional attributes to include with the log
71
+ #
72
+ # @return [LogEvent, nil] The created log event or nil if logging is disabled
73
+ def debug(message, parameters = [], **attributes)
74
+ log(__method__, message, parameters: parameters, **attributes)
75
+ end
76
+
77
+ # Logs a message at INFO level
78
+ #
79
+ # @param message [String] The log message
80
+ # @param parameters [Array] Array of values to replace template parameters in the message
81
+ # @param attributes [Hash] Additional attributes to include with the log
82
+ #
83
+ # @return [LogEvent, nil] The created log event or nil if logging is disabled
84
+ def info(message, parameters = [], **attributes)
85
+ log(__method__, message, parameters: parameters, **attributes)
86
+ end
87
+
88
+ # Logs a message at WARN level
89
+ #
90
+ # @param message [String] The log message
91
+ # @param parameters [Array] Array of values to replace template parameters in the message
92
+ # @param attributes [Hash] Additional attributes to include with the log
93
+ #
94
+ # @return [LogEvent, nil] The created log event or nil if logging is disabled
95
+ def warn(message, parameters = [], **attributes)
96
+ log(__method__, message, parameters: parameters, **attributes)
97
+ end
98
+
99
+ # Logs a message at ERROR level
100
+ #
101
+ # @param message [String] The log message
102
+ # @param parameters [Array] Array of values to replace template parameters in the message
103
+ # @param attributes [Hash] Additional attributes to include with the log
104
+ #
105
+ # @return [LogEvent, nil] The created log event or nil if logging is disabled
106
+ def error(message, parameters = [], **attributes)
107
+ log(__method__, message, parameters: parameters, **attributes)
108
+ end
109
+
110
+ # Logs a message at FATAL level
111
+ #
112
+ # @param message [String] The log message
113
+ # @param parameters [Array] Array of values to replace template parameters in the message
114
+ # @param attributes [Hash] Additional attributes to include with the log
115
+ #
116
+ # @return [LogEvent, nil] The created log event or nil if logging is disabled
117
+ def fatal(message, parameters = [], **attributes)
118
+ log(__method__, message, parameters: parameters, **attributes)
119
+ end
120
+
121
+ # Logs a message at the specified level
122
+ #
123
+ # @param level [Symbol] The log level (:trace, :debug, :info, :warn, :error, :fatal)
124
+ # @param message [String] The log message
125
+ # @param parameters [Array, Hash] Array or Hash of values to replace template parameters in the message
126
+ # @param attributes [Hash] Additional attributes to include with the log
127
+ #
128
+ # @return [LogEvent, nil] The created log event or nil if logging is disabled
129
+ def log(level, message, parameters:, **attributes)
130
+ case parameters
131
+ when Array then
132
+ Sentry.capture_log(message, level: level, severity: LEVELS[level], parameters: parameters, **attributes)
133
+ else
134
+ Sentry.capture_log(message, level: level, severity: LEVELS[level], **parameters)
135
+ end
136
+ end
137
+ end
138
+ end
@@ -1,6 +1,13 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Sentry
2
4
  module TestHelper
3
- DUMMY_DSN = 'http://12345:67890@sentry.localdomain/sentry/42'
5
+ module_function
6
+
7
+ DUMMY_DSN = "http://12345:67890@sentry.localdomain/sentry/42"
8
+
9
+ # Not really real, but it will be resolved as a non-local for testing needs
10
+ REAL_DSN = "https://user:pass@getsentry.io/project/42"
4
11
 
5
12
  # Alters the existing SDK configuration with test-suitable options. Mainly:
6
13
  # - Sets a dummy DSN instead of `nil` or an actual DSN.
@@ -44,6 +51,8 @@ module Sentry
44
51
  def teardown_sentry_test
45
52
  return unless Sentry.initialized?
46
53
 
54
+ clear_sentry_events
55
+
47
56
  # pop testing layer created by `setup_sentry_test`
48
57
  # but keep the base layer to avoid nil-pointer errors
49
58
  # TODO: find a way to notify users if they somehow popped the test layer before calling this method
@@ -53,6 +62,21 @@ module Sentry
53
62
  Sentry::Scope.global_event_processors.clear
54
63
  end
55
64
 
65
+ def clear_sentry_events
66
+ return unless Sentry.initialized?
67
+
68
+ sentry_transport.clear if sentry_transport.respond_to?(:clear)
69
+
70
+ if Sentry.configuration.enable_logs && sentry_logger.respond_to?(:clear)
71
+ sentry_logger.clear
72
+ end
73
+ end
74
+
75
+ # @return [Sentry::StructuredLogger, Sentry::DebugStructuredLogger]
76
+ def sentry_logger
77
+ Sentry.logger
78
+ end
79
+
56
80
  # @return [Transport]
57
81
  def sentry_transport
58
82
  Sentry.get_current_client.transport
@@ -70,6 +94,13 @@ module Sentry
70
94
  sentry_transport.envelopes
71
95
  end
72
96
 
97
+ def sentry_logs
98
+ sentry_envelopes
99
+ .flat_map(&:items)
100
+ .select { |item| item.headers[:type] == "log" }
101
+ .flat_map { |item| item.payload[:items] }
102
+ end
103
+
73
104
  # Returns the last captured event object.
74
105
  # @return [Event, nil]
75
106
  def last_sentry_event
@@ -81,5 +112,18 @@ module Sentry
81
112
  def extract_sentry_exceptions(event)
82
113
  event&.exception&.values || []
83
114
  end
115
+
116
+ def reset_sentry_globals!
117
+ Sentry::MUTEX.synchronize do
118
+ # Don't check initialized? because sometimes we stub it in tests
119
+ if Sentry.instance_variable_defined?(:@main_hub)
120
+ Sentry::GLOBALS.each do |var|
121
+ Sentry.instance_variable_set(:"@#{var}", nil)
122
+ end
123
+
124
+ Thread.current.thread_variable_set(Sentry::THREAD_LOCAL, nil)
125
+ end
126
+ end
127
+ end
84
128
  end
85
129
  end
@@ -4,11 +4,11 @@ module Sentry
4
4
  class ThreadedPeriodicWorker
5
5
  include LoggingHelper
6
6
 
7
- def initialize(logger, internal)
7
+ def initialize(sdk_logger, interval)
8
8
  @thread = nil
9
9
  @exited = false
10
- @interval = internal
11
- @logger = logger
10
+ @interval = interval
11
+ @sdk_logger = sdk_logger
12
12
  end
13
13
 
14
14
  def ensure_thread
@@ -2,6 +2,7 @@
2
2
 
3
3
  require "sentry/baggage"
4
4
  require "sentry/profiler"
5
+ require "sentry/utils/sample_rand"
5
6
  require "sentry/propagation_context"
6
7
 
7
8
  module Sentry
@@ -9,7 +10,7 @@ module Sentry
9
10
  # @deprecated Use Sentry::PropagationContext::SENTRY_TRACE_REGEXP instead.
10
11
  SENTRY_TRACE_REGEXP = PropagationContext::SENTRY_TRACE_REGEXP
11
12
 
12
- UNLABELD_NAME = "<unlabeled transaction>".freeze
13
+ UNLABELD_NAME = "<unlabeled transaction>"
13
14
  MESSAGE_PREFIX = "[Tracing]"
14
15
 
15
16
  # https://develop.sentry.dev/sdk/event-payloads/transaction/#transaction-annotations
@@ -45,9 +46,6 @@ module Sentry
45
46
  # @deprecated Use Sentry.configuration instead.
46
47
  attr_reader :configuration
47
48
 
48
- # @deprecated Use Sentry.logger instead.
49
- attr_reader :logger
50
-
51
49
  # The effective sample rate at which this transaction was sampled.
52
50
  # @return [Float, nil]
53
51
  attr_reader :effective_sample_rate
@@ -60,12 +58,17 @@ module Sentry
60
58
  # @return [Profiler]
61
59
  attr_reader :profiler
62
60
 
61
+ # Sample rand value generated from trace_id
62
+ # @return [String]
63
+ attr_reader :sample_rand
64
+
63
65
  def initialize(
64
66
  hub:,
65
67
  name: nil,
66
68
  source: :custom,
67
69
  parent_sampled: nil,
68
70
  baggage: nil,
71
+ sample_rand: nil,
69
72
  **options
70
73
  )
71
74
  super(transaction: self, **options)
@@ -78,15 +81,25 @@ module Sentry
78
81
  @tracing_enabled = hub.configuration.tracing_enabled?
79
82
  @traces_sampler = hub.configuration.traces_sampler
80
83
  @traces_sample_rate = hub.configuration.traces_sample_rate
81
- @logger = hub.configuration.logger
84
+ @sdk_logger = hub.configuration.sdk_logger
82
85
  @release = hub.configuration.release
83
86
  @environment = hub.configuration.environment
84
87
  @dsn = hub.configuration.dsn
85
88
  @effective_sample_rate = nil
86
89
  @contexts = {}
87
90
  @measurements = {}
88
- @profiler = Profiler.new(@configuration)
91
+ @sample_rand = sample_rand
92
+
93
+ unless @hub.profiler_running?
94
+ @profiler = @configuration.profiler_class.new(@configuration)
95
+ end
96
+
89
97
  init_span_recorder
98
+
99
+ unless @sample_rand
100
+ generator = Utils::SampleRand.new(trace_id: @trace_id)
101
+ @sample_rand = generator.generate_from_trace_id
102
+ end
90
103
  end
91
104
 
92
105
  # @deprecated use Sentry.continue_trace instead.
@@ -122,12 +135,15 @@ module Sentry
122
135
 
123
136
  baggage.freeze!
124
137
 
138
+ sample_rand = extract_sample_rand_from_baggage(baggage, trace_id, parent_sampled)
139
+
125
140
  new(
126
141
  trace_id: trace_id,
127
142
  parent_span_id: parent_span_id,
128
143
  parent_sampled: parent_sampled,
129
144
  hub: hub,
130
145
  baggage: baggage,
146
+ sample_rand: sample_rand,
131
147
  **options
132
148
  )
133
149
  end
@@ -138,6 +154,11 @@ module Sentry
138
154
  PropagationContext.extract_sentry_trace(sentry_trace)
139
155
  end
140
156
 
157
+ def self.extract_sample_rand_from_baggage(baggage, trace_id, parent_sampled)
158
+ PropagationContext.extract_sample_rand_from_baggage(baggage, trace_id) ||
159
+ PropagationContext.generate_sample_rand(baggage, trace_id, parent_sampled)
160
+ end
161
+
141
162
  # @return [Hash]
142
163
  def to_hash
143
164
  hash = super
@@ -152,6 +173,13 @@ module Sentry
152
173
  hash
153
174
  end
154
175
 
176
+ def parent_sample_rate
177
+ return unless @baggage&.items
178
+
179
+ sample_rate_str = @baggage.items["sample_rate"]
180
+ sample_rate_str&.to_f
181
+ end
182
+
155
183
  # @return [Transaction]
156
184
  def deep_dup
157
185
  copy = super
@@ -224,7 +252,7 @@ module Sentry
224
252
  @effective_sample_rate /= 2**factor
225
253
  end
226
254
 
227
- @sampled = Random.rand < @effective_sample_rate
255
+ @sampled = @sample_rand < @effective_sample_rate
228
256
  end
229
257
 
230
258
  if @sampled
@@ -257,7 +285,7 @@ module Sentry
257
285
  @name = UNLABELD_NAME
258
286
  end
259
287
 
260
- @profiler.stop
288
+ @hub.stop_profiler!(self)
261
289
 
262
290
  if @sampled
263
291
  event = hub.current_client.event_from_transaction(self)
@@ -265,8 +293,8 @@ module Sentry
265
293
  else
266
294
  is_backpressure = Sentry.backpressure_monitor&.downsample_factor&.positive?
267
295
  reason = is_backpressure ? :backpressure : :sample_rate
268
- hub.current_client.transport.record_lost_event(reason, 'transaction')
269
- hub.current_client.transport.record_lost_event(reason, 'span')
296
+ hub.current_client.transport.record_lost_event(reason, "transaction")
297
+ hub.current_client.transport.record_lost_event(reason, "span")
270
298
  end
271
299
  end
272
300
 
@@ -299,6 +327,8 @@ module Sentry
299
327
  # Start the profiler.
300
328
  # @return [void]
301
329
  def start_profiler!
330
+ return unless profiler
331
+
302
332
  profiler.set_initial_sample_decision(sampled)
303
333
  profiler.start
304
334
  end
@@ -328,6 +358,7 @@ module Sentry
328
358
  items = {
329
359
  "trace_id" => trace_id,
330
360
  "sample_rate" => effective_sample_rate&.to_s,
361
+ "sample_rand" => Utils::SampleRand.format(@sample_rand),
331
362
  "sampled" => sampled&.to_s,
332
363
  "environment" => @environment,
333
364
  "release" => @release,
@@ -336,9 +367,6 @@ module Sentry
336
367
 
337
368
  items["transaction"] = name unless source_low_quality?
338
369
 
339
- user = @hub.current_scope&.user
340
- items["user_segment"] = user["segment"] if user && user["segment"]
341
-
342
370
  items.compact!
343
371
  @baggage = Baggage.new(items, mutable: false)
344
372
  end
@@ -59,8 +59,11 @@ module Sentry
59
59
 
60
60
  private
61
61
 
62
+ EMPTY_PROFILE = {}.freeze
63
+
62
64
  def populate_profile(transaction)
63
- profile_hash = transaction.profiler.to_hash
65
+ profile_hash = transaction.profiler&.to_hash || EMPTY_PROFILE
66
+
64
67
  return if profile_hash.empty?
65
68
 
66
69
  profile_hash.merge!(
@@ -74,8 +77,7 @@ module Sentry
74
77
  id: event_id,
75
78
  name: transaction.name,
76
79
  trace_id: transaction.trace_id,
77
- # TODO-neel-profiler stubbed for now, see thread_id note in profiler.rb
78
- active_thead_id: '0'
80
+ active_thread_id: transaction.profiler.active_thread_id.to_s
79
81
  }
80
82
  )
81
83
 
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "fileutils"
5
+ require "pathname"
6
+ require "delegate"
7
+
8
+ module Sentry
9
+ # DebugTransport is a transport that logs events to a file for debugging purposes.
10
+ #
11
+ # It can optionally also send events to Sentry via HTTP transport if a real DSN
12
+ # is provided.
13
+ class DebugTransport < SimpleDelegator
14
+ DEFAULT_LOG_FILE_PATH = File.join("log", "sentry_debug_events.log")
15
+
16
+ attr_reader :log_file, :backend
17
+
18
+ def initialize(configuration)
19
+ @log_file = initialize_log_file(configuration)
20
+ @backend = initialize_backend(configuration)
21
+
22
+ super(@backend)
23
+ end
24
+
25
+ def send_event(event)
26
+ log_envelope(envelope_from_event(event))
27
+ backend.send_event(event)
28
+ end
29
+
30
+ def log_envelope(envelope)
31
+ envelope_json = {
32
+ timestamp: Time.now.utc.iso8601,
33
+ envelope_headers: envelope.headers,
34
+ items: envelope.items.map do |item|
35
+ { headers: item.headers, payload: item.payload }
36
+ end
37
+ }
38
+
39
+ File.open(log_file, "a") { |file| file << JSON.dump(envelope_json) << "\n" }
40
+ end
41
+
42
+ def logged_envelopes
43
+ return [] unless File.exist?(log_file)
44
+
45
+ File.readlines(log_file).map do |line|
46
+ JSON.parse(line)
47
+ end
48
+ end
49
+
50
+ def clear
51
+ File.write(log_file, "")
52
+ log_debug("DebugTransport: Cleared events from #{log_file}")
53
+ end
54
+
55
+ private
56
+
57
+ def initialize_backend(configuration)
58
+ backend = configuration.dsn.local? ? DummyTransport : HTTPTransport
59
+ backend.new(configuration)
60
+ end
61
+
62
+ def initialize_log_file(configuration)
63
+ log_file = Pathname(configuration.sdk_debug_transport_log_file || DEFAULT_LOG_FILE_PATH)
64
+
65
+ FileUtils.mkdir_p(log_file.dirname) unless log_file.dirname.exist?
66
+
67
+ log_file
68
+ end
69
+ end
70
+ end
@@ -12,6 +12,7 @@ module Sentry
12
12
 
13
13
  def send_event(event)
14
14
  @events << event
15
+ super
15
16
  end
16
17
 
17
18
  def send_envelope(envelope)
@@ -7,7 +7,7 @@ module Sentry
7
7
  class HTTPTransport < Transport
8
8
  GZIP_ENCODING = "gzip"
9
9
  GZIP_THRESHOLD = 1024 * 30
10
- CONTENT_TYPE = 'application/x-sentry-envelope'
10
+ CONTENT_TYPE = "application/x-sentry-envelope"
11
11
 
12
12
  DEFAULT_DELAY = 60
13
13
  RETRY_AFTER_HEADER = "retry-after"
@@ -23,7 +23,6 @@ module Sentry
23
23
  Zlib::BufError, Errno::EHOSTUNREACH, Errno::ECONNREFUSED
24
24
  ].freeze
25
25
 
26
-
27
26
  def initialize(*args)
28
27
  super
29
28
  log_debug("Sentry HTTP Transport will connect to #{@dsn.server}") if @dsn
@@ -38,19 +37,15 @@ module Sentry
38
37
  end
39
38
 
40
39
  headers = {
41
- 'Content-Type' => CONTENT_TYPE,
42
- 'Content-Encoding' => encoding,
43
- 'User-Agent' => USER_AGENT
40
+ "Content-Type" => CONTENT_TYPE,
41
+ "Content-Encoding" => encoding,
42
+ "User-Agent" => USER_AGENT
44
43
  }
45
44
 
46
45
  auth_header = generate_auth_header
47
- headers['X-Sentry-Auth'] = auth_header if auth_header
46
+ headers["X-Sentry-Auth"] = auth_header if auth_header
48
47
 
49
- response = conn.start do |http|
50
- request = ::Net::HTTP::Post.new(endpoint, headers)
51
- request.body = data
52
- http.request(request)
53
- end
48
+ response = do_request(endpoint, headers, data)
54
49
 
55
50
  if response.code.match?(/\A2\d{2}/)
56
51
  handle_rate_limited_response(response) if has_rate_limited_header?(response)
@@ -60,7 +55,7 @@ module Sentry
60
55
  else
61
56
  error_info = "the server responded with status #{response.code}"
62
57
  error_info += "\nbody: #{response.body}"
63
- error_info += " Error in headers is: #{response['x-sentry-error']}" if response['x-sentry-error']
58
+ error_info += " Error in headers is: #{response['x-sentry-error']}" if response["x-sentry-error"]
64
59
 
65
60
  raise Sentry::ExternalError, error_info
66
61
  end
@@ -78,13 +73,13 @@ module Sentry
78
73
 
79
74
  now = Sentry.utc_now.to_i
80
75
  fields = {
81
- 'sentry_version' => PROTOCOL_VERSION,
82
- 'sentry_client' => USER_AGENT,
83
- 'sentry_timestamp' => now,
84
- 'sentry_key' => @dsn.public_key
76
+ "sentry_version" => PROTOCOL_VERSION,
77
+ "sentry_client" => USER_AGENT,
78
+ "sentry_timestamp" => now,
79
+ "sentry_key" => @dsn.public_key
85
80
  }
86
- fields['sentry_secret'] = @dsn.secret_key if @dsn.secret_key
87
- 'Sentry ' + fields.map { |key, value| "#{key}=#{value}" }.join(', ')
81
+ fields["sentry_secret"] = @dsn.secret_key if @dsn.secret_key
82
+ "Sentry " + fields.map { |key, value| "#{key}=#{value}" }.join(", ")
88
83
  end
89
84
 
90
85
  def conn
@@ -112,6 +107,14 @@ module Sentry
112
107
  connection
113
108
  end
114
109
 
110
+ def do_request(endpoint, headers, body)
111
+ conn.start do |http|
112
+ request = ::Net::HTTP::Post.new(endpoint, headers)
113
+ request.body = body
114
+ http.request(request)
115
+ end
116
+ end
117
+
115
118
  private
116
119
 
117
120
  def has_rate_limited_header?(headers)