sentry-ruby 0.1.3 → 0.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: b16bf44cdc5e7689122b6727e08f680c7cfb98be5bebba286fdfc8a9e9f2cb81
4
- data.tar.gz: 55a4793366f3e1a1dba8db0bec236d5c7bed5b2f1cb838244faaf629f4a1edb4
3
+ metadata.gz: f8f89cd1b0354cede1b5aab23b0b8652fed01bcebc3af7f0cb45a13d0f1d99ba
4
+ data.tar.gz: 38fa765f4acb958b8170a31017028a45cce342667bd8dee7dbff07c4e097ffc5
5
5
  SHA512:
6
- metadata.gz: 99da84ece5e4ee5c459fa93284edfa2154c4bb0409278bb763ff98c2ceeddb57821f173c8fe3d1cf3a1cfa48d9d168e676f8cc666117604d7b367c5f8f352f4c
7
- data.tar.gz: aed7ff1ecf47362e8f8be166cc173b0b5ef62dacd17dc21d7d10e6c8240193024b934d1b510cbb72ccd39837c24b79421b568577ad3e3921adbb4b050395a976
6
+ metadata.gz: be0a56af7629bf528be987f62bcb0a4fd12430d75b7541acc6d946314306ab4968c214eb7a533695dcd8aa3b3f434225f8dffceecee4bc39a8b5bd45e168e3d1
7
+ data.tar.gz: 383d6e88932c6ff3f6e79ca3c35c05d7a23151dd85db057e45dc8577f88673e8195659df3a0c01d71b1e67a61e7f1008eb36ab294047fdeea5cedf2a7d02f92c
@@ -1,5 +1,10 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.2.0
4
+
5
+ - Multiple fixes and refactorings
6
+ - Tracing support
7
+
3
8
  ## 0.1.3
4
9
 
5
10
  Fix require reference
data/Gemfile CHANGED
@@ -9,3 +9,8 @@ gem "codecov"
9
9
 
10
10
  gem "pry"
11
11
  gem "rack"
12
+
13
+ gem "benchmark-ips"
14
+ gem "benchmark_driver"
15
+ gem "benchmark-ipsa"
16
+ gem "benchmark-memory"
data/Rakefile CHANGED
@@ -1,6 +1,8 @@
1
1
  require "bundler/gem_tasks"
2
2
  require "rspec/core/rake_task"
3
3
 
4
- RSpec::Core::RakeTask.new(:spec)
4
+ RSpec::Core::RakeTask.new(:spec).tap do |task|
5
+ task.rspec_opts = "--order rand"
6
+ end
5
7
 
6
8
  task :default => :spec
@@ -3,6 +3,9 @@ require "sentry/core_ext/object/deep_dup"
3
3
  require "sentry/configuration"
4
4
  require "sentry/logger"
5
5
  require "sentry/event"
6
+ require "sentry/transaction_event"
7
+ require "sentry/span"
8
+ require "sentry/transaction"
6
9
  require "sentry/hub"
7
10
  require "sentry/rack"
8
11
 
@@ -20,6 +23,10 @@ module Sentry
20
23
  META
21
24
  end
22
25
 
26
+ def self.utc_now
27
+ Time.now.utc
28
+ end
29
+
23
30
  class << self
24
31
  def init(&block)
25
32
  config = Configuration.new
@@ -92,6 +99,10 @@ module Sentry
92
99
  get_current_hub.capture_message(message, **options, &block)
93
100
  end
94
101
 
102
+ def start_transaction(**options)
103
+ get_current_hub.start_transaction(**options)
104
+ end
105
+
95
106
  def last_event_id
96
107
  get_current_hub.last_event_id
97
108
  end
@@ -0,0 +1,14 @@
1
+ module Sentry
2
+ class BenchmarkTransport < Transport
3
+ attr_accessor :events
4
+
5
+ def initialize(*)
6
+ super
7
+ @events = []
8
+ end
9
+
10
+ def send_event(event)
11
+ @events << encode(event.to_hash)
12
+ end
13
+ end
14
+ end
@@ -7,7 +7,7 @@ module Sentry
7
7
  @data = {}
8
8
  @level = nil
9
9
  @message = nil
10
- @timestamp = Time.now.to_i
10
+ @timestamp = Sentry.utc_now.to_i
11
11
  @type = nil
12
12
  end
13
13
 
@@ -83,20 +83,6 @@ module Sentry
83
83
  Sentry.breadcrumbs
84
84
  end
85
85
  end
86
- module OldBreadcrumbsSentryLogger
87
- def self.included(base)
88
- base.class_eval do
89
- include Sentry::Breadcrumbs::SentryLogger
90
- alias_method :add_without_sentry, :add
91
- alias_method :add, :add_with_sentry
92
- end
93
- end
94
-
95
- def add_with_sentry(*args)
96
- add_breadcrumb(*args)
97
- add_without_sentry(*args)
98
- end
99
- end
100
86
  end
