Skip to content

Instantly share code, notes, and snippets.

@stayallive
Last active November 8, 2023 16:57
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save stayallive/ca548d8728cfbd148931f28e9f56bbe5 to your computer and use it in GitHub Desktop.
Save stayallive/ca548d8728cfbd148931f28e9f56bbe5 to your computer and use it in GitHub Desktop.
Cloudflare worker Sentry tunnel

Tunnel events through a Cloudflare Worker

This example shows how you can use a Cloudflare Worker to proxy events to Sentry from any SDK but most useful for the tunnel option in the JavaScript SDK.

To deploy this example:

  1. Create a new Cloudflare Worker from your Cloudflare dashboard
  2. Replace the example worker code with the contents of the worker.js file
  3. Update the SENTRY_DSN_WHITELIST and/or SENTRY_HOST_WHITELIST with all the DSN's or hosts that are allowed to be proxied
  4. (optional) Update PROXY_PATHNAME with the path you want the proxy to operate on, be careful to not use "sentry" or another keyword here that might trigger an adblocker, keep it simple
  5. (optional) Update ENABLE_CORS to false if you are running the worker on the same domain as the application talking to Sentry (best to leave it on though if you doubt)
  6. Set the tunnel configuration option to your Workers endpoint, for example https://example.com/ingest or my personal favorite https://visibility.on.example.com/ingest

Note: if you intend to use a subdomain, be careful to not use sentry or other interesting keywords in the worker name, otherwise the Worker might still be blocked by adblockers. Best is to setup a Worker route on your own domain.

// This is the path where your worker is going to be deployed to
const PROXY_PATHNAME = '/ingest';
// These are the DSN that are allowed to use this proxy endpoint
const SENTRY_DSN_WHITELIST = [
// 'https://publicKey@o1234567.ingest.sentry.io/1234567',
];
// These ingest domains are allowed to use this proxy endpoint
const SENTRY_HOST_WHITELIST = [
// 'o1234567.ingest.sentry.io',
];
// When enabled CORS headers are added with all origins allowed
const ENABLE_CORS = true;
function isDSNAllowed(dsn) {
// Check if the full DSN is whitelisted
if (SENTRY_DSN_WHITELIST.includes(dsn)) {
return true;
}
// Micro-optimization, if the host whitelist is empty we know the DSN is not allowed
if (SENTRY_HOST_WHITELIST.length === 0) {
return false;
}
const parsed = new URL(dsn);
// Check if the DSN host is whitelisted
if (SENTRY_HOST_WHITELIST.includes(parsed.host)) {
return true;
}
return false;
}
export default {
async fetch(request, env) {
const {
origin,
pathname
} = new URL(request.url);
if (request.method === 'POST' && pathname === PROXY_PATHNAME) {
// Handle the OPTIONS request if CORS is enabled
if (ENABLE_CORS && request.method === 'OPTIONS') {
return handleOptions(request);
}
// Only POST requests are valid
if (request.method !== 'POST') {
return new Response('Method Not Allowed', {
status: 405
});
}
// Get the payload from the request as plain text
const payload = await request.clone().text();
// The Sentry payload is split in JSON blobs seperated by newlines
const packets = payload.split('\n');
// We would expect to see at least 2 packets (the header + N extra packets)
if (packets.length < 2) {
return new Response('Bad Request', {
status: 400
});
}
// The first packet is always the header which contains the Sentry DSN
const header = JSON.parse(packets[0]);
// Make sure we have a DSN and it's one that is whitelisted
if (header.dsn && !isDSNAllowed(header.dsn)) {
return new Response('Forbidden', {
status: 403
});
}
// Parse the Sentry DSN as an URL so we can extract the parts we need
const dsn = new URL(header.dsn);
// The project ID is the first part of the Sentry DSN pathname
const projectId = dsn.pathname.substr(1).split('/')[0];
// The API URL can be constructed from the Sentry DSN host and project ID
const apiUrl = `https://${dsn.host}/api/${projectId}/envelope/`;
// Build the Sentry API request
const sentryRequest = new Request(apiUrl, {
body: request.body,
method: 'POST',
headers: {
'Content-Type': 'application/x-sentry-envelope',
},
});
// Return with the response from the Sentry API
const sentryResponse = await fetch(sentryRequest);
if (!ENABLE_CORS) {
return sentryResponse;
}
// Recreate the response so the headers can be modified
const response = new Response(sentryResponse.body, sentryResponse);
// Set CORS headers
response.headers.set('Access-Control-Allow-Origin', corsHeaders['Access-Control-Allow-Origin']);
// Append the Vary header so browsers will cache the response correctly
response.headers.append('Vary', 'Origin');
return response;
}
return new Response('Not Found', {
status: 404
});
}
}
const corsHeaders = {
'Access-Control-Max-Age': '86400',
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'POST,OPTIONS',
};
function handleOptions(request) {
// Make sure the necessary headers are present
// for this to be a valid pre-flight request
let headers = request.headers;
if (
headers.get('Origin') !== null &&
headers.get('Access-Control-Request-Method') !== null &&
headers.get('Access-Control-Request-Headers') !== null
) {
// Handle CORS pre-flight request.
// If you want to check or reject the requested method + headers
// you can do that here.
let respHeaders = {
...corsHeaders,
// Allow all future content Request headers to go back to browser
// such as Authorization (Bearer) or X-Client-Name-Version
'Access-Control-Allow-Headers': request.headers.get('Access-Control-Request-Headers'),
};
return new Response(null, {
headers: respHeaders,
});
} else {
// Handle standard OPTIONS request.
// If you want to allow other HTTP Methods, you can do that here.
return new Response(null, {
headers: {
Allow: corsHeaders['Access-Control-Allow-Methods'],
},
});
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment