Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

### Features

- You can now use [Spotlight](https://spotlightjs.com) with your apps that use sentry-ruby! [#2175](https://github.com/getsentry/sentry-ruby/pulls/2175)
- Improve default slug generation for `sidekiq-scheduler` [#2184](https://github.com/getsentry/sentry-ruby/pull/2184)

### Bug Fixes
Expand Down
7 changes: 7 additions & 0 deletions sentry-ruby/lib/sentry/client.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ class Client
# @return [Transport]
attr_reader :transport

# The Transport object that'll send events for the client.
# @return [SpotlightTransport, nil]
attr_reader :spotlight_transport

# @!macro configuration
attr_reader :configuration

Expand All @@ -32,6 +36,8 @@ def initialize(configuration)
DummyTransport.new(configuration)
end
end

@spotlight_transport = SpotlightTransport.new(configuration) if configuration.spotlight
end

# Applies the given scope's data to the event and sends it to Sentry.
Expand Down Expand Up @@ -167,6 +173,7 @@ def send_event(event, hint = nil)
end

transport.send_event(event)
spotlight_transport&.send_event(event)

event
rescue => e
Expand Down
11 changes: 10 additions & 1 deletion sentry-ruby/lib/sentry/configuration.rb
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,14 @@ class Configuration
# @return [Boolean]
attr_accessor :include_local_variables

# Whether to capture events and traces into Spotlight. Default is false.
# If you set this to true, Sentry will send events and traces to the local
# Sidecar proxy at http://localhost:8969/stream.
# If you want to use a different Sidecar proxy address, set this to String
# with the proxy URL.
# @return [Boolean, String]
attr_accessor :spotlight

# @deprecated Use {#include_local_variables} instead.
alias_method :capture_exception_frame_locals, :include_local_variables

Expand Down Expand Up @@ -344,6 +352,7 @@ def initialize
self.auto_session_tracking = true
self.trusted_proxies = []
self.dsn = ENV['SENTRY_DSN']
self.spotlight = false
self.server_name = server_name_from_env
self.instrumenter = :sentry
self.trace_propagation_targets = [PROPAGATION_TARGETS_MATCH_ALL]
Expand Down Expand Up @@ -451,7 +460,7 @@ def profiles_sample_rate=(profiles_sample_rate)
def sending_allowed?
@errors = []

valid? && capture_in_environment?
spotlight || (valid? && capture_in_environment?)
end

def sample_allowed?
Expand Down
13 changes: 1 addition & 12 deletions sentry-ruby/lib/sentry/transport.rb
Original file line number Diff line number Diff line change
Expand Up @@ -119,18 +119,6 @@ def is_rate_limited?(item_type)
!!delay && delay > Time.now
end

def generate_auth_header
now = Sentry.utc_now.to_i
fields = {
'sentry_version' => PROTOCOL_VERSION,
'sentry_client' => USER_AGENT,
'sentry_timestamp' => now,
'sentry_key' => @dsn.public_key
}
fields['sentry_secret'] = @dsn.secret_key if @dsn.secret_key
'Sentry ' + fields.map { |key, value| "#{key}=#{value}" }.join(', ')
end

def envelope_from_event(event)
# Convert to hash
event_payload = event.to_hash
Expand Down Expand Up @@ -220,3 +208,4 @@ def reject_rate_limited_items(envelope)

require "sentry/transport/dummy_transport"
require "sentry/transport/http_transport"
require "sentry/transport/spotlight_transport"
79 changes: 49 additions & 30 deletions sentry-ruby/lib/sentry/transport/http_transport.rb
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,7 @@ class HTTPTransport < Transport

def initialize(*args)
super
@endpoint = @dsn.envelope_endpoint

log_debug("Sentry HTTP Transport will connect to #{@dsn.server}")
log_debug("Sentry HTTP Transport will connect to #{@dsn.server}") if @dsn
end

def send_data(data)
Expand All @@ -42,12 +40,14 @@ def send_data(data)
headers = {
'Content-Type' => CONTENT_TYPE,
'Content-Encoding' => encoding,
'X-Sentry-Auth' => generate_auth_header,
'User-Agent' => USER_AGENT
}

auth_header = generate_auth_header
headers['X-Sentry-Auth'] = auth_header if auth_header

response = conn.start do |http|
request = ::Net::HTTP::Post.new(@endpoint, headers)
request = ::Net::HTTP::Post.new(endpoint, headers)
request.body = data
http.request(request)
end
Expand All @@ -69,9 +69,53 @@ def send_data(data)
raise Sentry::ExternalError, error_info
end
rescue SocketError, *HTTP_ERRORS => e
on_error if respond_to?(:on_error)
raise Sentry::ExternalError.new(e&.message)
end

def endpoint
@dsn.envelope_endpoint
end

def generate_auth_header
return nil unless @dsn

now = Sentry.utc_now.to_i
fields = {
'sentry_version' => PROTOCOL_VERSION,
'sentry_client' => USER_AGENT,
'sentry_timestamp' => now,
'sentry_key' => @dsn.public_key
}
fields['sentry_secret'] = @dsn.secret_key if @dsn.secret_key
'Sentry ' + fields.map { |key, value| "#{key}=#{value}" }.join(', ')
end

def conn
server = URI(@dsn.server)

# connection respects proxy setting from @transport_configuration, or environment variables (HTTP_PROXY, HTTPS_PROXY, NO_PROXY)
# Net::HTTP will automatically read the env vars.
# See https://ruby-doc.org/3.2.2/stdlibs/net/Net/HTTP.html#class-Net::HTTP-label-Proxies
connection =
if proxy = normalize_proxy(@transport_configuration.proxy)
::Net::HTTP.new(server.hostname, server.port, proxy[:uri].hostname, proxy[:uri].port, proxy[:user], proxy[:password])
else
::Net::HTTP.new(server.hostname, server.port)
end

connection.use_ssl = server.scheme == "https"
connection.read_timeout = @transport_configuration.timeout
connection.write_timeout = @transport_configuration.timeout if connection.respond_to?(:write_timeout)
connection.open_timeout = @transport_configuration.open_timeout

ssl_configuration.each do |key, value|
connection.send("#{key}=", value)
end

connection
end

private

def has_rate_limited_header?(headers)
Expand Down Expand Up @@ -136,31 +180,6 @@ def should_compress?(data)
@transport_configuration.encoding == GZIP_ENCODING && data.bytesize >= GZIP_THRESHOLD
end

def conn
server = URI(@dsn.server)

# connection respects proxy setting from @transport_configuration, or environment variables (HTTP_PROXY, HTTPS_PROXY, NO_PROXY)
# Net::HTTP will automatically read the env vars.
# See https://ruby-doc.org/3.2.2/stdlibs/net/Net/HTTP.html#class-Net::HTTP-label-Proxies
connection =
if proxy = normalize_proxy(@transport_configuration.proxy)
::Net::HTTP.new(server.hostname, server.port, proxy[:uri].hostname, proxy[:uri].port, proxy[:user], proxy[:password])
else
::Net::HTTP.new(server.hostname, server.port)
end

connection.use_ssl = server.scheme == "https"
connection.read_timeout = @transport_configuration.timeout
connection.write_timeout = @transport_configuration.timeout if connection.respond_to?(:write_timeout)
connection.open_timeout = @transport_configuration.open_timeout

ssl_configuration.each do |key, value|
connection.send("#{key}=", value)
end

connection
end

# @param proxy [String, URI, Hash] Proxy config value passed into `config.transport`.
# Accepts either a URI formatted string, URI, or a hash with the `uri`, `user`, and `password` keys.
# @return [Hash] Normalized proxy config that will be passed into `Net::HTTP`
Expand Down
50 changes: 50 additions & 0 deletions sentry-ruby/lib/sentry/transport/spotlight_transport.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# frozen_string_literal: true

require "net/http"
require "zlib"

module Sentry
# Designed to just report events to Spotlight in development.
class SpotlightTransport < HTTPTransport
DEFAULT_SIDECAR_URL = "http://localhost:8969/stream"
MAX_FAILED_REQUESTS = 3

def initialize(configuration)
super
@sidecar_url = configuration.spotlight.is_a?(String) ? configuration.spotlight : DEFAULT_SIDECAR_URL
@failed = 0
@logged = false

log_debug("[Spotlight] initialized for url #{@sidecar_url}")
end

def endpoint
"/stream"
end

def send_data(data)
if @failed >= MAX_FAILED_REQUESTS
unless @logged
log_debug("[Spotlight] disabling because of too many request failures")
@logged = true
end

return
end

super
end

def on_error
@failed += 1
end

# Similar to HTTPTransport connection, but does not support Proxy and SSL
def conn
sidecar = URI(@sidecar_url)
connection = ::Net::HTTP.new(sidecar.hostname, sidecar.port, nil)
connection.use_ssl = false
connection
end
end
end
28 changes: 28 additions & 0 deletions sentry-ruby/spec/sentry/client_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,34 @@ def sentry_context
end
end

describe "#spotlight_transport" do
it "nil by default" do
expect(subject.spotlight_transport).to eq(nil)
end

it "nil when false" do
configuration.spotlight = false
expect(subject.spotlight_transport).to eq(nil)
end

it "has a transport when true" do
configuration.spotlight = true
expect(described_class.new(configuration).spotlight_transport).to be_a(Sentry::SpotlightTransport)
end
end

describe "#send_event" do
context "with spotlight enabled" do
before { configuration.spotlight = true }

it "calls spotlight transport" do
event = subject.event_from_message('test')
expect(subject.spotlight_transport).to receive(:send_event).with(event)
subject.send_event(event)
end
end
end

describe '#event_from_message' do
let(:message) { 'This is a message' }

Expand Down
13 changes: 13 additions & 0 deletions sentry-ruby/spec/sentry/configuration_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,19 @@
end
end

describe "#spotlight" do
it "false by default" do
expect(subject.spotlight).to eq(false)
end
end

describe "#sending_allowed?" do
it "true when spotlight" do
subject.spotlight = true
expect(subject.sending_allowed?).to eq(true)
end
end

context 'configuring for async' do
it 'should be configurable to send events async' do
subject.async = ->(_e) { :ok }
Expand Down
39 changes: 36 additions & 3 deletions sentry-ruby/spec/sentry/transport/http_transport_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
end
let(:client) { Sentry::Client.new(configuration) }
let(:event) { client.event_from_message("foobarbaz") }
let(:fake_time) { Time.now }
let(:data) do
subject.serialize_envelope(subject.envelope_from_event(event.to_hash)).first
end
Expand Down Expand Up @@ -132,7 +133,7 @@
it "accepts a proxy from ENV[HTTP_PROXY]" do
begin
ENV["http_proxy"] = "https://stan:[email protected]:8080"

stub_request(fake_response) do |_, http_obj|
expect(http_obj.proxy_address).to eq("example.com")
expect(http_obj.proxy_port).to eq(8080)
Expand All @@ -142,7 +143,7 @@
expect(http_obj.proxy_pass).to eq("foobar")
end
end

subject.send_data(data)
ensure
ENV["http_proxy"] = nil
Expand Down Expand Up @@ -277,7 +278,7 @@
allow(::Net::HTTP).to receive(:new).and_raise(SocketError.new("socket error"))
expect do
subject.send_data(data)
end.to raise_error(Sentry::ExternalError)
end.to raise_error(Sentry::ExternalError)
end

it "reports other errors to Sentry if they are not recognized" do
Expand Down Expand Up @@ -321,4 +322,36 @@
end
end
end

describe "#generate_auth_header" do
it "generates an auth header" do
expect(subject.send(:generate_auth_header)).to eq(
"Sentry sentry_version=7, sentry_client=sentry-ruby/#{Sentry::VERSION}, sentry_timestamp=#{fake_time.to_i}, " \
"sentry_key=12345, sentry_secret=67890"
)
end

it "generates an auth header without a secret (Sentry 9)" do
configuration.server = "https://[email protected]/42"

expect(subject.send(:generate_auth_header)).to eq(
"Sentry sentry_version=7, sentry_client=sentry-ruby/#{Sentry::VERSION}, sentry_timestamp=#{fake_time.to_i}, " \
"sentry_key=66260460f09b5940498e24bb7ce093a0"
)
end
end

describe "#endpoint" do
it "returns correct endpoint" do
expect(subject.endpoint).to eq("/sentry/api/42/envelope/")
end
end

describe "#conn" do
it "returns a connection" do
expect(subject.conn).to be_a(Net::HTTP)
expect(subject.conn.address).to eq("sentry.localdomain")
expect(subject.conn.use_ssl?).to eq(false)
end
end
end
Loading