Tech: Rails
Problem: you have a self-hosted Sentry server behind a firewall and you want to report your frontend errors.
One way to accomplish it is by modifying Sentry dsn to send it to your backend and then proxying them to the Sentry server.
First, let’s set up a new route:
post 'frontend_errors/api/:project_id/store', to: 'frontend_errors#create'
It has to follow a specific pattern to work with the Sentry frontend library. The only thing you can change in the above is frontend_errors
– pick whatever name you want. The code above will expect you to have a FrontendErrorsController.
Now, the FrontEndErrorsController needs to redirect to your actual Sentry server in the format that Sentry expects. Let’s create a new class to handle it:
class SentryProxy
# This could be different based on your Sentry version.
# Look into raven-sentry gem codebase if this doesn't work
# Look for http_transport.rb files - https://github.com/getsentry/sentry-ruby/blob/f6625bd12fa5ef86e4ce6a1515e8a8171cea9ece/sentry-ruby/lib/sentry/transport/http_transport.rb
PROTOCOL_VERSION = '5'
USER_AGENT = "raven-ruby/#{Raven::VERSION}"
def initialize(body:, sentry_dsn:)
@body = body
@sentry_dsn = sentry_dsn
end
def post_to_sentry
return if @sentry_dsn.blank?
sentry_connection.post do |faraday|
faraday.body = @body
end
end
private
def sentry_connection
Faraday.new(url: sentry_post_url) do |faraday|
faraday.headers['X-Sentry-Auth'] = generate_auth_header
faraday.headers[:user_agent] = "sentry-ruby/#{Raven::VERSION}"
faraday.adapter(Faraday.default_adapter)
end
end
def sentry_post_url
key, url = @sentry_dsn.split('@')
path, project_id = url.split('/')
http_prefix, _keys = key.split('//')
"#{http_prefix}//#{path}/api/#{project_id}/store/"
end
def generate_auth_header
now = Time.now.to_i.to_s
public_key, secret_key = @sentry_dsn.split('//').second.split('@').first.split(':')
fields = {
'sentry_version' => PROTOCOL_VERSION,
'sentry_client' => USER_AGENT,
'sentry_timestamp' => now,
'sentry_key' => public_key,
'sentry_secret' => secret_key
}
'Sentry ' + fields.map { |key, value| "#{key}=#{value}" }.join(', ')
end
end
Now in your controller you can call it like this (assumes you can get your sentry_dsn on the backend):
def create
SentryProxy.new(body: request.body.read, sentry_dsn: sentry_dsn).post_to_sentry
head(:no_content)
end
And to make sure your frontend is properly configured, first import Sentry frontend libraries, then initialize them using:
Sentry.init({
dsn: `${window.location.protocol}//[email protected]${window.location.host}/frontend_errors/0`});
public_key
is supposed to be… your public key. You have to supply it in the dsn even if you’re getting the dsn key on the backend, otherwise, the Sentry frontend library will throw errors. 0 is the project id – the same idea, you have to supply it for the Sentry frontend to properly parse it. It doesn’t have to be real, as we’re reconstructing the Sentry url on the backend, and you can get proper keys/project id on the backend.
This should do it. Now you can configure Sentry frontend library to capture all errors, capture specific exceptions or messages.