sentry-ruby 0.1.3 → 4.1.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.
@@ -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
 
@@ -74,12 +80,16 @@ module Sentry
74
80
 
75
81
  return unless event
76
82
 
83
+ options[:hint] ||= {}
84
+ options[:hint] = options[:hint].merge(exception: exception)
77
85
  capture_event(event, **options, &block)
78
86
  end
79
87
 
80
88
  def capture_message(message, **options, &block)
81
89
  return unless current_client
82
90
 
91
+ options[:hint] ||= {}
92
+ options[:hint] = options[:hint].merge(message: message)
83
93
  event = current_client.event_from_message(message)
84
94
  capture_event(event, **options, &block)
85
95
  end
@@ -87,6 +97,7 @@ module Sentry
87
97
  def capture_event(event, **options, &block)
88
98
  return unless current_client
89
99
 
100
+ hint = options.delete(:hint) || {}
90
101
  scope = current_scope.dup
91
102
 
92
103
  if block
@@ -97,9 +108,9 @@ module Sentry
97
108
  scope.update_from_options(**options)
98
109
  end
99
110
 
100
- event = current_client.capture_event(event, scope)
111
+ event = current_client.capture_event(event, scope, hint)
101
112
 
102
- @last_event_id = event.id
113
+ @last_event_id = event.event_id
103
114
  event
104
115
  end
105
116
 
@@ -1,5 +1,3 @@
1
- require 'rack'
2
-
3
1
  module Sentry
4
2
  class RequestInterface < Interface
5
3
  REQUEST_ID_HEADERS = %w(action_dispatch.request_id HTTP_X_REQUEST_ID).freeze
@@ -18,36 +16,8 @@ module Sentry
18
16
  self.cookies = nil
19
17
  end
20
18
 
21
- def from_rack(env_hash)
22
- req = ::Rack::Request.new(env_hash)
23
-
24
- if Sentry.configuration.send_default_pii
25
- self.data = read_data_from(req)
26
- self.cookies = req.cookies
27
- else
28
- # need to completely wipe out ip addresses
29
- IP_HEADERS.each { |h| env_hash.delete(h) }
30
- end
31
-
32
- self.url = req.scheme && req.url.split('?').first
33
- self.method = req.request_method
34
- self.query_string = req.query_string
35
-
36
- self.headers = format_headers_for_sentry(env_hash)
37
- self.env = format_env_for_sentry(env_hash)
38
- end
39
-
40
19
  private
41
20
 
42
- # Request ID based on ActionDispatch::RequestId
43
- def read_request_id_from(env_hash)
44
- REQUEST_ID_HEADERS.each do |key|
45
- request_id = env_hash[key]
46
- return request_id if request_id
47
- end
48
- nil
49
- end
50
-
51
21
  # See Sentry server default limits at
52
22
  # https://github.com/getsentry/sentry/blob/master/src/sentry/conf/server.py
53
23
  def read_data_from(request)
@@ -67,7 +37,7 @@ module Sentry
67
37
  begin
68
38
  key = key.to_s # rack env can contain symbols
69
39
  value = value.to_s
70
- next memo['X-Request-Id'] ||= read_request_id_from(env_hash) if REQUEST_ID_HEADERS.include?(key)
40
+ next memo['X-Request-Id'] ||= Utils::RequestId.read_from(env_hash) if Utils::RequestId::REQUEST_ID_HEADERS.include?(key)
71
41
  next unless key.upcase == key # Non-upper case stuff isn't either
72
42
 
73
43
  # Rack adds in an incorrect HTTP_VERSION key, which causes downstream
@@ -1,4 +1,4 @@
1
- require 'time'
2
1
  require 'rack'
3
2
 
4
- require 'sentry/rack/capture_exception'
3
+ require 'sentry/rack/capture_exceptions'
4
+ require 'sentry/rack/interface'
@@ -1,45 +1,54 @@
1
1
  module Sentry
2
2
  module Rack
3
- class CaptureException
3
+ class CaptureExceptions
4
4
  def initialize(app)
5
5
  @app = app
6
6
  end
7
7
 
8
8
  def call(env)
9
- # this call clones the main (global) hub
10
- # and assigns it to the current thread's Sentry#get_current_hub
11
- # it's essential for multi-thread servers (e.g. puma)
12
- Sentry.clone_hub_to_current_thread unless Sentry.get_current_hub
13
- # this call creates an isolated scope for every request
14
- # it's essential for multi-process servers (e.g. unicorn)
9
+ return @app.call(env) unless Sentry.initialized?
10
+
11
+ # make sure the current thread has a clean hub
12
+ Sentry.clone_hub_to_current_thread
13
+
15
14
  Sentry.with_scope do |scope|
16
- # there could be some breadcrumbs already stored in the top-level scope
17
- # and for request information, we don't need those breadcrumbs
18
15
  scope.clear_breadcrumbs
19
- env['sentry.client'] = Sentry.get_current_client
20
-
21
16
  scope.set_transaction_name(env["PATH_INFO"]) if env["PATH_INFO"]
22
17
  scope.set_rack_env(env)
23
18
 
