sentry-ruby 0.1.2 → 4.0.1

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