sentry-ruby 0.1.3 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 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