19
+ span = Sentry.start_transaction(name: scope.transaction_name, op: "rack.request")
20
+ scope.set_span(span)
21
+
24
22
  begin
25
23
  response = @app.call(env)
26
24
  rescue Sentry::Error
25
+ finish_span(span, 500)
27
26
  raise # Don't capture Sentry errors
28
27
  rescue Exception => e
29
28
  Sentry.capture_exception(e)
29
+ finish_span(span, 500)
30
30
  raise
31
31
  end
32
32
 
33
33
  exception = collect_exception(env)
34
34
  Sentry.capture_exception(exception) if exception
35
35
 
36
+ finish_span(span, response[0])
37
+
36
38
  response
37
39
  end
38
40
  end
39
41
 
42
+ private
43
+
40
44
  def collect_exception(env)
41
45
  env['rack.exception'] || env['sinatra.error']
42
46
  end
47
+
48
+ def finish_span(span, status_code)
49
+ span.set_http_status(status_code)
50
+ span.finish
51
+ end
43
52
  end
44
53
  end
45
54
  end
@@ -0,0 +1,22 @@
1
+ module Sentry
2
+ class RequestInterface
3
+ def from_rack(env_hash)
4
+ req = ::Rack::Request.new(env_hash)
5
+
6
+ if Sentry.configuration.send_default_pii
7
+ self.data = read_data_from(req)
8
+ self.cookies = req.cookies
9
+ else
10
+ # need to completely wipe out ip addresses
11
+ IP_HEADERS.each { |h| env_hash.delete(h) }
12
+ end
13
+
14
+ self.url = req.scheme && req.url.split('?').first
15
+ self.method = req.request_method
16
+ self.query_string = req.query_string
17
+
18
+ self.headers = format_headers_for_sentry(env_hash)
19
+ self.env = format_env_for_sentry(env_hash)
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,17 @@
1
+ require "rake"
2
+ require "rake/task"
3
+
4
+ module Rake
5
+ class Application
6
+ alias orig_display_error_messsage display_error_message
7
+ def display_error_message(ex)
8
+ Sentry.capture_exception(ex, hint: { background: false }) do |scope|
9
+ task_name = top_level_tasks.join(' ')
10
+ scope.set_transaction_name(task_name)
11
+ scope.set_tag("rake_task", task_name)
12
+ end if Sentry.initialized?
13
+
14
+ orig_display_error_messsage(ex)
15
+ end
16
+ end
17
+ 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
 
@@ -15,20 +15,25 @@ module Sentry
15
15
  set_default_value
16
16
  end
17
17
 
18
- def apply_to_event(event)
18
+ def apply_to_event(event, hint = nil)
19
19
  event.tags = tags.merge(event.tags)
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
- event.level ||= level
29
+ event.level = level
25
30
  event.transaction = transaction_names.last
26
31
  event.breadcrumbs = breadcrumbs
27
- event.rack_env = rack_env
32
+ event.rack_env = rack_env if rack_env
28
33
 
29
34
  unless @event_processors.empty?
30
35
  @event_processors.each do |processor_block|
31
- event = processor_block.call(event)
36
+ event = processor_block.call(event, hint)
32
37
  end
33
38
  end
34
39
 