101
87
  end
102
88
 
@@ -51,6 +51,18 @@ module Sentry
51
51
  Event.new(configuration: configuration, message: message)
52
52
  end
53
53
 
54
+ def event_from_transaction(transaction)
55
+ TransactionEvent.new(configuration: configuration).tap do |event|
56
+ event.transaction = transaction.name
57
+ event.contexts.merge!(trace: transaction.get_trace_context)
58
+ event.timestamp = transaction.timestamp
59
+ event.start_timestamp = transaction.start_timestamp
60
+
61
+ finished_spans = transaction.span_recorder.spans.select { |span| span.timestamp && span != transaction }
62
+ event.spans = finished_spans.map(&:to_hash)
63
+ end
64
+ end
65
+
54
66
  def send_event(event)
55
67
  return false unless configuration.sending_allowed?(event)
56
68
 
@@ -15,6 +15,28 @@ module Sentry
15
15
  attr_reader :async
16
16
  alias async? async
17
17
 
18
+ # a proc/lambda that takes an array of stack traces
19
+ # it'll be used to silence (reduce) backtrace of the exception
20
+ #
21
+ # for example:
22
+ #
23
+ # ```ruby
24
+ # Sentry.configuration.backtrace_cleanup_callback = lambda do |backtrace|
25
+ # Rails.backtrace_cleaner.clean(backtrace)
26
+ # end
27
+ # ```
28
+ #
29
+ attr_accessor :backtrace_cleanup_callback
30
+
31
+ # Optional Proc, called before sending an event to the server/
32
+ # E.g.: lambda { |event| event }
33
+ # E.g.: lambda { |event| nil }
34
+ # E.g.: lambda { |event|
35
+ # event[:message] = 'a'
36
+ # event
37
+ # }
38
+ attr_reader :before_send
39
+
18
40
  # An array of breadcrumbs loggers to be used. Available options are:
19
41
  # - :sentry_logger
20
42
  # - :active_support_logger
@@ -26,6 +48,9 @@ module Sentry
26
48
  # RACK_ENV by default.
27
49
  attr_reader :current_environment
28
50
 
51
+ # the dsn value, whether it's set via `config.dsn=` or `ENV["SENTRY_DSN"]`
52
+ attr_reader :dsn
53
+
29
54
  # Whitelist of environments that will send notifications to Sentry. Array of Strings.
30
55
  attr_accessor :environments
31
56
 
@@ -64,22 +89,14 @@ module Sentry
64
89
  # any events, and a value of 1.0 will send 100% of events.
65
90
  attr_accessor :sample_rate
66
91
 
67
- # a proc/lambda that takes an array of stack traces
68
- # it'll be used to silence (reduce) backtrace of the exception
69
- #
70
- # for example:
71
- #
72
- # ```ruby
73
- # Sentry.configuration.backtrace_cleanup_callback = lambda do |backtrace|
74
- # Rails.backtrace_cleaner.clean(backtrace)
75
- # end
76
- # ```
77
- #
78
- attr_accessor :backtrace_cleanup_callback
79
-
80
92
  # Include module versions in reports - boolean.
81
93
  attr_accessor :send_modules
82
94
 
95
+ # When send_default_pii's value is false (default), sensitive information like
96
+ # - user ip
97
+ # - user cookie
98
+ # - request body
99
+ # will not be sent to Sentry.
83
100
  attr_accessor :send_default_pii
84
101
 
85
102
  attr_accessor :server_name
@@ -90,25 +107,24 @@ module Sentry
90
107
  # e.g. lambda { |exc_or_msg| exc_or_msg.some_attr == false }
91
108
  attr_reader :should_capture
92
109
 
93
- # Silences ready message when true.
94
- attr_accessor :silence_ready
95
-
110
+ # Return a Transport::Configuration object for transport-related configurations.
96
111
  attr_reader :transport
97
112
 
98
- # Optional Proc, called before sending an event to the server/
99
- # E.g.: lambda { |event| event }
100
- # E.g.: lambda { |event| nil }
101
- # E.g.: lambda { |event|
102
- # event[:message] = 'a'
103
- # event
104
- # }
105
- attr_reader :before_send
113
+ # Take a float between 0.0 and 1.0 as the sample rate for tracing events (transactions).
114
+ attr_accessor :traces_sample_rate
106
115
 
107
- # Errors object - an Array that contains error messages. See #
108
- attr_reader :errors
116
+ # Take a Proc that controls the sample rate for every tracing event, e.g.
117
+ # ```
118
+ # lambda do |tracing_context|
119
+ # # tracing_context[:transaction_context] contains the information about the transaction
120
+ # # tracing_context[:parent_sampled] contains the transaction's parent's sample decision
121
+ # true # return value can be a boolean or a float between 0.0 and 1.0
122
+ # end
123
+ # ```
124
+ attr_accessor :traces_sampler
109
125
 
