sentry-ruby-core 4.1.5.pre.beta.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.
Files changed (55) hide show
  1. checksums.yaml +7 -0
  2. data/.craft.yml +33 -0
  3. data/.gitignore +11 -0
  4. data/.rspec +3 -0
  5. data/.travis.yml +6 -0
  6. data/CHANGELOG.md +125 -0
  7. data/CODE_OF_CONDUCT.md +74 -0
  8. data/Gemfile +16 -0
  9. data/LICENSE.txt +21 -0
  10. data/README.md +258 -0
  11. data/Rakefile +13 -0
  12. data/bin/console +14 -0
  13. data/bin/setup +8 -0
  14. data/lib/sentry-ruby.rb +191 -0
  15. data/lib/sentry/background_worker.rb +37 -0
  16. data/lib/sentry/backtrace.rb +126 -0
  17. data/lib/sentry/benchmarks/benchmark_transport.rb +14 -0
  18. data/lib/sentry/breadcrumb.rb +25 -0
  19. data/lib/sentry/breadcrumb/sentry_logger.rb +87 -0
  20. data/lib/sentry/breadcrumb_buffer.rb +47 -0
  21. data/lib/sentry/client.rb +96 -0
  22. data/lib/sentry/configuration.rb +396 -0
  23. data/lib/sentry/core_ext/object/deep_dup.rb +57 -0
  24. data/lib/sentry/core_ext/object/duplicable.rb +153 -0
  25. data/lib/sentry/dsn.rb +48 -0
  26. data/lib/sentry/event.rb +173 -0
  27. data/lib/sentry/hub.rb +143 -0
  28. data/lib/sentry/integrable.rb +24 -0
  29. data/lib/sentry/interface.rb +22 -0
  30. data/lib/sentry/interfaces/exception.rb +11 -0
  31. data/lib/sentry/interfaces/request.rb +113 -0
  32. data/lib/sentry/interfaces/single_exception.rb +14 -0
  33. data/lib/sentry/interfaces/stacktrace.rb +90 -0
  34. data/lib/sentry/linecache.rb +44 -0
  35. data/lib/sentry/logger.rb +20 -0
  36. data/lib/sentry/rack.rb +4 -0
  37. data/lib/sentry/rack/capture_exceptions.rb +68 -0
  38. data/lib/sentry/rack/deprecations.rb +19 -0
  39. data/lib/sentry/rake.rb +17 -0
  40. data/lib/sentry/scope.rb +210 -0
  41. data/lib/sentry/span.rb +133 -0
  42. data/lib/sentry/transaction.rb +157 -0
  43. data/lib/sentry/transaction_event.rb +29 -0
  44. data/lib/sentry/transport.rb +88 -0
  45. data/lib/sentry/transport/configuration.rb +21 -0
  46. data/lib/sentry/transport/dummy_transport.rb +14 -0
  47. data/lib/sentry/transport/http_transport.rb +62 -0
  48. data/lib/sentry/utils/argument_checking_helper.rb +11 -0
  49. data/lib/sentry/utils/exception_cause_chain.rb +20 -0
  50. data/lib/sentry/utils/real_ip.rb +70 -0
  51. data/lib/sentry/utils/request_id.rb +16 -0
  52. data/lib/sentry/version.rb +3 -0
  53. data/sentry-ruby-core.gemspec +27 -0
  54. data/sentry-ruby.gemspec +23 -0
  55. metadata +128 -0
