Skip to content

Commit c2bff60

Browse files
authored
Add Integrable module (#1177)
* Add Sentry::Integrable module to simplify integration setup * Allow subclass to override middleware's exception capturing method * Use the new Integrable module in sentry-rails * Use the new Integrable module in sentry-sidekiq * Create EXTENSION.md
1 parent f3bc6a9 commit c2bff60

File tree

19 files changed

+214
-39
lines changed

19 files changed

+214
-39
lines changed

EXTENSION.md

+51
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
## What's an extension?
2+
3+
An extension is a gem built on top of the core Ruby SDK (`sentry-ruby`) that provides additional functionality or integration support to the users. For example, `sentry-rails` and `sentry-sidekiq` are SDK extensions that offer integration support with specific libraries.
4+
5+
## Sentry::Integrable
6+
7+
You can write extensions for `sentry-ruby` any way you want to. But if you're going to build an extension for integration support, `sentry-ruby` provides a module called `Sentry::Integrable` that will save you some work.
8+
9+
### Usage
10+
11+
Let me use `sentry-rails` as our example.
12+
13+
#### Register the extension
14+
15+
```ruby
16+
require "sentry-ruby"
17+
18+
# the integrable module needs to be required separately
19+
require "sentry/integrable"
20+
21+
module Sentry
22+
# the module/class of the extension should be defined under the Sentry namespace
23+
module Rails
24+
25+
# extend the module
26+
extend Integrable
27+
28+
# use the register_integration method to register your extension to the SDK core
29+
register_integration name: "rails", version: Sentry::Rails::VERSION
30+
end
31+
end
32+
```
33+
34+
Once the extension is registered, it will do 2 things for you:
35+
36+
1. It'll generate `.capture_exception` and `.capture_message` methods for your extension. In our example, they'll be `Sentry::Rails.capture_exception` and `Sentry::Rails.capture_message`.
37+
2. It'll also generate the SDK meta for the extension, which is `{name: "sentry.ruby.rails", version: Sentry::Rails::VERSION}` in this case.
38+
39+
#### Use the generated helpers
40+
41+
All the integration-level exception/message should be captured via the newly generated helpers in the extension gem. This is because:
42+
43+
- Those helpers will inject `{ integration: "integration_name" }` to the event hints. So you or the users can later identify each event's source in the `before_send` callback.
44+
- Events created from those helpers will have the integration meta as their SDK information.
45+
- In the future, we might also introduce more advanced integration-based features. And those features will rely on these helpers.
46+
47+
### Future plan
48+
49+
- Methods like `configure_integration` for generating integration-level config options, like `config.integration_name.option_name`.
50+
- Support integration-specific excluded exceptions list.
51+

sentry-rails/lib/sentry/rails.rb

+3-5
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,12 @@
11
require "sentry-ruby"
2+
require "sentry/integrable"
23
require "sentry/rails/configuration"
34
require "sentry/rails/railtie"
45
require "sentry/rails/tracing"
56

67
module Sentry
78
module Rails
8-
META = { "name" => "sentry.ruby.rails", "version" => Sentry::Rails::VERSION }.freeze
9-
end
10-
11-
def self.sdk_meta
12-
Sentry::Rails::META
9+
extend Integrable
10+
register_integration name: "rails", version: Sentry::Rails::VERSION
1311
end
1412
end

sentry-rails/lib/sentry/rails/active_job.rb

+1-1
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ def capture_and_reraise_with_sentry(job, block)
2626
rescue_handler_result = rescue_with_handler(e)
2727
return rescue_handler_result if rescue_handler_result
2828

29-
Sentry.capture_exception(e, :extra => sentry_context(job))
29+
Sentry::Rails.capture_exception(e, extra: sentry_context(job))
3030
raise e
3131
end
3232

Original file line numberDiff line numberDiff line change
@@ -1,13 +1,19 @@
11
module Sentry
22
module Rails
33
class CaptureExceptions < Sentry::Rack::CaptureExceptions
4+
private
5+
46
def collect_exception(env)
57
super || env["action_dispatch.exception"] || env["sentry.rescued_exception"]
68
end
79

810
def transaction_op
911
"rails.request".freeze
1012
end
13+
14+
def capture_exception(exception)
15+
Sentry::Rails.capture_exception(exception)
16+
end
1117
end
1218
end
1319
end

sentry-rails/lib/sentry/rails/controller_methods.rb

+2-2
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,13 @@ module Rails
33
module ControllerMethods
44
def capture_message(message, options = {})
55
with_request_scope do
6-
Sentry.capture_message(message, **options)
6+
Sentry::Rails.capture_message(message, **options)
77
end
88
end
99

1010
def capture_exception(exception, options = {})
1111
with_request_scope do
12-
Sentry.capture_exception(exception, **options)
12+
Sentry::Rails.capture_exception(exception, **options)
1313
end
1414
end
1515

sentry-rails/lib/sentry/rails/overrides/streaming_reporter.rb

+2-2
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ module Rails
33
module Overrides
44
module StreamingReporter
55
def log_error(exception)
6-
Sentry.capture_exception(exception)
6+
Sentry::Rails.capture_exception(exception)
77
super
88
end
99
end
@@ -14,7 +14,7 @@ def self.included(base)
1414
end
1515

1616
def log_error_with_raven(exception)
17-
Sentry.capture_exception(exception)
17+
Sentry::Rails.capture_exception(exception)
1818
log_error_without_raven(exception)
1919
end
2020
end

sentry-rails/spec/sentry/rails/event_spec.rb

+3-3
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,9 @@
1010
end
1111

1212
it "sets right SDK information" do
13-
event_hash = Sentry.capture_message("foo").to_hash
13+
event_hash = Sentry::Rails.capture_message("foo").to_hash
1414

15-
expect(event_hash[:sdk]).to eq("name" => "sentry.ruby.rails", "version" => Sentry::Rails::VERSION)
15+
expect(event_hash[:sdk]).to eq(name: "sentry.ruby.rails", version: Sentry::Rails::VERSION)
1616
end
1717

1818
context 'with an application stacktrace' do
@@ -29,7 +29,7 @@
2929
e
3030
end
3131

32-
let(:hash) { Sentry.capture_exception(exception).to_hash }
32+
let(:hash) { Sentry::Rails.capture_exception(exception).to_hash }
3333

3434
it 'marks in_app correctly' do
3535
frames = hash[:exception][:values][0][:stacktrace][:frames]

sentry-rails/spec/sentry/rails_spec.rb

+1
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@
8787

8888
expect(event["exception"]["values"][0]["type"]).to eq("RuntimeError")
8989
expect(event["exception"]["values"][0]["value"]).to eq("An unhandled exception!")
90+
expect(event["sdk"]).to eq("name" => "sentry.ruby.rails", "version" => Sentry::Rails::VERSION)
9091
end
9192

9293
it "filters exception backtrace with custom BacktraceCleaner" do

sentry-ruby/lib/sentry-ruby.rb

+11
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,17 @@ def self.utc_now
4040
Time.now.utc
4141
end
4242

43+
class << self
44+
def integrations
45+
@integrations ||= {}
46+
end
47+
48+
def register_integration(name, version)
49+
meta = { name: "sentry.ruby.#{name}", version: version }.freeze
50+
integrations[name.to_s] = meta
51+
end
52+
end
53+
4354
class << self
4455
extend Forwardable
4556

sentry-ruby/lib/sentry/client.rb

+6-4
Original file line numberDiff line numberDiff line change
@@ -47,16 +47,18 @@ def capture_event(event, scope, hint = {})
4747
event
4848
end
4949

50-
def event_from_exception(exception)
50+
def event_from_exception(exception, hint = {})
51+
integration_meta = Sentry.integrations[hint[:integration]]
5152
return unless @configuration.exception_class_allowed?(exception)
5253

53-
Event.new(configuration: configuration).tap do |event|
54+
Event.new(configuration: configuration, integration_meta: integration_meta).tap do |event|
5455
event.add_exception_interface(exception)
5556
end
5657
end
5758

58-
def event_from_message(message)
59-
Event.new(configuration: configuration, message: message)
59+
def event_from_message(message, hint = {})
60+
integration_meta = Sentry.integrations[hint[:integration]]
61+
Event.new(configuration: configuration, integration_meta: integration_meta, message: message)
6062
end
6163

6264
def event_from_transaction(transaction)

sentry-ruby/lib/sentry/event.rb

+2-2
Original file line numberDiff line numberDiff line change
@@ -20,15 +20,15 @@ class Event
2020
attr_accessor(*ATTRIBUTES)
2121
attr_reader :configuration, :request, :exception, :stacktrace
2222

23-
def initialize(configuration:, message: nil)
23+
def initialize(configuration:, integration_meta: nil, message: nil)
2424
# this needs to go first because some setters rely on configuration
2525
@configuration = configuration
2626

2727
# Set some simple default values
2828
@event_id = SecureRandom.uuid.delete("-")
2929
@timestamp = Sentry.utc_now.iso8601
3030
@platform = :ruby
31-
@sdk = Sentry.sdk_meta
31+
@sdk = integration_meta || Sentry.sdk_meta
3232

3333
@user = {}
3434
@extra = {}

sentry-ruby/lib/sentry/hub.rb

+5-5
Original file line numberDiff line numberDiff line change
@@ -76,21 +76,21 @@ def start_transaction(transaction: nil, **options)
7676
def capture_exception(exception, **options, &block)
7777
return unless current_client
7878

79-
event = current_client.event_from_exception(exception)
79+
options[:hint] ||= {}
80+
options[:hint][:exception] = exception
81+
event = current_client.event_from_exception(exception, options[:hint])
8082

8183
return unless event
8284

83-
options[:hint] ||= {}
84-
options[:hint] = options[:hint].merge(exception: exception)
8585
capture_event(event, **options, &block)
8686
end
8787

8888
def capture_message(message, **options, &block)
8989
return unless current_client
9090

9191
options[:hint] ||= {}
92-
options[:hint] = options[:hint].merge(message: message)
93-
event = current_client.event_from_message(message)
92+
options[:hint][:message] = message
93+
event = current_client.event_from_message(message, options[:hint])
9494
capture_event(event, **options, &block)
9595
end
9696

sentry-ruby/lib/sentry/integrable.rb

+24
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
module Sentry
2+
module Integrable
3+
def register_integration(name:, version:)
4+
Sentry.register_integration(name, version)
5+
@integration_name = name
6+
end
7+
8+
def integration_name
9+
@integration_name
10+
end
11+
12+
def capture_exception(exception, **options, &block)
13+
options[:hint] ||= {}
14+
options[:hint][:integration] = integration_name
15+
Sentry.capture_exception(exception, **options, &block)
16+
end
17+
18+
def capture_message(message, **options, &block)
19+
options[:hint] ||= {}
20+
options[:hint][:integration] = integration_name
21+
Sentry.capture_message(message, **options, &block)
22+
end
23+
end
24+
end

sentry-ruby/lib/sentry/rack/capture_exceptions.rb

+6-2
Original file line numberDiff line numberDiff line change
@@ -25,13 +25,13 @@ def call(env)
2525
finish_span(span, 500)
2626
raise # Don't capture Sentry errors
2727
rescue Exception => e
28-
Sentry.capture_exception(e)
28+
capture_exception(e)
2929
finish_span(span, 500)
3030
raise
3131
end
3232

3333
exception = collect_exception(env)
34-
Sentry.capture_exception(exception) if exception
34+
capture_exception(exception) if exception
3535

3636
finish_span(span, response[0])
3737

@@ -49,6 +49,10 @@ def transaction_op
4949
"rack.request".freeze
5050
end
5151

52+
def capture_exception(exception)
53+
Sentry.capture_exception(exception)
54+
end
55+
5256
def finish_span(span, status_code)
5357
span.set_http_status(status_code)
5458
span.finish
+63
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
require "spec_helper"
2+
require "sentry/integrable"
3+
4+
RSpec.describe Sentry::Integrable do
5+
module Sentry
6+
module FakeIntegration
7+
extend Sentry::Integrable
8+
9+
register_integration name: "fake_integration", version: "0.1.0"
10+
end
11+
end
12+
13+
it "registers correct meta" do
14+
meta = Sentry.integrations["fake_integration"]
15+
16+
expect(meta).to eq({ name: "sentry.ruby.fake_integration", version: "0.1.0" })
17+
end
18+
19+
describe "helpers generation" do
20+
before do
21+
perform_basic_setup
22+
end
23+
24+
let(:exception) { ZeroDivisionError.new("1/0") }
25+
let(:message) { "test message" }
26+
27+
it "generates Sentry::FakeIntegration.capture_exception" do
28+
hint = nil
29+
30+
Sentry.configuration.before_send = lambda do |event, h|
31+
hint = h
32+
event
33+
end
34+
35+
Sentry::FakeIntegration.capture_exception(exception, hint: { additional_hint: "foo" })
36+
37+
expect(hint).to eq({ additional_hint: "foo", integration: "fake_integration", exception: exception })
38+
end
39+
40+
it "generates Sentry::FakeIntegration.capture_exception" do
41+
hint = nil
42+
43+
Sentry.configuration.before_send = lambda do |event, h|
44+
hint = h
45+
event
46+
end
47+
48+
Sentry::FakeIntegration.capture_message(message, hint: { additional_hint: "foo" })
49+
50+
expect(hint).to eq({ additional_hint: "foo", integration: "fake_integration", message: message })
51+
end
52+
53+
it "sets correct meta when the event is captured by integration helpers" do
54+
event = Sentry::FakeIntegration.capture_message(message)
55+
expect(event.sdk).to eq({ name: "sentry.ruby.fake_integration", version: "0.1.0" })
56+
end
57+
58+
it "doesn't change the events captured by original helpers" do
59+
event = Sentry.capture_message(message)
60+
expect(event.sdk).to eq(Sentry.sdk_meta)
61+
end
62+
end
63+
end

sentry-sidekiq/lib/sentry-sidekiq.rb

+3-4
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,16 @@
11
require "sidekiq"
22
require "sentry-ruby"
3+
require "sentry/integrable"
34
require "sentry/sidekiq/version"
45
require "sentry/sidekiq/error_handler"
56
require "sentry/sidekiq/sentry_context_middleware"
67
# require "sentry/sidekiq/configuration"
78

89
module Sentry
910
module Sidekiq
10-
META = { "name" => "sentry.ruby.sidekiq", "version" => Sentry::Sidekiq::VERSION }.freeze
11-
end
11+
extend Sentry::Integrable
1212

13-
def self.sdk_meta
14-
Sentry::Sidekiq::META
13+
register_integration name: "sidekiq", version: Sentry::Sidekiq::VERSION
1514
end
1615
end
1716

0 commit comments

Comments
 (0)