sentry-ruby 0.1.1 → 4.0.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
 
@@ -39,15 +39,6 @@ module Sentry
39
39
 
40
40
  private
41
41
 
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
42
  # See Sentry server default limits at
52
43
  # https://github.com/getsentry/sentry/blob/master/src/sentry/conf/server.py
53
44
  def read_data_from(request)
@@ -67,7 +58,7 @@ module Sentry
67
58
  begin
68
59
  key = key.to_s # rack env can contain symbols
69
60
  value = value.to_s
70
- next memo['X-Request-Id'] ||= read_request_id_from(env_hash) if REQUEST_ID_HEADERS.include?(key)
61
+ next memo['X-Request-Id'] ||= Utils::RequestId.read_from(env_hash) if Utils::RequestId::REQUEST_ID_HEADERS.include?(key)
71
62
  next unless key.upcase == key # Non-upper case stuff isn't either
72
63
 
73
64
  # Rack adds in an incorrect HTTP_VERSION key, which causes downstream
@@ -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
 
@@ -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
32
  event.rack_env = 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
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,155 @@
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
+ self
49
+ end
50
+
51
+ def to_sentry_trace
52
+ sampled_flag = ""
53
+ sampled_flag = @sampled ? 1 : 0 unless @sampled.nil?
54
+
55
+ "#{@trace_id}-#{@span_id}-#{sampled_flag}"
56
+ end
57
+
58
+ def to_hash
59
+ {
60
+ trace_id: @trace_id,
61
+ span_id: @span_id,
62
+ parent_span_id: @parent_span_id,
63
+ start_timestamp: @start_timestamp,
64
+ timestamp: @timestamp,
65
+ description: @description,
66
+ op: @op,
67
+ status: @status,
68
+ tags: @tags,
69
+ data: @data
70
+ }
71
+ end
72
+
73
+ def get_trace_context
74
+ {
75
+ trace_id: @trace_id,
76
+ span_id: @span_id,
77
+ description: @description,
78
+ op: @op,
79
+ status: @status
80
+ }
81
+ end
82
+
83
+ def start_child(**options)
84
+ options = options.dup.merge(trace_id: @trace_id, parent_span_id: @span_id, sampled: @sampled)
85
+ child_span = Span.new(options)
86
+ child_span.span_recorder = @span_recorder
87
+
88
+ if @span_recorder && @sampled
89
+ @span_recorder.add(child_span)
90
+ end
91
+
92
+ child_span
93
+ end
94
+
95
+ def with_child_span(**options, &block)
96
+ child_span = start_child(**options)
97
+
98
+ yield(child_span)
99
+
100
+ child_span.finish
101
+ end
102
+
103
+ def set_op(op)
104
+ @op = op
105
+ end
106
+
107
+ def set_description(description)
108
+ @description = description
109
+ end
110
+
111
+ def set_status(status)
112
+ @status = status
113
+ end
114
+
115
+ def set_timestamp(timestamp)
116
+ @timestamp = timestamp
117
+ end
118
+
119
+ def set_http_status(status_code)
120
+ status_code = status_code.to_i
121
+ set_data("status_code", status_code)
122
+
123
+ status =
124
+ if status_code >= 200 && status_code < 299
125
+ "ok"
126
+ else
127
+ STATUS_MAP[status_code]
128
+ end
129
+ set_status(status)
130
+ end
131
+
132
+ def set_data(key, value)
133
+ @data[key] = value
134
+ end
135
+
136
+ def set_tag(key, value)
137
+ @tags[key] = value
138
+ end
139
+
140
+ class SpanRecorder
141
+ attr_reader :max_length, :spans
142
+
143
+ def initialize(max_length)
144
+ @max_length = max_length
145
+ @spans = []
146
+ end
147
+
148
+ def add(span)
149
+ if @spans.count < @max_length
150
+ @spans << span
151
+ end
152
+ end
153
+ end
154
+ end
155
+ 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