110
- # the dsn value, whether it's set via `config.dsn=` or `ENV["SENTRY_DSN"]`
111
- attr_reader :dsn
126
+ # these are not config options
127
+ attr_reader :errors, :gem_specs
112
128
 
113
129
  # Most of these errors generate 4XX responses. In general, Sentry clients
114
130
  # only automatically report 5xx responses.
@@ -155,9 +171,11 @@ module Sentry
155
171
  self.server_name = server_name_from_env
156
172
  self.should_capture = false
157
173
 
158
- @transport = Transport::Configuration.new
159
174
  self.before_send = false
160
175
  self.rack_env_whitelist = RACK_ENV_WHITELIST_DEFAULT
176
+
177
+ @transport = Transport::Configuration.new
178
+ @gem_specs = Hash[Gem::Specification.map { |spec| [spec.name, spec.version.to_s] }] if Gem::Specification.respond_to?(:map)
161
179
  post_initialization_callback
162
180
  end
163
181
 
@@ -211,13 +229,6 @@ module Sentry
211
229
  @before_send = value
212
230
  end
213
231
 
214
- # Allows config options to be read like a hash
215
- #
216
- # @param [Symbol] option Key for a given attribute
217
- def [](option)
218
- public_send(option)
219
- end
220
-
221
232
  def current_environment=(environment)
222
233
  @current_environment = environment.to_s
223
234
  end
@@ -234,8 +245,8 @@ module Sentry
234
245
  alias sending_allowed? capture_allowed?
235
246
 
236
247
  def error_messages
237
- @errors = [errors[0]] + errors[1..-1].map(&:downcase) # fix case of all but first
238
- errors.join(", ")
248
+ @errors = [@errors[0]] + @errors[1..-1].map(&:downcase) # fix case of all but first
249
+ @errors.join(", ")
239
250
  end
240
251
 
241
252
  def project_root=(root_dir)
@@ -259,6 +270,10 @@ module Sentry
259
270
  environments.empty? || environments.include?(current_environment)
260
271
  end
261
272
 
273
+ def tracing_enabled?
274
+ !!((@traces_sample_rate && @traces_sample_rate > 0.0) || @traces_sampler)
275
+ end
276
+
262
277
  private
263
278
 
264
279
  def detect_project_root
@@ -279,13 +294,17 @@ module Sentry
279
294
  end
280
295
 
281
296
  def excluded_exception?(incoming_exception)
282
- excluded_exceptions.any? do |excluded_exception|
283
- matches_exception?(get_exception_class(excluded_exception), incoming_exception)
297
+ excluded_exception_classes.any? do |excluded_exception|
298
+ matches_exception?(excluded_exception, incoming_exception)
284
299
  end
285
300
  end
286
301
 
302
+ def excluded_exception_classes
303
+ @excluded_exception_classes ||= excluded_exceptions.map { |e| get_exception_class(e) }
304
+ end
305
+
287
306
  def get_exception_class(x)
288
- x.is_a?(Module) ? x : qualified_const_get(x)
307
+ x.is_a?(Module) ? x : safe_const_get(x)
289
308
  end
290
309
 
291
310
  def matches_exception?(excluded_exception_class, incoming_exception)
@@ -296,14 +315,9 @@ module Sentry
296
315
  end
297
316
  end
298
317
 
299
- # In Ruby <2.0 const_get can't lookup "SomeModule::SomeClass" in one go
300
- def qualified_const_get(x)
301
- x = x.to_s
302
- if !x.match(/::/)
303
- Object.const_get(x)
304
- else
305
- x.split(MODULE_SEPARATOR).reject(&:empty?).inject(Object) { |a, e| a.const_get(e) }
306
- end
318
+ def safe_const_get(x)
319
+ x = x.to_s unless x.is_a?(String)
320
+ Object.const_get(x)
307
321
  rescue NameError # There's no way to safely ask if a constant exist for an unknown string
308
322
  nil
309
323
  end
@@ -2,7 +2,10 @@ require "uri"
2
2
 
3
3
  module Sentry
4
4
  class DSN
5
- attr_reader :scheme, :project_id, :public_key, :secret_key, :host, :port, :path
5
+ PORT_MAP = { 'http' => 80, 'https' => 443 }.freeze
6
+ REQUIRED_ATTRIBUTES = %w(host path public_key project_id).freeze
7
+
8
+ attr_reader :scheme, :secret_key, :port, *REQUIRED_ATTRIBUTES
6
9
 
7
10
  def initialize(dsn_string)
8
11
  @raw_value = dsn_string
@@ -24,7 +27,7 @@ module Sentry
24
27
  end
25
28
 
26
29
  def valid?
