sentry-ruby 0.1.3 → 4.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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