@@ -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.deep_dup
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,132 @@
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 finish
39
+ # already finished
40
+ return if @timestamp
41
+
42
+ @timestamp = Sentry.utc_now.to_f
43
+ self
44
+ end
45
+
46
+ def to_sentry_trace
47
+ sampled_flag = ""
48
+ sampled_flag = @sampled ? 1 : 0 unless @sampled.nil?
49
+
50
+ "#{@trace_id}-#{@span_id}-#{sampled_flag}"
51
+ end
52
+
53
+ def to_hash
54
+ {
55
+ trace_id: @trace_id,
56
+ span_id: @span_id,
57
+ parent_span_id: @parent_span_id,
58
+ start_timestamp: @start_timestamp,
59
+ timestamp: @timestamp,
60
+ description: @description,
61
+ op: @op,
62
+ status: @status,
63
+ tags: @tags,
64
+ data: @data
65
+ }
66
+ end
67
+
68
+ def get_trace_context
69
+ {
70
+ trace_id: @trace_id,
71
+ span_id: @span_id,
72
+ description: @description,
73
+ op: @op,
74
+ status: @status
75
+ }
76
+ end
77
+
78
+ def start_child(**options)
79
+ options = options.dup.merge(trace_id: @trace_id, parent_span_id: @span_id, sampled: @sampled)
80
+ Span.new(options)
81
+ end
82
+
83
+ def with_child_span(**options, &block)
84
+ child_span = start_child(**options)
85
+
86
+ yield(child_span)
87
+
88
+ child_span.finish
89
+ end
90
+
91
+ def deep_dup
92
+ dup
93
+ end
94
+
95
+ def set_op(op)
96
+ @op = op
97
+ end
98
+
99
+ def set_description(description)
100
+ @description = description
101
+ end
102
+
103
+ def set_status(status)
104
+ @status = status
105
+ end
106
+
107
+ def set_timestamp(timestamp)
108
+ @timestamp = timestamp
109
+ end
110
+
111
+ def set_http_status(status_code)
112
+ status_code = status_code.to_i
113
+ set_data("status_code", status_code)
114
+
115
+ status =
116
+ if status_code >= 200 && status_code < 299
117
+ "ok"
118
+ else
119
+ STATUS_MAP[status_code]
120
+ end
121
+ set_status(status)
122
+ end
123
+
124
+ def set_data(key, value)
125
+ @data[key] = value
126
+ end
127
+
128
+ def set_tag(key, value)
129
+ @tags[key] = value
130
+ end
131
+ end
132
+ end
@@ -0,0 +1,157 @@
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 set_span_recorder
24
+ @span_recorder = SpanRecorder.new(1000)
25
+ @span_recorder.add(self)
26
+ end
27
+
28
+ def self.from_sentry_trace(sentry_trace, **options)
29
+ return unless sentry_trace
30
+
31
+ match = SENTRY_TRACE_REGEXP.match(sentry_trace)
32
+ trace_id, parent_span_id, sampled_flag = match[1..3]
33
+
34
+ sampled = sampled_flag != "0"
35
+
36
+ new(trace_id: trace_id, parent_span_id: parent_span_id, parent_sampled: sampled, **options)
37
+ end
38
+
39
+ def to_hash
40
+ hash = super
41
+ hash.merge!(name: @name, sampled: @sampled, parent_sampled: @parent_sampled)
42
+ hash
43
+ end
44
+
45
+ def start_child(**options)
46
+ child_span = super
47
+ child_span.span_recorder = @span_recorder
48
+
49
+ if @sampled
50
+ @span_recorder.add(child_span)
51
+ end
52
+
53
+ child_span
54
+ end
55
+
56
+ def deep_dup
57
+ copy = super
58
+ copy.set_span_recorder
59
+
60
+ @span_recorder.spans.each do |span|
61
+ # span_recorder's first span is the current span, which should not be added to the copy's spans
62
+ next if span == self
63
+ copy.span_recorder.add(span.dup)
64
+ end
65
+
66
+ copy
67
+ end
68
+
69
+ def set_initial_sample_desicion(sampling_context = {})
70
+ unless Sentry.configuration.tracing_enabled?
71
+ @sampled = false
72
+ return
73
+ end
74
+
75
+ return unless @sampled.nil?
76
+
77
+ transaction_description = generate_transaction_description
78
+
79
+ logger = Sentry.configuration.logger
80
+ sample_rate = Sentry.configuration.traces_sample_rate
81
+ traces_sampler = Sentry.configuration.traces_sampler
82
+
83
+ if traces_sampler.is_a?(Proc)
84
+ sampling_context = sampling_context.merge(
85
+ parent_sampled: @parent_sampled,
86
+ transaction_context: self.to_hash
87
+ )
88
+
89
+ sample_rate = traces_sampler.call(sampling_context)
90
+ end
91
+
92
+ unless [true, false].include?(sample_rate) || (sample_rate.is_a?(Float) && sample_rate >= 0.0 && sample_rate <= 1.0)
93
+ @sampled = false
94
+ logger.warn("#{MESSAGE_PREFIX} Discarding #{transaction_description} because of invalid sample_rate: #{sample_rate}")
95
+ return
96
+ end
97
+
98
+ if sample_rate == 0.0 || sample_rate == false
99
+ @sampled = false
100
+ logger.debug("#{MESSAGE_PREFIX} Discarding #{transaction_description} because traces_sampler returned 0 or false")
101
+ return
102
+ end
103
+
104
+ if sample_rate == true
105
+ @sampled = true
106
+ else
107
+ @sampled = Random.rand < sample_rate
108
+ end
109
+
110
+ if @sampled
111
+ logger.debug("#{MESSAGE_PREFIX} Starting #{transaction_description}")
112
+ else
113
+ logger.debug(
114
+ "#{MESSAGE_PREFIX} Discarding #{transaction_description} because it's not included in the random sample (sampling rate = #{sample_rate})"
115
+ )
116
+ end
117
+ end
118
+
119
+ def finish(hub: nil)
120
+ super() # Span#finish doesn't take arguments
121
+
122
+ if @name.nil?
123
+ @name = UNLABELD_NAME
124
+ end
125
+
126
+ return unless @sampled
127
+
128
+ hub ||= Sentry.get_current_hub
129
+ event = hub.current_client.event_from_transaction(self)
130
+ hub.capture_event(event)
131
+ end
132
+
133
+ private
134
+
135
+ def generate_transaction_description
136
+ result = op.nil? ? "" : "<#{@op}> "
137
+ result += "transaction"
138
+ result += " <#{@name}>" if @name
139
+ result
140
+ end
141
+
142
+ class SpanRecorder
143
+ attr_reader :max_length, :spans
144
+
145
+ def initialize(max_length)
146
+ @max_length = max_length
147
+ @spans = []
148
+ end
149
+
150
+ def add(span)
151
+ if @spans.count < @max_length
152
+ @spans << span
153
+ end
154
+ end
155
+ end
156
+ end
157
+ end