@@ -0,0 +1,13 @@
1
+ require "rake/clean"
2
+ CLOBBER.include "pkg"
3
+
4
+ require "bundler/gem_helper"
5
+ Bundler::GemHelper.install_tasks(name: "sentry-ruby")
6
+
7
+ require "rspec/core/rake_task"
8
+
9
+ RSpec::Core::RakeTask.new(:spec).tap do |task|
10
+ task.rspec_opts = "--order rand"
11
+ end
12
+
13
+ task :default => :spec
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "sentry/ruby"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start(__FILE__)
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,191 @@
1
+ require "forwardable"
2
+ require "time"
3
+
4
+ require "sentry/version"
5
+ require "sentry/core_ext/object/deep_dup"
6
+ require "sentry/utils/argument_checking_helper"
7
+ require "sentry/configuration"
8
+ require "sentry/logger"
9
+ require "sentry/event"
10
+ require "sentry/transaction_event"
11
+ require "sentry/span"
12
+ require "sentry/transaction"
13
+ require "sentry/hub"
14
+ require "sentry/background_worker"
15
+
16
+
17
+ [
18
+ "sentry/rake",
19
+ "sentry/rack",
20
+ ].each do |lib|
21
+ begin
22
+ require lib
23
+ rescue LoadError
24
+ end
25
+ end
26
+
27
+ module Sentry
28
+ class Error < StandardError
29
+ end
30
+
31
+ META = { "name" => "sentry.ruby", "version" => Sentry::VERSION }.freeze
32
+
33
+ LOGGER_PROGNAME = "sentry".freeze
34
+
35
+ THREAD_LOCAL = :sentry_hub
36
+
37
+ def self.sdk_meta
38
+ META
39
+ end
40
+
41
+ def self.utc_now
42
+ Time.now.utc
43
+ end
44
+
45
+ class << self
46
+ # Returns a hash that contains all the integrations that have been registered to the main SDK.
47
+ def integrations
48
+ @integrations ||= {}
49
+ end
50
+
51
+ # Registers the SDK integration with its name and version.
52
+ def register_integration(name, version)
53
+ meta = { name: "sentry.ruby.#{name}", version: version }.freeze
54
+ integrations[name.to_s] = meta
55
+ end
56
+ end
57
+
58
+ class << self
59
+ extend Forwardable
60
+
61
+ def_delegators :get_current_client, :configuration, :send_event
62
+ def_delegators :get_current_scope, :set_tags, :set_extras, :set_user
63
+
64
+ attr_accessor :background_worker
65
+
66
+ def init(&block)
67
+ config = Configuration.new
68
+ yield(config)
69
+ client = Client.new(config)
70
+ scope = Scope.new
71
+ hub = Hub.new(client, scope)
72
+ Thread.current[THREAD_LOCAL] = hub
73
+ @main_hub = hub
74
+ @background_worker = Sentry::BackgroundWorker.new(config)
75
+ end
76
+
77
+ # Returns the main thread's active hub.
78
+ def get_main_hub
79
+ @main_hub
80
+ end
81
+
82
+ # Takes an instance of Sentry::Breadcrumb and stores it to the current active scope.
83
+ def add_breadcrumb(breadcrumb)
84
+ get_current_scope.breadcrumbs.record(breadcrumb)
85
+ end
86
+
87
+ # Returns the current active hub.
88
+ # If the current thread doesn't have an active hub, it will clone the main thread's active hub,
89
+ # stores it in the current thread, and then returns it.
90
+ def get_current_hub
91
+ # we need to assign a hub to the current thread if it doesn't have one yet
92
+ #
93
+ # ideally, we should do this proactively whenever a new thread is created
94
+ # but it's impossible for the SDK to keep track every new thread
95
+ # so we need to use this rather passive way to make sure the app doesn't crash
96
+ Thread.current[THREAD_LOCAL] || clone_hub_to_current_thread
97
+ end
98
+
99
+ # Returns the current active client.
100
+ def get_current_client
101
+ get_current_hub&.current_client
102
+ end
103
+
104
+ # Returns the current active scope.
105
+ def get_current_scope
106
+ get_current_hub&.current_scope
107
+ end
108
+
109
+ # Clones the main thread's active hub and stores it to the current thread.
110
+ def clone_hub_to_current_thread
111
+ Thread.current[THREAD_LOCAL] = get_main_hub.clone
112
+ end
113
+
114
+ # Takes a block and yields the current active scope.
115
+ #
116
+ # ```ruby
117
+ # Sentry.configure_scope do |scope|
118
+ # scope.set_tags(foo: "bar")
119
+ # end
120
+ #
121
+ # Sentry.capture_message("test message") # this event will have tags { foo: "bar" }
122
+ # ```
123
+ #
124
+ def configure_scope(&block)
125
+ get_current_hub&.configure_scope(&block)
126
+ end
127
+
128
+ # Takes a block and yields a temporary scope.
129
+ # The temporary scope will inherit all the attributes from the current active scope and replace it to be the active
130
+ # scope inside the block. For example:
131
+ #
132
+ # ```ruby
133
+ # Sentry.configure_scope do |scope|
134
+ # scope.set_tags(foo: "bar")
135
+ # end
136
+ #
137
+ # Sentry.capture_message("test message") # this event will have tags { foo: "bar" }
138
+ #
139
+ # Sentry.with_scope do |temp_scope|
140
+ # temp_scope.set_tags(foo: "baz")
141
+ # Sentry.capture_message("test message 2") # this event will have tags { foo: "baz" }
142
+ # end
143
+ #
144
+ # Sentry.capture_message("test message 3") # this event will have tags { foo: "bar" }
145
+ # ```
146
+ #
147
+ def with_scope(&block)
148
+ get_current_hub&.with_scope(&block)
149
+ end
150
+
151
+ # Takes an exception and reports it to Sentry via the currently active hub.
152
+ def capture_exception(exception, **options, &block)
153
+ get_current_hub&.capture_exception(exception, **options, &block)
154
+ end
155
+
156
+ # Takes a message string and reports it to Sentry via the currently active hub.
157
+ def capture_message(message, **options, &block)
158
+ get_current_hub&.capture_message(message, **options, &block)
159
+ end
160
+
161
+ # Takes an instance of Sentry::Event and dispatches it to the currently active hub.
162
+ def capture_event(event)
163
+ get_current_hub&.capture_event(event)
164
+ end
165
+
166
+ # Takes or initializes a new Sentry::Transaction and makes a sampling decision for it.
167
+ def start_transaction(**options)
168
+ get_current_hub&.start_transaction(**options)
169
+ end
170
+
171
+ # Returns the id of the lastly reported Sentry::Event.
172
+ def last_event_id
173
+ get_current_hub&.last_event_id
174
+ end
175
+
176
+ def sys_command(command)
177
+ result = `#{command} 2>&1` rescue nil
178
+ return if result.nil? || result.empty? || ($CHILD_STATUS && $CHILD_STATUS.exitstatus != 0)
179
+
180
+ result.strip
181
+ end
182
+
183
+ def initialized?
184
+ !!@main_hub
185
+ end
186
+
187
+ def logger
188
+ configuration.logger
189
+ end
190
+ end
191
+ end
@@ -0,0 +1,37 @@
1
+ require "concurrent/executor/thread_pool_executor"
2
+ require "concurrent/executor/immediate_executor"
3
+
4
+ module Sentry
5
+ class BackgroundWorker
6
+ attr_reader :max_queue, :number_of_threads
7
+
8
+ def initialize(configuration)
9
+ @max_queue = 30
10
+ @number_of_threads = configuration.background_worker_threads
11
+
12
+ @executor =
13
+ if configuration.async
14
+ configuration.logger.debug(LOGGER_PROGNAME) { "config.async is set, BackgroundWorker is disabled" }
15
+ Concurrent::ImmediateExecutor.new
16
+ elsif @number_of_threads == 0
17
+ configuration.logger.debug(LOGGER_PROGNAME) { "config.background_worker_threads is set to 0, all events will be sent synchronously" }
18
+ Concurrent::ImmediateExecutor.new
19
+ else
20
+ configuration.logger.debug(LOGGER_PROGNAME) { "initialized a background worker with #{@number_of_threads} threads" }
21
+
22
+ Concurrent::ThreadPoolExecutor.new(
23
+ min_threads: 0,
24
+ max_threads: @number_of_threads,
25
+ max_queue: @max_queue,
26
+ fallback_policy: :discard
27
+ )
28
+ end
29
+ end
30
+
31
+ def perform(&block)
32
+ @executor.post do
33
+ block.call
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,126 @@
1
+ # frozen_string_literal: true
2
+
3
+ ## Inspired by Rails' and Airbrake's backtrace parsers.
4
+
5
+ module Sentry
6
+ # Front end to parsing the backtrace for each notice
7
+ class Backtrace
8
+ # Handles backtrace parsing line by line
9
+ class Line
10
+ RB_EXTENSION = ".rb"
11
+ # regexp (optional leading X: on windows, or JRuby9000 class-prefix)
12
+ RUBY_INPUT_FORMAT = /
13
+ ^ \s* (?: [a-zA-Z]: | uri:classloader: )? ([^:]+ | <.*>):
14
+ (\d+)
15
+ (?: :in \s `([^']+)')?$
16
+ /x.freeze
17
+
18
+ # org.jruby.runtime.callsite.CachingCallSite.call(CachingCallSite.java:170)
19
+ JAVA_INPUT_FORMAT = /^(.+)\.([^\.]+)\(([^\:]+)\:(\d+)\)$/.freeze
20
+
21
+ # The file portion of the line (such as app/models/user.rb)
22
+ attr_reader :file
23
+
24
+ # The line number portion of the line
25
+ attr_reader :number
26
+
27
+ # The method of the line (such as index)
28
+ attr_reader :method
29
+
30
+ # The module name (JRuby)
31
+ attr_reader :module_name
32
+
33
+ attr_reader :in_app_pattern
34
+
35
+ # Parses a single line of a given backtrace
36
+ # @param [String] unparsed_line The raw line from +caller+ or some backtrace
37
+ # @return [Line] The parsed backtrace line
38
+ def self.parse(unparsed_line, in_app_pattern)
39
+ ruby_match = unparsed_line.match(RUBY_INPUT_FORMAT)
40
+ if ruby_match
41
+ _, file, number, method = ruby_match.to_a
42
+ file.sub!(/\.class$/, RB_EXTENSION)
43
+ module_name = nil
44
+ else
45
+ java_match = unparsed_line.match(JAVA_INPUT_FORMAT)
46
+ _, module_name, method, file, number = java_match.to_a
47
+ end
48
+ new(file, number, method, module_name, in_app_pattern)
49
+ end
50
+
51
+ def initialize(file, number, method, module_name, in_app_pattern)
52
+ @file = file
53
+ @module_name = module_name
54
+ @number = number.to_i
55
+ @method = method
56
+ @in_app_pattern = in_app_pattern
57
+ end
58
+
59
+ def in_app
60
+ if file =~ in_app_pattern
61
+ true
62
+ else
63
+ false
64
+ end
65
+ end
66
+
67
+ # Reconstructs the line in a readable fashion
68
+ def to_s
69
+ "#{file}:#{number}:in `#{method}'"
70
+ end
71
+
72
+ def ==(other)
73
+ to_s == other.to_s
74
+ end
75
+
76
+ def inspect
77
+ "<Line:#{self}>"
78
+ end
79
+ end
80
+
81
+ APP_DIRS_PATTERN = /(bin|exe|app|config|lib|test)/.freeze
82
+
83
+ # holder for an Array of Backtrace::Line instances
84
+ attr_reader :lines
85
+
86
+ def self.parse(backtrace, project_root, app_dirs_pattern, &backtrace_cleanup_callback)
87
+ ruby_lines = backtrace.is_a?(Array) ? backtrace : backtrace.split(/\n\s*/)
88
+
89
+ ruby_lines = backtrace_cleanup_callback.call(ruby_lines) if backtrace_cleanup_callback
90
+
91
+ in_app_pattern ||= begin
92
+ Regexp.new("^(#{project_root}/)?#{app_dirs_pattern || APP_DIRS_PATTERN}")
93
+ end
94
+
95
+ lines = ruby_lines.to_a.map do |unparsed_line|
96
+ Line.parse(unparsed_line, in_app_pattern)
97
+ end
98
+
99
+ new(lines)
100
+ end
101
+
102
+ def initialize(lines)
103
+ @lines = lines
104
+ end
105
+
106
+ def inspect
107
+ "<Backtrace: " + lines.map(&:inspect).join(", ") + ">"
108
+ end
109
+
110
+ def to_s
111
+ content = []
112
+ lines.each do |line|
113
+ content << line
114
+ end
115
+ content.join("\n")
116
+ end
117
+
118
+ def ==(other)
119
+ if other.respond_to?(:lines)
120
+ lines == other.lines
121
+ else
122
+ false
123
+ end
124
+ end
125
+ end
126
+ end
@@ -0,0 +1,14 @@
1
+ module Sentry
2
+ class BenchmarkTransport < Transport
3
+ attr_accessor :events
4
+
5
+ def initialize(*)
6
+ super
7
+ @events = []
8
+ end
9
+
10
+ def send_event(event)
11
+ @events << encode(event.to_hash)
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,25 @@
1
+ module Sentry
2
+ class Breadcrumb
3
+ attr_accessor :category, :data, :message, :level, :timestamp, :type
4
+
5
+ def initialize(category: nil, data: nil, message: nil, timestamp: nil, level: nil, type: nil)
6
+ @category = category
7
+ @data = data || {}
8
+ @level = level
9
+ @message = message
10
+ @timestamp = timestamp || Sentry.utc_now.to_i
11
+ @type = type
12
+ end
13
+
14
+ def to_hash
15
+ {
16
+ :category => @category,
17
+ :data => @data,
18
+ :level => @level,
19
+ :message => @message,
20
+ :timestamp => @timestamp,
21
+ :type => @type
22
+ }
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,87 @@
1
+ require 'logger'
2
+
3
+ module Sentry
4
+ class Breadcrumb
5
+ module SentryLogger
6
+ LEVELS = {
7
+ ::Logger::DEBUG => 'debug',
8
+ ::Logger::INFO => 'info',
9
+ ::Logger::WARN => 'warn',
10
+ ::Logger::ERROR => 'error',
11
+ ::Logger::FATAL => 'fatal'
12
+ }.freeze
13
+
14
+ def add(*args, &block)
15
+ super
16
+ add_breadcrumb(*args, &block)
17
+ end
18
+
19
+ def add_breadcrumb(severity, message = nil, progname = nil)
20
+ # because the breadcrumbs now belongs to different Hub's Scope in different threads
21
+ # we need to make sure the current thread's Hub has been set before adding breadcrumbs
22
+ return unless Sentry.get_current_hub
23
+
24
+ category = "logger"
25
+
26
+ # this is because the nature of Ruby Logger class:
27
+ #
28
+ # when given 1 argument, the argument will become both message and progname
29
+ #
30
+ # ```
31
+ # logger.info("foo")
32
+ # # message == progname == "foo"
33
+ # ```
34
+ #
35
+ # and to specify progname with a different message,
36
+ # we need to pass the progname as the argument and pass the message as a proc
37
+ #
38
+ # ```
39
+ # logger.info("progname") { "the message" }
40
+ # ```
41
+ #
42
+ # so the condition below is to replicate the similar behavior
43
+ if message.nil?
44
+ if block_given?
45
+ message = yield
46
+ category = progname
47
+ else
48
+ message = progname
49
+ end
50
+ end
51
+
52
+ return if ignored_logger?(progname) || message.empty?
53
+
54
+ # some loggers will add leading/trailing space as they (incorrectly, mind you)
55
+ # think of logging as a shortcut to std{out,err}
56
+ message = message.to_s.strip
57
+
58
+ last_crumb = current_breadcrumbs.peek
59
+ # try to avoid dupes from logger broadcasts
60
+ if last_crumb.nil? || last_crumb.message != message
61
+ level = Sentry::Breadcrumb::SentryLogger::LEVELS.fetch(severity, nil)
62
+ crumb = Sentry::Breadcrumb.new(
63
+ level: level,
64
+ category: category,
65
+ message: message,
66
+ type: severity >= 3 ? "error" : level
67
+ )
68
+
69
+ Sentry.add_breadcrumb(crumb)
70
+ end
71
+ end
72
+
73
+ private
74
+
75
+ def ignored_logger?(progname)
76
+ progname == LOGGER_PROGNAME ||
77
+ Sentry.configuration.exclude_loggers.include?(progname)
78
+ end
79
+
80
+ def current_breadcrumbs
81
+ Sentry.get_current_scope.breadcrumbs
82
+ end
83
+ end
84
+ end
85
+ end
86
+
87
+ ::Logger.send(:prepend, Sentry::Breadcrumb::SentryLogger)