sentry-ruby 0.1.2 → 4.0.1

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.
@@ -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
@@ -5,6 +5,7 @@ require 'securerandom'
5
5
  require 'sentry/interface'
6
6
  require 'sentry/backtrace'
7
7
  require 'sentry/utils/real_ip'
8
+ require 'sentry/utils/request_id'
8
9
 
9
10
  module Sentry
10
11
  class Event
@@ -13,21 +14,19 @@ module Sentry
13
14
  release environment server_name modules
14
15
  message user tags contexts extra
15
16
  fingerprint breadcrumbs backtrace transaction
16
- platform sdk
17
+ platform sdk type
17
18
  )
18
19
 
19
20
  attr_accessor(*ATTRIBUTES)
20
- attr_reader :id, :configuration
21
-
22
- alias event_id id
21
+ attr_reader :configuration, :request, :exception, :stacktrace
23
22
 
24
23
  def initialize(configuration:, message: nil)
25
24
  # this needs to go first because some setters rely on configuration
26
25
  @configuration = configuration
27
26
 
28
27
  # Set some simple default values
29
- @id = SecureRandom.uuid.delete("-")
30
- @timestamp = Time.now.utc
28
+ @event_id = SecureRandom.uuid.delete("-")
29
+ @timestamp = Sentry.utc_now.iso8601
31
30
  @platform = :ruby
32
31
  @sdk = Sentry.sdk_meta
33
32
 
@@ -39,9 +38,9 @@ module Sentry
39
38
  @fingerprint = []
40
39
 
41
40
  @server_name = configuration.server_name
42
- @environment = configuration.current_environment
41
+ @environment = configuration.environment
43
42
  @release = configuration.release
44
- @modules = list_gem_specs if configuration.send_modules
43
+ @modules = configuration.gem_specs if configuration.send_modules
45
44
 
46
45
  @message = message || ""
47
46
 
@@ -51,8 +50,8 @@ module Sentry
51
50
  class << self
52
51
  def get_log_message(event_hash)
53
52
  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?
53
+ message = get_message_from_exception(event_hash) if message.nil? || message.empty?
54
+ message = '<no message value>' if message.nil? || message.empty?
56
55
  message
57
56
  end
58
57
 
@@ -68,7 +67,7 @@ module Sentry
68
67
  end
69
68
 
70
69
  def timestamp=(time)
71
- @timestamp = time.is_a?(Time) ? time.strftime('%Y-%m-%dT%H:%M:%S') : time
70
+ @timestamp = time.is_a?(Time) ? time.to_f : time
72
71
  end
73
72
 
74
73
  def level=(new_level) # needed to meet the Sentry spec
@@ -76,7 +75,7 @@ module Sentry
76
75
  end
77
76
 
78
77
  def rack_env=(env)
79
- unless @request || env.empty?
78
+ unless request || env.empty?
80
79
  @request = Sentry::RequestInterface.new.tap do |int|
81
80
  int.from_rack(env)
82
81
  end
@@ -84,18 +83,22 @@ module Sentry
84
83
  if configuration.send_default_pii && ip = calculate_real_ip_from_rack(env.dup)
85
84
  user[:ip_address] = ip
86
85
  end
86
+ if request_id = Utils::RequestId.read_from(env)
87
+ tags[:request_id] = request_id
88
+ end
87
89
  end
88
90
  end
89
91
 
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
92
+ def type
93
+ "event"
94
+ end
94
95
 
96
+ def to_hash
97
+ data = serialize_attributes
95
98
  data[:breadcrumbs] = breadcrumbs.to_hash if breadcrumbs
96
- data[:stacktrace] = @stacktrace.to_hash if @stacktrace
97
- data[:request] = @request.to_hash if @request
98
- data[:exception] = @exception.to_hash if @exception
99
+ data[:stacktrace] = stacktrace.to_hash if stacktrace
100
+ data[:request] = request.to_hash if request
101
+ data[:exception] = exception.to_hash if exception
99
102
 
100
103
  data
101
104
  end
@@ -141,9 +144,9 @@ module Sentry
141
144
  frame.in_app = line.in_app
142
145
  frame.module = line.module_name if line.module_name
143
146
 
144
- if configuration[:context_lines] && frame.abs_path
147
+ if configuration.context_lines && frame.abs_path
145
148
  frame.pre_context, frame.context_line, frame.post_context = \
146
- configuration.linecache.get_file_context(frame.abs_path, frame.lineno, configuration[:context_lines])
149
+ configuration.linecache.get_file_context(frame.abs_path, frame.lineno, configuration.context_lines)
147
150
  end
148
151
 
149
152
  memo << frame if frame.filename
@@ -152,6 +155,14 @@ module Sentry
152
155
 
153
156
  private
154
157
 
158
+ def serialize_attributes
159
+ self.class::ATTRIBUTES.each_with_object({}) do |att, memo|
160
+ if value = public_send(att)
161
+ memo[att] = value
162
+ end
163
+ end
164
+ end
165
+
155
166
  # When behind a proxy (or if the user is using a proxy), we can't use
156
167
  # REMOTE_ADDR to determine the Event IP, and must use other headers instead.
157
168
  def calculate_real_ip_from_rack(env)
@@ -162,10 +173,5 @@ module Sentry
162
173
  :forwarded_for => env["HTTP_X_FORWARDED_FOR"]
163
174
  ).calculate_ip
164
175
  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
176
  end
171
177
  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
 
@@ -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
@@ -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) 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
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
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