27
- %w(host path public_key project_id).all? { |k| public_send(k) }
30
+ REQUIRED_ATTRIBUTES.all? { |k| public_send(k) }
28
31
  end
29
32
 
30
33
  def to_s
@@ -33,7 +36,7 @@ module Sentry
33
36
 
34
37
  def server
35
38
  server = "#{scheme}://#{host}"
36
- server += ":#{port}" unless port == { 'http' => 80, 'https' => 443 }[scheme]
39
+ server += ":#{port}" unless port == PORT_MAP[scheme]
37
40
  server += path
38
41
  server
39
42
  end
@@ -13,21 +13,19 @@ module Sentry
13
13
  release environment server_name modules
14
14
  message user tags contexts extra
15
15
  fingerprint breadcrumbs backtrace transaction
16
- platform sdk
16
+ platform sdk type
17
17
  )
18
18
 
19
19
  attr_accessor(*ATTRIBUTES)
20
- attr_reader :id, :configuration
21
-
22
- alias event_id id
20
+ attr_reader :configuration
23
21
 
24
22
  def initialize(configuration:, message: nil)
25
23
  # this needs to go first because some setters rely on configuration
26
24
  @configuration = configuration
27
25
 
28
26
  # Set some simple default values
29
- @id = SecureRandom.uuid.delete("-")
30
- @timestamp = Time.now.utc
27
+ @event_id = SecureRandom.uuid.delete("-")
28
+ @timestamp = Sentry.utc_now.iso8601
31
29
  @platform = :ruby
32
30
  @sdk = Sentry.sdk_meta
33
31
 
@@ -41,7 +39,7 @@ module Sentry
41
39
  @server_name = configuration.server_name
42
40
  @environment = configuration.current_environment
43
41
  @release = configuration.release
44
- @modules = list_gem_specs if configuration.send_modules
42
+ @modules = configuration.gem_specs if configuration.send_modules
45
43
 
46
44
  @message = message || ""
47
45
 
@@ -51,8 +49,8 @@ module Sentry
51
49
  class << self
52
50
  def get_log_message(event_hash)
53
51
  message = event_hash[:message] || event_hash['message']
54
- message = get_message_from_exception(event_hash) if message.empty?
55
- message = '<no message value>' if message.empty?
52
+ message = get_message_from_exception(event_hash) if message.nil? || message.empty?
53
+ message = '<no message value>' if message.nil? || message.empty?
56
54
  message
57
55
  end
58
56
 
@@ -68,7 +66,7 @@ module Sentry
68
66
  end
69
67
 
70
68
  def timestamp=(time)
71
- @timestamp = time.is_a?(Time) ? time.strftime('%Y-%m-%dT%H:%M:%S') : time
69
+ @timestamp = time.is_a?(Time) ? time.to_f : time
72
70
  end
73
71
 
74
72
  def level=(new_level) # needed to meet the Sentry spec
@@ -87,11 +85,12 @@ module Sentry
87
85
  end
88
86
  end
89
87
 
90
- def to_hash
91
- data = ATTRIBUTES.each_with_object({}) do |att, memo|
92
- memo[att] = public_send(att) if public_send(att)
93
- end
88
+ def type
89
+ "event"
90
+ end
94
91
 
92
+ def to_hash
93
+ data = serialize_attributes
95
94
  data[:breadcrumbs] = breadcrumbs.to_hash if breadcrumbs
96
95
  data[:stacktrace] = @stacktrace.to_hash if @stacktrace
97
96
  data[:request] = @request.to_hash if @request
@@ -141,9 +140,9 @@ module Sentry
141
140
  frame.in_app = line.in_app
142
141
  frame.module = line.module_name if line.module_name
143
142
 
144
- if configuration[:context_lines] && frame.abs_path
143
+ if configuration.context_lines && frame.abs_path
145
144
  frame.pre_context, frame.context_line, frame.post_context = \
146
- configuration.linecache.get_file_context(frame.abs_path, frame.lineno, configuration[:context_lines])
145
+ configuration.linecache.get_file_context(frame.abs_path, frame.lineno, configuration.context_lines)
147
146
  end
148
147
 
149
148
  memo << frame if frame.filename
@@ -152,6 +151,14 @@ module Sentry
152
151
 
153
152
  private
154
153
 
154
+ def serialize_attributes
155
+ self.class::ATTRIBUTES.each_with_object({}) do |att, memo|
156
+ if value = public_send(att)
157
+ memo[att] = value
158
+ end
159
+ end
160
+ end
161
+
155
162
  # When behind a proxy (or if the user is using a proxy), we can't use
156
163
  # REMOTE_ADDR to determine the Event IP, and must use other headers instead.
157
164
  def calculate_real_ip_from_rack(env)
@@ -162,10 +169,5 @@ module Sentry
162
169
  :forwarded_for => env["HTTP_X_FORWARDED_FOR"]
