sentry-ruby 6.4.1 → 6.6.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 98ee25c5aaeb49754f4d101eff2633c74f00b3b01236e7c012c1b7bb5898e276
4
- data.tar.gz: a463f19fe5fe50719398470b2926509ebd5708cb7702948207d164846c873a29
3
+ metadata.gz: 3ac6a6802b328b59cb636ac1d91b5b1ab5710e9c1427c3d910e7bd05f30b618b
4
+ data.tar.gz: a77ee9a574da2de452d962848dbfca19aef525304e1bdbf65b21cb11ec7e9e3b
5
5
  SHA512:
6
- metadata.gz: 0d108cff7d2da68566f330f02706c31bf70ca22291e5dd8575dfb1fcc76b073e14a22d2f18a814346ca38bc51cb1868ac328989de5d7b7c8bf1eab5d3a5d85b0
7
- data.tar.gz: 9e6975759fb9a06307ab201f7377822f5fa4d4f407eacb656371f083cce2ff65ba98c0b4cc418f81e608d0e8e78449ee9c53e3c26e6706a3b8b4bfe8e77c37d2
6
+ metadata.gz: c1f221cea1a011d2540c666e14823423d12fa37f3ea4ee59f3d68035ad36e33dd62fbc8f23712fee46b8da31e3bbb7c027e664927945b959197c15d10626f4e9
7
+ data.tar.gz: 95f32db537d5fa0a95b3897dcd7fc6d34dff884fe474fc9a43d0567d6a3ef6a84cb181f06c9ef60b0e1e132cb5afa28e65faf427c408c518beeb1305ddf9af1f
data/README.md CHANGED
@@ -21,6 +21,7 @@ Sentry SDK for Ruby
21
21
  | [![Gem Version](https://img.shields.io/gem/v/sentry-delayed_job?label=sentry-delayed_job)](https://rubygems.org/gems/sentry-delayed_job) | [![Build Status](https://github.com/getsentry/sentry-ruby/actions/workflows/tests.yml/badge.svg)](https://github.com/getsentry/sentry-ruby/actions/workflows/tests.yml) | [![codecov](https://codecov.io/gh/getsentry/sentry-ruby/graph/badge.svg?token=ZePzrpZFP6&component=sentry-delayed_job)](https://codecov.io/gh/getsentry/sentry-ruby) | [![API doc](https://img.shields.io/badge/API%20doc-rubydoc.info-blue)](https://www.rubydoc.info/gems/sentry-delayed_job) |
22
22
  | [![Gem Version](https://img.shields.io/gem/v/sentry-resque?label=sentry-resque)](https://rubygems.org/gems/sentry-resque) | [![Build Status](https://github.com/getsentry/sentry-ruby/actions/workflows/tests.yml/badge.svg)](https://github.com/getsentry/sentry-ruby/actions/workflows/tests.yml) | [![codecov](https://codecov.io/gh/getsentry/sentry-ruby/graph/badge.svg?token=ZePzrpZFP6&component=sentry-resque)](https://codecov.io/gh/getsentry/sentry-ruby) | [![API doc](https://img.shields.io/badge/API%20doc-rubydoc.info-blue)](https://www.rubydoc.info/gems/sentry-resque) |
23
23
  | [![Gem Version](https://img.shields.io/gem/v/sentry-opentelemetry?label=sentry-opentelemetry)](https://rubygems.org/gems/sentry-opentelemetry) | [![Build Status](https://github.com/getsentry/sentry-ruby/actions/workflows/tests.yml/badge.svg)](https://github.com/getsentry/sentry-ruby/actions/workflows/tests.yml) | [![codecov](https://codecov.io/gh/getsentry/sentry-ruby/graph/badge.svg?token=ZePzrpZFP6&component=sentry-opentelemetry)](https://codecov.io/gh/getsentry/sentry-ruby) | [![API doc](https://img.shields.io/badge/API%20doc-rubydoc.info-blue)](https://www.rubydoc.info/gems/sentry-opentelemetry) |
24
+ | [![Gem Version](https://img.shields.io/gem/v/sentry-yabeda?label=sentry-yabeda)](https://rubygems.org/gems/sentry-yabeda) | [![Build Status](https://github.com/getsentry/sentry-ruby/actions/workflows/tests.yml/badge.svg)](https://github.com/getsentry/sentry-ruby/actions/workflows/tests.yml) | [![codecov](https://codecov.io/gh/getsentry/sentry-ruby/graph/badge.svg?token=ZePzrpZFP6&component=sentry-yabeda)](https://codecov.io/gh/getsentry/sentry-ruby) | [![API doc](https://img.shields.io/badge/API%20doc-rubydoc.info-blue)](https://www.rubydoc.info/gems/sentry-yabeda) |
24
25
 
25
26
 
26
27
 
@@ -53,6 +54,7 @@ gem "sentry-sidekiq"
53
54
  gem "sentry-delayed_job"
54
55
  gem "sentry-resque"
55
56
  gem "sentry-opentelemetry"
57
+ gem "sentry-yabeda"
56
58
  ```
57
59
 
58
60
  ### Configuration
@@ -93,6 +95,7 @@ To learn more about sampling transactions, please visit the [official documentat
93
95
  - [DelayedJob](https://docs.sentry.io/platforms/ruby/guides/delayed_job/)
94
96
  - [Resque](https://docs.sentry.io/platforms/ruby/guides/resque/)
95
97
  - [OpenTelemetry](https://docs.sentry.io/platforms/ruby/performance/instrumentation/opentelemetry/)
98
+ - [Yabeda](https://docs.sentry.io/platforms/ruby/guides/yabeda/)
96
99
 
97
100
  ### Enriching Events
98
101
 
@@ -6,6 +6,7 @@ module Sentry
6
6
  # Handles backtrace parsing line by line
7
7
  class Line
8
8
  RB_EXTENSION = ".rb"
9
+ CLASS_EXTENSION = ".class"
9
10
  # regexp (optional leading X: on windows, or JRuby9000 class-prefix)
10
11
  RUBY_INPUT_FORMAT = /
11
12
  ^ \s* (?: [a-zA-Z]: | uri:classloader: )? ([^:]+ | <.*>):
@@ -37,12 +38,21 @@ module Sentry
37
38
  ruby_match = unparsed_line.match(RUBY_INPUT_FORMAT)
38
39
 
39
40
  if ruby_match
40
- _, file, number, _, module_name, method = ruby_match.to_a
41
- file.sub!(/\.class$/, RB_EXTENSION)
42
- module_name = module_name
41
+ file = ruby_match[1]
42
+ number = ruby_match[2]
43
+ module_name = ruby_match[4]
44
+ method = ruby_match[5]
45
+ if file.end_with?(CLASS_EXTENSION)
46
+ file.sub!(/\.class$/, RB_EXTENSION)
47
+ end
43
48
  else
44
49
  java_match = unparsed_line.match(JAVA_INPUT_FORMAT)
45
- _, module_name, method, file, number = java_match.to_a
50
+ if java_match
51
+ module_name = java_match[1]
52
+ method = java_match[2]
53
+ file = java_match[3]
54
+ number = java_match[4]
55
+ end
46
56
  end
47
57
  new(file, number, method, module_name, in_app_pattern)
48
58
  end
@@ -74,12 +84,9 @@ module Sentry
74
84
 
75
85
  def in_app
76
86
  return false unless in_app_pattern
87
+ return false unless file
77
88
 
78
- if file =~ in_app_pattern
79
- true
80
- else
81
- false
82
- end
89
+ file.match?(in_app_pattern)
83
90
  end
84
91
 
85
92
  # Reconstructs the line in a readable fashion
@@ -10,14 +10,16 @@ module Sentry
10
10
  # holder for an Array of Backtrace::Line instances
11
11
  attr_reader :lines
12
12
 
13
- def self.parse(backtrace, project_root, app_dirs_pattern, &backtrace_cleanup_callback)
13
+ # @deprecated project_root, in_app_pattern passed from outside
14
+ # @deprecated app_dirs_pattern, in_app_pattern passed from outside
15
+ def self.parse(backtrace, project_root, app_dirs_pattern, in_app_pattern: nil, &backtrace_cleanup_callback)
14
16
  ruby_lines = backtrace.is_a?(Array) ? backtrace : backtrace.split(/\n\s*/)
15
17
 
16
18
  ruby_lines = backtrace_cleanup_callback.call(ruby_lines) if backtrace_cleanup_callback
17
19
 
18
- in_app_pattern ||= begin
19
- Regexp.new("^(#{project_root}/)?#{app_dirs_pattern}")
20
- end
20
+ # in_app_pattern is now passed in from StacktraceBuilder, so this regex won't be triggered
21
+ # only here for backwards compat and will be deleted
22
+ in_app_pattern ||= Regexp.new("^(#{project_root}/)?#{app_dirs_pattern}")
21
23
 
22
24
  lines = ruby_lines.to_a.map do |unparsed_line|
23
25
  Line.parse(unparsed_line, in_app_pattern)
@@ -25,7 +25,7 @@ module Sentry
25
25
  # The presence of a Sentry item makes the baggage object immutable.
26
26
  #
27
27
  # @param header [String] The incoming Baggage header string.
28
- # @return [Baggage, nil]
28
+ # @return [Baggage]
29
29
  def self.from_incoming_header(header)
30
30
  items = {}
31
31
  mutable = true
@@ -371,6 +371,24 @@ module Sentry
371
371
  # @return [Proc, nil]
372
372
  attr_reader :std_lib_logger_filter
373
373
 
374
+ # An optional organization ID. The SDK will try to extract it from the DSN in most cases
375
+ # but you can provide it explicitly for self-hosted and Relay setups.
376
+ # This value is used for trace propagation and for features like strict_trace_continuation.
377
+ # @return [String, nil]
378
+ attr_reader :org_id
379
+
380
+ # If set to true, the SDK will only continue a trace if the org_id of the incoming trace found in the
381
+ # baggage header matches the org_id of the current Sentry client and only if BOTH are present.
382
+ #
383
+ # If set to false, consistency of org_id will only be enforced if both are present.
384
+ # If either are missing, the trace will be continued.
385
+ #
386
+ # The client's organization ID is extracted from the DSN or can be set with the org_id option.
387
+ # If the organization IDs do not match, the SDK will start a new trace instead of continuing the incoming one.
388
+ # This is useful to prevent traces of unknown third-party services from being continued in your application.
389
+ # @return [Boolean]
390
+ attr_accessor :strict_trace_continuation
391
+
374
392
  # these are not config options
375
393
  # @!visibility private
376
394
  attr_reader :errors, :gem_specs
@@ -443,7 +461,8 @@ module Sentry
443
461
  def callbacks
444
462
  @callbacks ||= {
445
463
  initialize: { before: [], after: [] },
446
- configured: { before: [], after: [] }
464
+ configured: { before: [], after: [] },
465
+ closed: { before: [], after: [] }
447
466
  }
448
467
  end
449
468
 
@@ -520,6 +539,8 @@ module Sentry
520
539
  self.trusted_proxies = []
521
540
  self.dsn = ENV["SENTRY_DSN"]
522
541
  self.capture_queue_time = true
542
+ self.org_id = nil
543
+ self.strict_trace_continuation = false
523
544
 
524
545
  spotlight_env = ENV["SENTRY_SPOTLIGHT"]
525
546
  spotlight_bool = Sentry::Utils::EnvHelper.env_to_bool(spotlight_env, strict: true)
@@ -673,6 +694,16 @@ module Sentry
673
694
  @profiler_class = profiler_class
674
695
  end
675
696
 
697
+ def org_id=(value)
698
+ @org_id = value&.to_s
699
+ end
700
+
701
+ # Returns the effective org ID, preferring the explicit config option over the DSN-parsed value.
702
+ # @return [String, nil]
703
+ def effective_org_id
704
+ org_id || dsn&.org_id
705
+ end
706
+
676
707
  def sending_allowed?
677
708
  spotlight || sending_to_dsn_allowed?
678
709
  end
@@ -768,6 +799,11 @@ module Sentry
768
799
  @errors.join(", ")
769
800
  end
770
801
 
802
+ # @api private
803
+ def run_after_close_callbacks
804
+ run_callbacks(:after, :closed)
805
+ end
806
+
771
807
  private
772
808
 
773
809
  def init_dsn(dsn_string)
data/lib/sentry/dsn.rb CHANGED
@@ -11,8 +11,9 @@ module Sentry
11
11
  REQUIRED_ATTRIBUTES = %w[host path public_key project_id].freeze
12
12
  LOCALHOST_NAMES = %w[localhost 127.0.0.1 ::1 [::1]].freeze
13
13
  LOCALHOST_PATTERN = /\.local(host|domain)?$/i
14
+ ORG_ID_REGEX = /\Ao(\d+)\./
14
15
 
15
- attr_reader :scheme, :secret_key, :port, *REQUIRED_ATTRIBUTES
16
+ attr_reader :scheme, :secret_key, :port, :org_id, *REQUIRED_ATTRIBUTES
16
17
 
17
18
  def initialize(dsn_string)
18
19
  @raw_value = dsn_string
@@ -31,6 +32,8 @@ module Sentry
31
32
  @host = uri.host
32
33
  @port = uri.port if uri.port
33
34
  @path = uri_path.join("/")
35
+
36
+ @org_id = extract_org_id_from_host
34
37
  end
35
38
 
36
39
  def valid?
@@ -101,5 +104,14 @@ module Sentry
101
104
 
102
105
  "Sentry " + fields.map { |key, value| "#{key}=#{value}" }.join(", ")
103
106
  end
107
+
108
+ private
109
+
110
+ def extract_org_id_from_host
111
+ return nil unless @host
112
+
113
+ match = ORG_ID_REGEX.match(@host)
114
+ match ? match[1] : nil
115
+ end
104
116
  end
105
117
  end
@@ -6,4 +6,7 @@ module Sentry
6
6
 
7
7
  class ExternalError < Error
8
8
  end
9
+
10
+ class SizeExceededError < ExternalError
11
+ end
9
12
  end
data/lib/sentry/hub.rb CHANGED
@@ -54,6 +54,12 @@ module Sentry
54
54
  current_layer&.client
55
55
  end
56
56
 
57
+ # All clients bound across the hub's scope stack, base layer first.
58
+ # @return [Array<Client>]
59
+ def clients
60
+ @stack.map(&:client).compact
61
+ end
62
+
57
63
  def configuration
58
64
  current_client.configuration
59
65
  end
@@ -11,6 +11,9 @@ module Sentry
11
11
  "HTTP_X_FORWARDED_FOR"
12
12
  ].freeze
13
13
 
14
+ # Regex to detect lowercase chars — match? is allocation-free (no MatchData/String)
15
+ LOWERCASE_PATTERN = /[a-z]/.freeze
16
+
14
17
  # See Sentry server default limits at
15
18
  # https://github.com/getsentry/sentry/blob/master/src/sentry/conf/server.py
16
19
  MAX_BODY_LIMIT = 4096 * 4
@@ -93,7 +96,7 @@ module Sentry
93
96
  next if key == "HTTP_AUTHORIZATION" && !send_default_pii
94
97
 
95
98
  # Rack stores headers as HTTP_WHAT_EVER, we need What-Ever
96
- key = key.sub(/^HTTP_/, "")
99
+ key = key.delete_prefix("HTTP_")
97
100
  key = key.split("_").map(&:capitalize).join("-")
98
101
 
99
102
  memo[key] = Utils::EncodingHelper.encode_to_utf_8(value.to_s)
@@ -108,7 +111,7 @@ module Sentry
108
111
  end
109
112
 
110
113
  def is_skippable_header?(key)
111
- key.upcase != key || # lower-case envs aren't real http headers
114
+ key.match?(LOWERCASE_PATTERN) || # lower-case envs aren't real http headers
112
115
  key == "HTTP_COOKIE" || # Cookies don't go here, they go somewhere else
113
116
  !(key.start_with?("HTTP_") || CONTENT_HEADERS.include?(key))
114
117
  end
@@ -119,12 +122,18 @@ module Sentry
119
122
  # if the request has legitimately sent a Version header themselves.
120
123
  # See: https://github.com/rack/rack/blob/028438f/lib/rack/handler/cgi.rb#L29
121
124
  def is_server_protocol?(key, value, protocol_version)
122
- rack_version = Gem::Version.new(::Rack.release)
123
- return false if rack_version >= Gem::Version.new("3.0")
125
+ return false if self.class.rack_3_or_above?
124
126
 
125
127
  key == "HTTP_VERSION" && value == protocol_version
126
128
  end
127
129
 
130
+ def self.rack_3_or_above?
131
+ return @rack_3_or_above if defined?(@rack_3_or_above)
132
+
133
+ @rack_3_or_above = defined?(::Rack) &&
134
+ Gem::Version.new(::Rack.release) >= Gem::Version.new("3.0")
135
+ end
136
+
128
137
  def filter_and_format_env(env, rack_env_whitelist)
129
138
  return env if rack_env_whitelist.empty?
130
139
 
@@ -27,38 +27,19 @@ module Sentry
27
27
  attr_accessor :abs_path, :context_line, :function, :in_app, :filename,
28
28
  :lineno, :module, :pre_context, :post_context, :vars
29
29
 
30
- def initialize(project_root, line, strip_backtrace_load_path = true)
31
- @project_root = project_root
32
- @strip_backtrace_load_path = strip_backtrace_load_path
33
-
30
+ def initialize(project_root, line, strip_backtrace_load_path = true, filename_cache: nil)
34
31
  @abs_path = line.file
35
32
  @function = line.method if line.method
36
33
  @lineno = line.number
37
34
  @in_app = line.in_app
38
35
  @module = line.module_name if line.module_name
39
- @filename = compute_filename
36
+ @filename = filename_cache&.compute_filename(@abs_path, @in_app, strip_backtrace_load_path)
40
37
  end
41
38
 
42
39
  def to_s
43
40
  "#{@filename}:#{@lineno}"
44
41
  end
45
42
 
46
- def compute_filename
47
- return if abs_path.nil?
48
- return abs_path unless @strip_backtrace_load_path
49
-
50
- prefix =
51
- if under_project_root? && in_app
52
- @project_root
53
- elsif under_project_root?
54
- longest_load_path || @project_root
55
- else
56
- longest_load_path
57
- end
58
-
59
- prefix ? abs_path[prefix.to_s.chomp(File::SEPARATOR).length + 1..-1] : abs_path
60
- end
61
-
62
43
  def set_context(linecache, context_lines)
63
44
  return unless abs_path
64
45
 
@@ -76,14 +57,6 @@ module Sentry
76
57
  end
77
58
 
78
59
  private
79
-
80
- def under_project_root?
81
- @project_root && abs_path.start_with?(@project_root)
82
- end
83
-
84
- def longest_load_path
85
- $LOAD_PATH.select { |path| abs_path.start_with?(path.to_s) }.max_by(&:size)
86
- end
87
60
  end
88
61
  end
89
62
  end
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "sentry/utils/filename_cache"
4
+
3
5
  module Sentry
4
6
  class StacktraceBuilder
5
7
  # @return [String]
@@ -20,6 +22,9 @@ module Sentry
20
22
  # @return [Boolean]
21
23
  attr_reader :strip_backtrace_load_path
22
24
 
25
+ # @return [FilenameCache]
26
+ attr_reader :filename_cache
27
+
23
28
  # @param project_root [String]
24
29
  # @param app_dirs_pattern [Regexp, nil]
25
30
  # @param linecache [LineCache]
@@ -46,6 +51,8 @@ module Sentry
46
51
  @context_lines = context_lines
47
52
  @backtrace_cleanup_callback = backtrace_cleanup_callback
48
53
  @strip_backtrace_load_path = strip_backtrace_load_path
54
+ @in_app_pattern = Regexp.new("^(#{project_root}/)?#{app_dirs_pattern}") if app_dirs_pattern
55
+ @filename_cache = FilenameCache.new(project_root)
49
56
  end
50
57
 
51
58
  # Generates a StacktraceInterface with the given backtrace.
@@ -64,13 +71,21 @@ module Sentry
64
71
  # @yieldparam frame [StacktraceInterface::Frame]
65
72
  # @return [StacktraceInterface]
66
73
  def build(backtrace:, &frame_callback)
67
- parsed_lines = parse_backtrace_lines(backtrace).select(&:file)
74
+ parsed_lines = parse_backtrace_lines(backtrace)
75
+
76
+ # Build frames in reverse order, skipping lines without files
77
+ # Single pass instead of select + reverse + map + compact
78
+ frames = []
79
+ i = parsed_lines.size - 1
80
+ while i >= 0
81
+ line = parsed_lines[i]
82
+ i -= 1
83
+ next unless line.file
68
84
 
69
- frames = parsed_lines.reverse.map do |line|
70
85
  frame = convert_parsed_line_into_frame(line)
71
86
  frame = frame_callback.call(frame) if frame_callback
72
- frame
73
- end.compact
87
+ frames << frame if frame
88
+ end
74
89
 
75
90
  StacktraceInterface.new(frames: frames)
76
91
  end
@@ -78,14 +93,15 @@ module Sentry
78
93
  private
79
94
 
80
95
  def convert_parsed_line_into_frame(line)
81
- frame = StacktraceInterface::Frame.new(project_root, line, strip_backtrace_load_path)
96
+ frame = StacktraceInterface::Frame.new(project_root, line, strip_backtrace_load_path, filename_cache: @filename_cache)
82
97
  frame.set_context(linecache, context_lines) if context_lines
83
98
  frame
84
99
  end
85
100
 
86
101
  def parse_backtrace_lines(backtrace)
87
102
  Backtrace.parse(
88
- backtrace, project_root, app_dirs_pattern, &backtrace_cleanup_callback
103
+ backtrace, project_root, app_dirs_pattern,
104
+ in_app_pattern: @in_app_pattern, &backtrace_cleanup_callback
89
105
  ).lines
90
106
  end
91
107
  end
@@ -12,36 +12,33 @@ module Sentry
12
12
  # file. The number of lines retrieved is (2 * context) + 1, the middle
13
13
  # line should be the line requested by lineno. See specs for more information.
14
14
  def get_file_context(filename, lineno, context)
15
- return nil, nil, nil unless valid_path?(filename)
15
+ lines = getlines(filename)
16
+ return nil, nil, nil unless lines
16
17
 
17
- lines = Array.new(2 * context + 1) do |i|
18
- getline(filename, lineno - context + i)
19
- end
20
- [lines[0..(context - 1)], lines[context], lines[(context + 1)..-1]]
18
+ first_line = lineno - context
19
+ pre = Array.new(context) { |i| line_at(lines, first_line + i) }
20
+ context_line = line_at(lines, lineno)
21
+ post = Array.new(context) { |i| line_at(lines, lineno + 1 + i) }
22
+
23
+ [pre, context_line, post]
21
24
  end
22
25
 
23
26
  private
24
27
 
25
- def valid_path?(path)
26
- lines = getlines(path)
27
- !lines.nil?
28
+ def line_at(lines, n)
29
+ return nil if n < 1
30
+
31
+ lines[n - 1]
28
32
  end
29
33
 
30
34
  def getlines(path)
31
- @cache[path] ||= begin
32
- File.open(path, "r", &:readlines)
33
- rescue
34
- nil
35
+ @cache.fetch(path) do
36
+ @cache[path] = begin
37
+ File.open(path, "r", &:readlines)
38
+ rescue
39
+ nil
40
+ end
35
41
  end
36
42
  end
37
-
38
- def getline(path, n)
39
- return nil if n < 1
40
-
41
- lines = getlines(path)
42
- return nil if lines.nil?
43
-
44
- lines[n - 1]
45
- end
46
43
  end
47
44
  end
@@ -43,7 +43,7 @@ module Sentry
43
43
  private
44
44
 
45
45
  def serialize_attributes
46
- @attributes.transform_values! { |v| attribute_hash(v) }
46
+ @attributes.transform_values { |v| attribute_hash(v) }
47
47
  end
48
48
  end
49
49
  end
@@ -9,28 +9,8 @@ module Sentry
9
9
  abs_path.match?(@in_app_pattern)
10
10
  end
11
11
 
12
- # copied from stacktrace.rb since I don't want to touch existing code
13
- # TODO-neel-profiler try to fetch this from stackprof once we patch
14
- # the native extension
15
12
  def compute_filename(abs_path, in_app)
16
- return nil if abs_path.nil?
17
-
18
- under_project_root = @project_root && abs_path.start_with?(@project_root)
19
-
20
- prefix =
21
- if under_project_root && in_app
22
- @project_root
23
- else
24
- longest_load_path = $LOAD_PATH.select { |path| abs_path.start_with?(path.to_s) }.max_by(&:size)
25
-
26
- if under_project_root
27
- longest_load_path || @project_root
28
- else
29
- longest_load_path
30
- end
31
- end
32
-
33
- prefix ? abs_path[prefix.to_s.chomp(File::SEPARATOR).length + 1..-1] : abs_path
13
+ @filename_cache.compute_filename(abs_path, in_app, true)
34
14
  end
35
15
 
36
16
  def split_module(name)
@@ -26,6 +26,7 @@ module Sentry
26
26
  @project_root = configuration.project_root
27
27
  @app_dirs_pattern = configuration.app_dirs_pattern
28
28
  @in_app_pattern = Regexp.new("^(#{@project_root}/)?#{@app_dirs_pattern}")
29
+ @filename_cache = configuration.stacktrace_builder.filename_cache
29
30
  end
30
31
 
31
32
  def start
@@ -53,6 +53,44 @@ module Sentry
53
53
  [trace_id, parent_span_id, parent_sampled]
54
54
  end
55
55
 
56
+ # Determines whether we should continue an incoming trace based on org_id matching
57
+ # and the strict_trace_continuation configuration option.
58
+ #
59
+ # @param incoming_baggage [Baggage] the baggage from the incoming request
60
+ # @return [Boolean]
61
+ def self.should_continue_trace?(incoming_baggage)
62
+ return true unless Sentry.initialized?
63
+
64
+ configuration = Sentry.configuration
65
+ sdk_org_id = configuration.effective_org_id
66
+ baggage_org_id = incoming_baggage.items["org_id"]
67
+
68
+ # Mismatched org IDs always start a new trace regardless of strict mode
69
+ if sdk_org_id && baggage_org_id && sdk_org_id != baggage_org_id
70
+ Sentry.sdk_logger.debug(LOGGER_PROGNAME) do
71
+ "Starting a new trace because org IDs don't match (incoming baggage org_id: #{baggage_org_id}, SDK org_id: #{sdk_org_id})"
72
+ end
73
+
74
+ return false
75
+ end
76
+
77
+ return true unless configuration.strict_trace_continuation
78
+
79
+ # In strict mode, both must be present and match (unless both are missing)
80
+ if sdk_org_id.nil? && baggage_org_id.nil?
81
+ true
82
+ elsif sdk_org_id.nil? || baggage_org_id.nil?
83
+ Sentry.sdk_logger.debug(LOGGER_PROGNAME) do
84
+ "Starting a new trace because strict trace continuation is enabled and one org ID is missing " \
85
+ "(incoming baggage org_id: #{baggage_org_id.inspect}, SDK org_id: #{sdk_org_id.inspect})"
86
+ end
87
+
88
+ false
89
+ else
90
+ true
91
+ end
92
+ end
93
+
56
94
  def self.extract_sample_rand_from_baggage(baggage, trace_id = nil)
57
95
  return unless baggage&.items
58
96
 
@@ -96,9 +134,7 @@ module Sentry
96
134
  sentry_trace_data = self.class.extract_sentry_trace(sentry_trace_header)
97
135
 
98
136
  if sentry_trace_data
99
- @trace_id, @parent_span_id, @parent_sampled = sentry_trace_data
100
-
101
- @baggage =
137
+ incoming_baggage =
102
138
  if baggage_header && !baggage_header.empty?
103
139
  Baggage.from_incoming_header(baggage_header)
104
140
  else
@@ -108,10 +144,13 @@ module Sentry
108
144
  Baggage.new({})
109
145
  end
110
146
 
111
- @sample_rand = self.class.extract_sample_rand_from_baggage(@baggage, @trace_id)
112
-
113
- @baggage.freeze!
114
- @incoming_trace = true
147
+ if self.class.should_continue_trace?(incoming_baggage)
148
+ @trace_id, @parent_span_id, @parent_sampled = sentry_trace_data
149
+ @baggage = incoming_baggage
150
+ @sample_rand = self.class.extract_sample_rand_from_baggage(@baggage, @trace_id)
151
+ @baggage.freeze!
152
+ @incoming_trace = true
153
+ end
115
154
  end
116
155
  end
117
156
  end
@@ -162,7 +201,8 @@ module Sentry
162
201
  "sample_rand" => Utils::SampleRand.format(@sample_rand),
163
202
  "environment" => configuration.environment,
164
203
  "release" => configuration.release,
165
- "public_key" => configuration.dsn&.public_key
204
+ "public_key" => configuration.dsn&.public_key,
205
+ "org_id" => configuration.effective_org_id
166
206
  }
167
207
 
168
208
  items.compact!
@@ -6,6 +6,7 @@ module Sentry
6
6
  class << self
7
7
  def detect_release(project_root:, running_on_heroku:)
8
8
  detect_release_from_env ||
9
+ detect_release_from_kamal ||
9
10
  detect_release_from_git ||
10
11
  detect_release_from_capistrano(project_root) ||
11
12
  detect_release_from_heroku(running_on_heroku)
@@ -13,7 +14,7 @@ module Sentry
13
14
 
14
15
  def detect_release_from_heroku(running_on_heroku)
15
16
  return unless running_on_heroku
16
- ENV["HEROKU_SLUG_COMMIT"]
17
+ ENV["HEROKU_BUILD_COMMIT"] || ENV["HEROKU_SLUG_COMMIT"]
17
18
  end
18
19
 
19
20
  def detect_release_from_capistrano(project_root)
@@ -31,6 +32,10 @@ module Sentry
31
32
  Sentry.sys_command("git rev-parse HEAD") if File.directory?(".git")
32
33
  end
33
34
 
35
+ def detect_release_from_kamal
36
+ ENV["KAMAL_VERSION"]
37
+ end
38
+
34
39
  def detect_release_from_env
35
40
  ENV["SENTRY_RELEASE"]
36
41
  end
data/lib/sentry/scope.rb CHANGED
@@ -135,6 +135,7 @@ module Sentry
135
135
  copy.session = session.deep_dup
136
136
  copy.propagation_context = propagation_context.deep_dup
137
137
  copy.attachments = attachments.dup
138
+ copy.event_processors = event_processors.dup
138
139
  copy
139
140
  end
140
141
 
@@ -37,13 +37,25 @@ module Sentry
37
37
  # - auto_session_tracking
38
38
  block&.call(dummy_config)
39
39
 
40
+ # Install the testing clients on the *main* hub rather than the current
41
+ # thread's hub. `Sentry.clone_hub_to_current_thread` (used by
42
+ # Sentry::Rack::CaptureExceptions) always clones the main hub, so if we
43
+ # only mutated the thread-local hub a request-time clone would observe a
44
+ # stale transport.
45
+ main_hub = Sentry.get_main_hub
46
+
40
47
  # the base layer's client should already use the dummy config so nothing will be sent by accident
41
48
  base_client = Sentry::Client.new(dummy_config)
42
- Sentry.get_current_hub.bind_client(base_client)
49
+ main_hub.bind_client(base_client)
43
50
  # create a new layer so mutations made to the testing scope or configuration could be simply popped later
44
- Sentry.get_current_hub.push_scope
51
+ main_hub.push_scope
45
52
  test_client = Sentry::Client.new(dummy_config.dup)
46
- Sentry.get_current_hub.bind_client(test_client)
53
+ main_hub.bind_client(test_client)
54
+
55
+ # Realign the current thread's hub with the main hub so direct
56
+ # `sentry_events` reads and any hub the Rack middleware clones from the
57
+ # main hub all observe the same DummyTransport.
58
+ Thread.current.thread_variable_set(Sentry::THREAD_LOCAL, main_hub)
47
59
  end
48
60
 
49
61
  # Clears all stored events and envelopes.
@@ -54,11 +66,15 @@ module Sentry
54
66
 
55
67
  clear_sentry_events
56
68
 
57
- # pop testing layer created by `setup_sentry_test`
58
- # but keep the base layer to avoid nil-pointer errors
69
+ # pop the testing layer created by `setup_sentry_test` off the *main*
70
+ # hub (that is where `setup_sentry_test` pushed it), keeping the base
71
+ # layer to avoid nil-pointer errors. Popping the current thread's hub
72
+ # would leave the test layer dangling on the main hub, which the next
73
+ # request-time clone would inherit.
59
74
  # TODO: find a way to notify users if they somehow popped the test layer before calling this method
60
- if Sentry.get_current_hub.instance_variable_get(:@stack).size > 1
61
- Sentry.get_current_hub.pop_scope
75
+ main_hub = Sentry.get_main_hub
76
+ if main_hub.instance_variable_get(:@stack).size > 1
77
+ main_hub.pop_scope
62
78
  end
63
79
  Sentry::Scope.global_event_processors.clear
64
80
  end
@@ -66,7 +82,13 @@ module Sentry
66
82
  def clear_sentry_events
67
83
  return unless Sentry.initialized?
68
84
 
69
- sentry_transport.clear if sentry_transport.respond_to?(:clear)
85
+ # Clear every transport reachable from the current thread's hub and the
86
+ # main hub (including its base layer). A request-time clone shares the
87
+ # main hub's base-layer transport, so clearing only the current
88
+ # transport would let stale events survive into the next test.
89
+ sentry_test_transports.each do |transport|
90
+ transport.clear if transport.respond_to?(:clear)
91
+ end
70
92
 
71
93
  if Sentry.configuration.enable_logs && sentry_logger.respond_to?(:clear)
72
94
  sentry_logger.clear
@@ -83,6 +105,17 @@ module Sentry
83
105
  Sentry.get_current_client.transport
84
106
  end
85
107
 
108
+ # Every transport reachable from the current thread's hub and the main
109
+ # hub, across all stack layers. Used by `clear_sentry_events` so a stale
110
+ # DummyTransport (e.g. the main hub's base layer that a request-time clone
111
+ # shares) cannot carry leftover events into the next test.
112
+ # @return [Array<Transport>]
113
+ def sentry_test_transports
114
+ [Sentry.get_current_hub, Sentry.get_main_hub].compact.uniq.flat_map do |hub|
115
+ hub.clients.map(&:transport)
116
+ end.compact.uniq
117
+ end
118
+
86
119
  # Returns the captured event objects.
87
120
  # @return [Array<Event>]
88
121
  def sentry_events
@@ -295,7 +295,8 @@ module Sentry
295
295
  "sampled" => sampled&.to_s,
296
296
  "environment" => configuration&.environment,
297
297
  "release" => configuration&.release,
298
- "public_key" => configuration&.dsn&.public_key
298
+ "public_key" => configuration&.dsn&.public_key,
299
+ "org_id" => configuration&.effective_org_id
299
300
  }
300
301
 
301
302
  items["transaction"] = name unless source_low_quality?
@@ -18,5 +18,12 @@ module Sentry
18
18
  def send_envelope(envelope)
19
19
  @envelopes << envelope
20
20
  end
21
+
22
+ # Empties the captured events and envelopes so `TestHelper.clear_sentry_events`
23
+ # also clears the dummy transport instance
24
+ def clear
25
+ @events.clear
26
+ @envelopes.clear
27
+ end
21
28
  end
22
29
  end
@@ -49,6 +49,12 @@ module Sentry
49
49
 
50
50
  if response.code.match?(/\A2\d{2}/)
51
51
  handle_rate_limited_response(response) if has_rate_limited_header?(response)
52
+ elsif response.code == "413"
53
+ error_message = "HTTP 413: Envelope dropped due to exceeded size limit"
54
+ error_message += " (body: #{response.body})" if response.body && !response.body.empty?
55
+ log_warn(error_message)
56
+
57
+ raise Sentry::SizeExceededError, error_message
52
58
  elsif response.code == "429"
53
59
  log_debug("the server responded with status 429")
54
60
  handle_rate_limited_response(response)
@@ -19,7 +19,8 @@ module Sentry
19
19
  :before_send,
20
20
  :event_processor,
21
21
  :insufficient_data,
22
- :backpressure
22
+ :backpressure,
23
+ :send_error
23
24
  ]
24
25
 
25
26
  include LoggingHelper
@@ -61,6 +62,10 @@ module Sentry
61
62
  log_debug("[Transport] Sending envelope with items [#{serialized_items.map(&:type).join(', ')}] #{envelope.event_id} to Sentry")
62
63
  send_data(data)
63
64
  end
65
+ rescue Sentry::SizeExceededError
66
+ serialized_items&.each do |item|
67
+ record_lost_event(:send_error, item.data_category)
68
+ end
64
69
  end
65
70
 
66
71
  def serialize_envelope(envelope)
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sentry
4
+ class FilenameCache
5
+ attr_reader :cache
6
+
7
+ def initialize(project_root)
8
+ @project_root = project_root
9
+ @load_paths = $LOAD_PATH.map(&:to_s).sort_by(&:size).reverse.freeze
10
+ @cache = {}
11
+ end
12
+
13
+ def compute_filename(abs_path, in_app, strip_backtrace_load_path)
14
+ return unless abs_path
15
+ return abs_path unless strip_backtrace_load_path
16
+
17
+ @cache.fetch(abs_path) do
18
+ under_root = @project_root && abs_path.start_with?(@project_root)
19
+ prefix =
20
+ if under_root && in_app
21
+ @project_root
22
+ elsif under_root
23
+ longest_load_path(abs_path) || @project_root
24
+ else
25
+ longest_load_path(abs_path)
26
+ end
27
+
28
+ @cache[abs_path] = if prefix
29
+ offset = if prefix.end_with?(File::SEPARATOR)
30
+ prefix.bytesize
31
+ else
32
+ prefix.bytesize + 1
33
+ end
34
+ abs_path.byteslice(offset, abs_path.bytesize - offset)
35
+ else
36
+ abs_path
37
+ end
38
+ end
39
+ end
40
+
41
+ private
42
+
43
+ def longest_load_path(abs_path)
44
+ @load_paths.find { |path| abs_path.start_with?(path) }
45
+ end
46
+ end
47
+ end
@@ -12,7 +12,13 @@ module Sentry
12
12
  end
13
13
 
14
14
  def set_propagation_headers(req)
15
- Sentry.get_trace_propagation_headers&.each { |k, v| req[k] = v }
15
+ Sentry.get_trace_propagation_headers&.each do |k, v|
16
+ if k == BAGGAGE_HEADER_NAME && req[k]
17
+ req[k] = "#{v},#{req[k]}"
18
+ else
19
+ req[k] = v
20
+ end
21
+ end
16
22
  end
17
23
 
18
24
  def record_sentry_breadcrumb(request_info, response_status)
@@ -10,11 +10,12 @@ module Sentry
10
10
 
11
11
  attr_reader :profile
12
12
 
13
- def initialize(profile, project_root:, in_app_pattern:, app_dirs_pattern:)
13
+ def initialize(profile, project_root:, in_app_pattern:, app_dirs_pattern:, filename_cache:)
14
14
  @profile = profile
15
15
  @project_root = project_root
16
16
  @in_app_pattern = in_app_pattern
17
17
  @app_dirs_pattern = app_dirs_pattern
18
+ @filename_cache = filename_cache
18
19
  end
19
20
 
20
21
  def to_h
@@ -24,6 +24,7 @@ module Sentry
24
24
  @project_root = configuration.project_root
25
25
  @app_dirs_pattern = configuration.app_dirs_pattern
26
26
  @in_app_pattern = Regexp.new("^(#{@project_root}/)?#{@app_dirs_pattern}")
27
+ @filename_cache = configuration.stacktrace_builder.filename_cache
27
28
  end
28
29
 
29
30
  def set_initial_sample_decision(transaction_sampled)
@@ -125,7 +126,8 @@ module Sentry
125
126
  result,
126
127
  project_root: @project_root,
127
128
  app_dirs_pattern: @app_dirs_pattern,
128
- in_app_pattern: @in_app_pattern
129
+ in_app_pattern: @in_app_pattern,
130
+ filename_cache: @filename_cache
129
131
  )
130
132
  end
131
133
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Sentry
4
- VERSION = "6.4.1"
4
+ VERSION = "6.6.0"
5
5
  end
data/lib/sentry-ruby.rb CHANGED
@@ -268,6 +268,7 @@ module Sentry
268
268
  end
269
269
 
270
270
  if client = get_current_client
271
+ client.configuration.run_after_close_callbacks
271
272
  client.flush
272
273
 
273
274
  if client.configuration.include_local_variables
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: sentry-ruby
3
3
  version: !ruby/object:Gem::Version
4
- version: 6.4.1
4
+ version: 6.6.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sentry Team
@@ -157,6 +157,7 @@ files:
157
157
  - lib/sentry/utils/encoding_helper.rb
158
158
  - lib/sentry/utils/env_helper.rb
159
159
  - lib/sentry/utils/exception_cause_chain.rb
160
+ - lib/sentry/utils/filename_cache.rb
160
161
  - lib/sentry/utils/http_tracing.rb
161
162
  - lib/sentry/utils/logging_helper.rb
162
163
  - lib/sentry/utils/real_ip.rb
@@ -169,15 +170,15 @@ files:
169
170
  - lib/sentry/version.rb
170
171
  - sentry-ruby-core.gemspec
171
172
  - sentry-ruby.gemspec
172
- homepage: https://github.com/getsentry/sentry-ruby/tree/6.4.1/sentry-ruby
173
+ homepage: https://github.com/getsentry/sentry-ruby/tree/6.6.0/sentry-ruby
173
174
  licenses:
174
175
  - MIT
175
176
  metadata:
176
- homepage_uri: https://github.com/getsentry/sentry-ruby/tree/6.4.1/sentry-ruby
177
- source_code_uri: https://github.com/getsentry/sentry-ruby/tree/6.4.1/sentry-ruby
178
- changelog_uri: https://github.com/getsentry/sentry-ruby/blob/6.4.1/CHANGELOG.md
177
+ homepage_uri: https://github.com/getsentry/sentry-ruby/tree/6.6.0/sentry-ruby
178
+ source_code_uri: https://github.com/getsentry/sentry-ruby/tree/6.6.0/sentry-ruby
179
+ changelog_uri: https://github.com/getsentry/sentry-ruby/blob/6.6.0/CHANGELOG.md
179
180
  bug_tracker_uri: https://github.com/getsentry/sentry-ruby/issues
180
- documentation_uri: http://www.rubydoc.info/gems/sentry-ruby/6.4.1
181
+ documentation_uri: http://www.rubydoc.info/gems/sentry-ruby/6.6.0
181
182
  rdoc_options: []
182
183
  require_paths:
183
184
  - lib