- Add the
worker.js
code to a new Cloudflare Worker - Set up a worker for your domain than responds to
/tunnel/*
and point it to your new worker - Add the Sentry script to your html but replace
https://browser.sentry-cdn.com/
with./tunnel/
- Eg.
<script src="./tunnel/6.9.0/bundle.min.js"></script>
- Eg.
init
Sentry with thetunnel
option set to/tunnel/
- Eg.
Sentry.init({ dsn: "__DSN__", tunnel: "/tunnel/" })
- Eg.
- Rejoice at how everything now works with ad blockers
-
-
Save timfish/a69dd7457b8d6d97c0a8018675be6c23 to your computer and use it in GitHub Desktop.
const SLUG = '/tunnel/'; | |
addEventListener('fetch', event => { | |
event.respondWith(handleRequest(event.request)) | |
}) | |
async function handleRequest(request) { | |
const url = new URL(request.url) | |
// Handle requests for Sentry CDN JavaScript | |
if (request.method === 'GET' && url.pathname.startsWith(SLUG) && (url.pathname.endsWith('.js') || url.pathname.endsWith('.js.map'))) { | |
const path = url.pathname.slice(SLUG.length); | |
// Fetch and pass the same response, including headers | |
return fetch(`https://browser.sentry-cdn.com/${path}`); | |
} | |
if (request.method === 'POST' && url.pathname === SLUG) { | |
let { readable, writable } = new TransformStream() | |
request.body.pipeTo(writable); | |
// We tee the stream so we can pull the header out of one stream | |
// and pass the other straight as the fetch POST body | |
const [header, body] = readable.tee(); | |
let decoder = new TextDecoder() | |
let chunk = ''; | |
const headerReader = header.getReader(); | |
while (true) { | |
const { done, value } = await headerReader.read(); | |
if (done) { | |
break; | |
} | |
chunk += decoder.decode(value); | |
const index = chunk.indexOf('\n'); | |
if (index >= 0) { | |
// Get the first line | |
const firstLine = chunk.slice(0, index); | |
const event = JSON.parse(firstLine); | |
const dsn = new URL(event.dsn); | |
// Pass through the user IP address | |
const headers = request.headers | |
headers.set('X-Forwarded-For', request.headers.get('CF-Connecting-IP')) // enhance the original headers | |
// Post to the Sentry endpoint! | |
return fetch(`https://${dsn.host}/api${dsn.pathname}/envelope/`, { | |
method: 'POST', | |
body, | |
headers, | |
}) | |
} | |
} | |
} | |
// If the above routes don't match, return 404 | |
return new Response(null, { status: 404 }); | |
} |
X-Forwaded-For
should work getsentry/sentry-javascript#5798 (comment)
Let me try this out myself and get back to you!
I thought so too, but apparently that doesn't work. I'm in contact with Sentry support, and they're looking into it. For now I've been able to hack around it by following the approach here: https://forum.sentry.io/t/real-client-ip-with-sentry-nextjs-tunnel/15438 (page does not currently open in some browsers, FWIW). Basically, I'm hacking in forwarded_for
into the first line of the body, using the cf-connecting-ip
header. It's the relay
that sentry is running for injest, and this is how the person in the forum realized it could work: https://github.com/getsentry/relay/blob/2e924639d7bcfa24db69ba2ed78a82e2c07478e1/relay-server/src/envelope.rs#L1144. So far it does the trick, but hopefully Sentry will start to just respect the header so we don't have to muck around with the body.
It's apparently a bug in Sentry: getsentry/relay#2450
Nice! What changes do I need to make to this gist to support X-Forwaded-For
?
I was thinking of putting into a repo with a "Deploy to Cloudflare" button which automates the setup
@timfish I think you could replace line 47 with
const headers = request.headers
headers.set('X-Forwarded-For', request.headers.get('CF-Connecting-IP')) // enhance the original headers
// Post to the Sentry endpoint!
return fetch(`https://${dsn.host}/api${dsn.pathname}/envelope/`, {
method: 'POST',
body,
headers,
})
Updated, thanks!
The request.body.pipeTo(writable);
code seemed invalid to me as it is an asynchronous function, but after adding await
, the solution stopped working for me in one of my projects, for no apparent reason.
After replacing the code:
let { readable, writable } = new TransformStream()
request.body.pipeTo(writable);
// We tee the stream so we can pull the header out of one stream
// and pass the other straight as the fetch POST body
const [header, body] = readable.tee();
as follows:
// We tee the stream so we can pull the header out of one stream
// and pass the other straight as the fetch POST body
const [header, body] = request.body.tee();
everything was back to normal.
My full code only supports POST
because I'm just tunneling the request from Sentry.
export async function onRequestPost({request}) {
const [header, body] = request.body.tee();
let decoder = new TextDecoder();
let chunk = '';
const headerReader = header.getReader();
while (true) {
const {done, value} = await headerReader.read();
if (done) break;
chunk += decoder.decode(value);
const index = chunk.indexOf('\n');
if (index >= 0) {
const line = chunk.slice(0, index);
const dsn = new URL(JSON.parse(line).dsn);
const headers = request.headers;
headers.set('X-Forwarded-For', request.headers.get('CF-Connecting-IP'));
return fetch(`https://${dsn.host}/api${dsn.pathname}/envelope/`, {
method: 'POST', body, headers,
});
}
}
return new Response(null, {status: 404});
}
This is great, but I'm finding that the IP address reported to Sentry is that of cloudflare, rather than the real user's IP address. Have you found a way around that? I tried copying the headers from the request, and adding a
x-forwarded-for
header, with no luck so far (though maybe it's because the IP cloudflare is getting is an IPv6 address...).