163
170
  ).calculate_ip
164
171
  end
165
-
166
- def list_gem_specs
167
- # Older versions of Rubygems don't support iterating over all specs
168
- Hash[Gem::Specification.map { |spec| [spec.name, spec.version.to_s] }] if Gem::Specification.respond_to?(:map)
169
- end
170
172
  end
171
173
  end
@@ -67,6 +67,12 @@ module Sentry
67
67
  @stack.pop
68
68
  end
69
69
 
70
+ def start_transaction(transaction: nil, **options)
71
+ transaction ||= Transaction.new(**options)
72
+ transaction.set_initial_sample_desicion
73
+ transaction
74
+ end
75
+
70
76
  def capture_exception(exception, **options, &block)
71
77
  return unless current_client
72
78
 
@@ -99,7 +105,7 @@ module Sentry
99
105
 
100
106
  event = current_client.capture_event(event, scope)
101
107
 
102
- @last_event_id = event.id
108
+ @last_event_id = event.event_id
103
109
  event
104
110
  end
105
111
 
@@ -2,3 +2,4 @@ require 'time'
2
2
  require 'rack'
3
3
 
4
4
  require 'sentry/rack/capture_exception'
5
+ require 'sentry/rack/tracing'
@@ -0,0 +1,39 @@
1
+ module Sentry
2
+ module Rack
3
+ class Tracing
4
+ def initialize(app)
5
+ @app = app
6
+ end
7
+
8
+ def call(env)
9
+ Sentry.clone_hub_to_current_thread unless Sentry.get_current_hub
10
+
11
+ if Sentry.configuration.traces_sample_rate.to_f == 0.0
12
+ return @app.call(env)
13
+ end
14
+
15
+ Sentry.with_scope do |scope|
16
+ scope.clear_breadcrumbs
17
+ scope.set_transaction_name(env["PATH_INFO"]) if env["PATH_INFO"]
18
+ span = Sentry.start_transaction(name: scope.transaction_name, op: "rack.request")
19
+ scope.set_span(span)
20
+
21
+ begin
22
+ response = @app.call(env)
23
+ rescue
24
+ finish_span(span, 500)
25
+ raise
26
+ end
27
+
28
+ finish_span(span, response[0])
29
+ response
30
+ end
31
+ end
32
+
33
+ def finish_span(span, status_code)
34
+ span.set_http_status(status_code)
35
+ span.finish
36
+ end
37
+ end
38
+ end
39
+ end
@@ -3,7 +3,7 @@ require "etc"
3
3
 
4
4
  module Sentry
5
5
  class Scope
6
- ATTRIBUTES = [:transaction_names, :contexts, :extra, :tags, :user, :level, :breadcrumbs, :fingerprint, :event_processors, :rack_env]
6
+ ATTRIBUTES = [:transaction_names, :contexts, :extra, :tags, :user, :level, :breadcrumbs, :fingerprint, :event_processors, :rack_env, :span]
7
7
 
8
8
  attr_reader(*ATTRIBUTES)
9
9
 
@@ -20,6 +20,11 @@ module Sentry
20
20
  event.user = user.merge(event.user)
21
21
  event.extra = extra.merge(event.extra)
22
22
  event.contexts = contexts.merge(event.contexts)
23
+
24
+ if span
25
+ event.contexts[:trace] = span.get_trace_context
26
+ end
27
+
23
28
  event.fingerprint = fingerprint
24
29
  event.level ||= level
25
30
  event.transaction = transaction_names.last
@@ -52,6 +57,7 @@ module Sentry
52
57
  copy.user = user.deep_dup
53
58
  copy.transaction_names = transaction_names.deep_dup
54
59
  copy.fingerprint = fingerprint.deep_dup
60
+ copy.span = span
55
61
  copy
56
62
  end
57
63
 
@@ -63,6 +69,7 @@ module Sentry
63
69
  self.user = scope.user
64
70
  self.transaction_names = scope.transaction_names
65
71
  self.fingerprint = scope.fingerprint
72
+ self.span = scope.span
66
73
  end
67
74
 
68
75
  def update_from_options(
@@ -86,6 +93,11 @@ module Sentry
86
93
  @rack_env = env
87
94
  end
88
95
 
96
+ def set_span(span)
97
+ check_argument_type!(span, Span)
98
+ @span = span
99
+ end
100
+
89
101
  def set_user(user_hash)
90
102
  check_argument_type!(user_hash, Hash)
91
103
  @user = user_hash
@@ -130,6 +142,15 @@ module Sentry
130
142
  @transaction_names.last
131
143
  end
132
144
 
145
+ def get_transaction
146
+ # transaction will always be the first in the span_recorder
147
+ span.span_recorder.spans.first if span
148
+ end
149
+
150
+ def get_span
151
+ span
152
+ end
153
+
133
154
  def set_fingerprint(fingerprint)
134
155
  check_argument_type!(fingerprint, Array)
135
156
 
@@ -164,6 +185,7 @@ module Sentry
164
185
  @transaction_names = []
165
186
  @event_processors = []
166
187
  @rack_env = {}
188
+ @span = nil
167
189
  end
168
190
 
169
191
  class << self
@@ -0,0 +1,146 @@
1
+ # frozen_string_literal: true
2
+ require "securerandom"
3
+
4
+ module Sentry
5
+ class Span
6
+ STATUS_MAP = {
7
+ 400 => "invalid_argument",
8
+ 401 => "unauthenticated",
9
+ 403 => "permission_denied",
10
+ 404 => "not_found",
11
+ 409 => "already_exists",
12
+ 429 => "resource_exhausted",
13
+ 499 => "cancelled",
14
+ 500 => "internal_error",
15
+ 501 => "unimplemented",
16
+ 503 => "unavailable",
17
+ 504 => "deadline_exceeded"
18
+ }
19
+
20
+
21
+ attr_reader :trace_id, :span_id, :parent_span_id, :sampled, :start_timestamp, :timestamp, :description, :op, :status, :tags, :data
22
+ attr_accessor :span_recorder
23
+
24
+ def initialize(description: nil, op: nil, status: nil, trace_id: nil, parent_span_id: nil, sampled: nil, start_timestamp: nil, timestamp: nil)
25
+ @trace_id = trace_id || SecureRandom.uuid.delete("-")
26
+ @span_id = SecureRandom.hex(8)
27
+ @parent_span_id = parent_span_id
28
+ @sampled = sampled
29
+ @start_timestamp = start_timestamp || Sentry.utc_now.to_f
30
+ @timestamp = timestamp
31
+ @description = description
32
+ @op = op
33
+ @status = status
34
+ @data = {}
35
+ @tags = {}
36
+ end
37
+
38
+ def set_span_recorder
39
+ @span_recorder = SpanRecorder.new(1000)
40
+ @span_recorder.add(self)
41
+ end
42
+
43
+ def finish
44
+ # already finished
45
+ return if @timestamp
46
+
47
+ @timestamp = Sentry.utc_now.to_f
48
+ end
49
+
50
+ def to_sentry_trace
51
+ sampled_flag = ""
52
+ sampled_flag = @sampled ? 1 : 0 unless @sampled.nil?
53
+
54
+ "#{@trace_id}-#{@span_id}-#{sampled_flag}"
55
+ end
56
+
57
+ def to_hash
58
+ {
59
+ trace_id: @trace_id,
60
+ span_id: @span_id,
61
+ parent_span_id: @parent_span_id,
62
+ start_timestamp: @start_timestamp,
63
+ timestamp: @timestamp,
64
+ description: @description,
65
+ op: @op,
66
+ status: @status,
67
+ tags: @tags,
68
+ data: @data
69
+ }
70
+ end
71
+
72
+ def get_trace_context
73
+ {
74
+ trace_id: @trace_id,
75
+ span_id: @span_id,
76
+ description: @description,
77
+ op: @op,
78
+ status: @status
79
+ }
80
+ end
81
+
82
+ def start_child(**options)
83
+ options = options.dup.merge(trace_id: @trace_id, parent_span_id: @span_id, sampled: @sampled)
84
+ child_span = Span.new(options)
85
+ child_span.span_recorder = @span_recorder
86
+
87
+ if @span_recorder && @sampled
88
+ @span_recorder.add(child_span)
89
+ end
90
+
91
+ child_span
92
+ end
93
+
94
+ def set_op(op)
95
+ @op = op
96
+ end
97
+
98
+ def set_description(description)
99
+ @description = description
100
+ end
101
+
102
+ def set_status(status)
103
+ @status = status
104
+ end
105
+
106
+ def set_timestamp(timestamp)
107
+ @timestamp = timestamp
108
+ end
109
+
110
+ def set_http_status(status_code)
111
+ status_code = status_code.to_i
112
+ set_data("status_code", status_code)
113
+
114
+ status =
115
+ if status_code >= 200 && status_code < 299
116
+ "ok"
117
+ else
118
+ STATUS_MAP[status_code]
119
+ end
120
+ set_status(status)
121
+ end
122
+
123
+ def set_data(key, value)
124
+ @data[key] = value
125
+ end
126
+
127
+ def set_tag(key, value)
128
+ @tags[key] = value
129
+ end
130
+
131
+ class SpanRecorder
132
+ attr_reader :max_length, :spans
133
+
134
+ def initialize(max_length)
135
+ @max_length = max_length
136
+ @spans = []
137
+ end
138
+
139
+ def add(span)
140
+ if @spans.count < @max_length
141
+ @spans << span
142
+ end
143
+ end
144
+ end
145
+ end
146
+ end
@@ -0,0 +1,113 @@
1
+ module Sentry
2
+ class Transaction < Span
3
+ SENTRY_TRACE_REGEXP = Regexp.new(
4
+ "^[ \t]*" + # whitespace
5
+ "([0-9a-f]{32})?" + # trace_id
6
+ "-?([0-9a-f]{16})?" + # span_id
7
+ "-?([01])?" + # sampled
8
+ "[ \t]*$" # whitespace
9
+ )
10
+ UNLABELD_NAME = "<unlabeled transaction>".freeze
11
+ MESSAGE_PREFIX = "[Tracing]"
12
+
13
+ attr_reader :name, :parent_sampled
14
+
15
+ def initialize(name: nil, parent_sampled: nil, **options)
16
+ super(**options)
17
+
18
+ @name = name
19
+ @parent_sampled = parent_sampled
20
+ set_span_recorder
21
+ end
22
+
23
+ def self.from_sentry_trace(sentry_trace, **options)
24
+ return unless sentry_trace
25
+
26
+ match = SENTRY_TRACE_REGEXP.match(sentry_trace)
27
+ trace_id, parent_span_id, sampled_flag = match[1..3]
28
+
29
+ sampled = sampled_flag != "0"
30
+
31
+ new(trace_id: trace_id, parent_span_id: parent_span_id, parent_sampled: sampled, **options)
32
+ end
33
+
34
+ def to_hash
35
+ hash = super
36
+ hash.merge!(name: @name, sampled: @sampled, parent_sampled: @parent_sampled)
37
+ hash
38
+ end
39
+
40
+ def set_initial_sample_desicion(sampling_context = {})
41
+ unless Sentry.configuration.tracing_enabled?
42
+ @sampled = false
43
+ return
44
+ end
45
+
46
+ return unless @sampled.nil?
47
+
48
+ transaction_description = generate_transaction_description
49
+
50
+ logger = Sentry.configuration.logger
51
+ sample_rate = Sentry.configuration.traces_sample_rate
52
+ traces_sampler = Sentry.configuration.traces_sampler
53
+
54
+ if traces_sampler.is_a?(Proc)
55
+ sampling_context = sampling_context.merge(
56
+ parent_sampled: @parent_sampled,
57
+ transaction_context: self.to_hash
58
+ )
59
+
60
+ sample_rate = traces_sampler.call(sampling_context)
61
+ end
62
+
63
+ unless [true, false].include?(sample_rate) || (sample_rate.is_a?(Float) && sample_rate >= 0.0 && sample_rate <= 1.0)
64
+ @sampled = false
65
+ logger.warn("#{MESSAGE_PREFIX} Discarding #{transaction_description} because of invalid sample_rate: #{sample_rate}")
66
+ return
67
+ end
68
+
69
+ if sample_rate == 0.0 || sample_rate == false
70
+ @sampled = false
71
+ logger.debug("#{MESSAGE_PREFIX} Discarding #{transaction_description} because traces_sampler returned 0 or false")
72
+ return
73
+ end
74
+
75
+ if sample_rate == true
76
+ @sampled = true
77
+ else
78
+ @sampled = Random.rand < sample_rate
79
+ end
80
+
81
+ if @sampled
82
+ logger.debug("#{MESSAGE_PREFIX} Starting #{transaction_description}")
83
+ else
84
+ logger.debug(
85
+ "#{MESSAGE_PREFIX} Discarding #{transaction_description} because it's not included in the random sample (sampling rate = #{sample_rate})"
86
+ )
87
+ end
88
+ end
89
+
90
+ def finish(hub: nil)
91
+ super() # Span#finish doesn't take arguments
92
+
93
+ if @name.nil?
94
+ @name = UNLABELD_NAME
95
+ end
96
+
97
+ return unless @sampled
98
+
99
+ hub ||= Sentry.get_current_hub
100
+ event = hub.current_client.event_from_transaction(self)
101
+ hub.capture_event(event)
102
+ end
103
+
104
+ private
105
+
106
+ def generate_transaction_description
107
+ result = op.nil? ? "" : "<#{@op}> "
108
+ result += "transaction"
109
+ result += " <#{@name}>" if @name
110
+ result
111
+ end
112
+ end
113
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sentry
4
+ class TransactionEvent < Event
5
+ ATTRIBUTES = %i(
6
+ event_id level timestamp start_timestamp
7
+ release environment server_name modules
8
+ user tags contexts extra
9
+ transaction platform sdk type
10
+ )
11
+
12
+ attr_accessor(*ATTRIBUTES)
13
+ attr_accessor :spans
14
+
15
+ def start_timestamp=(time)
16
+ @start_timestamp = time.is_a?(Time) ? time.to_f : time
17
+ end
18
+
19
+ def type
20
+ "transaction"
21
+ end
22
+
23
+ def to_hash
24
+ data = super
25
+ data[:spans] = @spans.map(&:to_hash) if @spans
26
+ data
27
+ end
28
+ end
29
+ end
@@ -32,10 +32,11 @@ module Sentry
32
32
  event
33
33
  rescue => e
34
34
  failed_for_exception(e, event)
35
+ nil
35
36
  end
36
37
 
37
38
  def generate_auth_header
38
- now = Time.now.to_i.to_s
39
+ now = Sentry.utc_now.to_i
39
40
  fields = {
40
41
  'sentry_version' => PROTOCOL_VERSION,
41
42
  'sentry_client' => USER_AGENT,
@@ -48,11 +49,12 @@ module Sentry
48
49
 
49
50
  def encode(event_hash)
50
51
  event_id = event_hash[:event_id] || event_hash['event_id']
52
+ event_type = event_hash[:type] || event_hash['type']
51
53
 
52
54
  envelope = <<~ENVELOPE
53
- {"event_id":"#{event_id}","dsn":"#{configuration.dsn.to_s}","sdk":#{Sentry.sdk_meta.to_json},"sent_at":"#{DateTime.now.rfc3339}"}
54
- {"type":"event","content_type":"application/json"}
55
- #{event_hash.to_json}
55
+ {"event_id":"#{event_id}","dsn":"#{configuration.dsn.to_s}","sdk":#{Sentry.sdk_meta.to_json},"sent_at":"#{Sentry.utc_now.iso8601}"}
56
+ {"type":"#{event_type}","content_type":"application/json"}
57
+ #{JSON.generate(event_hash)}
56
58
  ENVELOPE
57
59
 
58
60
  [CONTENT_TYPE, envelope]
@@ -86,7 +88,7 @@ module Sentry
86
88
  end
87
89
 
88
90
  def log_not_sending(event)
89
- configuration.logger.warn(LOGGER_PROGNAME) { "Failed to submit event: #{Event.get_log_message(event.to_hash)}" }
91
+ configuration.logger.warn(LOGGER_PROGNAME) { "Failed to submit event. Unreported Event: #{Event.get_log_message(event.to_hash)}" }
90
92
  end
91
93
  end
92
94
  end
@@ -23,9 +23,12 @@ module Sentry
23
23
  end
24
24
  rescue Faraday::Error => e
25
25
  error_info = e.message
26
- if e.response && e.response[:headers]['x-sentry-error']
27
- error_info += " Error in headers is: #{e.response[:headers]['x-sentry-error']}"
26
+
27
+ if e.response
28
+ error_info += "\nbody: #{e.response[:body]}"
29
+ error_info += " Error in headers is: #{e.response[:headers]['x-sentry-error']}" if e.response[:headers]['x-sentry-error']
28
30
  end
31
+
29
32
  raise Sentry::Error, error_info
30
33
  end
31
34
 
@@ -9,7 +9,7 @@ module Sentry
9
9
  return true if @status == :online
10
10
 
11
11
  interval = @retry_after || [@retry_number, 6].min**2
12
- return true if Time.now - @last_check >= interval
12
+ return true if Sentry.utc_now - @last_check >= interval
13
13
 
14
14
  false
15
15
  end
@@ -17,7 +17,7 @@ module Sentry
17
17
  def failure(retry_after = nil)
18
18
  @status = :error
19
19
  @retry_number += 1
20
- @last_check = Time.now
20
+ @last_check = Sentry.utc_now
21
21
  @retry_after = retry_after
22
22
  end
23
23
 
@@ -1,3 +1,3 @@
1
1
  module Sentry
2
- VERSION = "0.1.3"
2
+ VERSION = "0.2.0"
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: sentry-ruby
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.3
4
+ version: 0.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: 2020-11-13 00:00:00.000000000 Z
11
+ date: 2020-11-30 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: faraday
@@ -46,6 +46,7 @@ files:
46
46
  - bin/setup
47
47
  - lib/sentry-ruby.rb
48
48
  - lib/sentry/backtrace.rb
49
+ - lib/sentry/benchmarks/benchmark_transport.rb
49
50
  - lib/sentry/breadcrumb.rb
50
51
  - lib/sentry/breadcrumb/sentry_logger.rb
51
52
  - lib/sentry/breadcrumb_buffer.rb
@@ -65,7 +66,11 @@ files:
65
66
  - lib/sentry/logger.rb
66
67
  - lib/sentry/rack.rb
67
68
  - lib/sentry/rack/capture_exception.rb
69
+ - lib/sentry/rack/tracing.rb
68
70
  - lib/sentry/scope.rb
71
+ - lib/sentry/span.rb
72
+ - lib/sentry/transaction.rb
73
+ - lib/sentry/transaction_event.rb
69
74
  - lib/sentry/transport.rb
70
75
  - lib/sentry/transport/configuration.rb
71
76
  - lib/sentry/transport/dummy_transport.rb