Repository navigation
Conversation
af27973 to
a2af1e1
Compare
c544ac0 to
89a5b2c
Compare
size-limit report 📦Compared: e7eedd7 (PR test merge using develop at d4a4e81) Against: ca4fc32 (develop) · base run #62950
|
3bba67e to
4eea4c9
Compare
|
bugbot run |
|
bugbot run |
1 similar comment
|
bugbot run |
a9a04bc to
d53b981
Compare
8e34dd6 to
f97d8a2
Compare
…ase path the same way
… base path after `init`
…elteKit's root span
… the tunnel outside init
… init or framework defaults Framework SDKs report tunnel options and the base path once with setManagedTunnelDefaults, and ServerRuntimeClient applies them when it's created, so per-request clients get the span filters before their first span starts. The span filters move out of the base Client into the server client, and the server-only helpers move to @sentry/core/server.
The core tunnel handler now streams the envelope to Sentry instead of buffering it, so the forwarded body is a stream.
The paths only depend on the client's tunnel options and DSN, so they're derived when the managed tunnel is settled and read per request. This drops the path cache, which the browser never needed, and the options forms of `isTunnelRequest` and `getTunnelPaths`.
The matcher forwarded through the public handler, which resolved the allowed DSNs again on every request. The client now stores them next to the tunnel paths, and the own DSN is passed explicitly, so a client without a DSN never picks up another client's. The header line is parsed as JSON directly, since it's all the handler reads.
… streamed body Next.js's edge runtime in dev rejects a Content-Length header on a streamed fetch body, so every forward failed with a 500. Relay takes the chunked upload.
They're derived per client, so core keeps them in its own map instead of on the public options type.
#25515's 411 and 413 checks run before the header line is read, so an oversized or unlengthed request is rejected without reading its body.
… ignore a root one
…ne written with a secret key still matches
4ae6673 to
feae0ed
Compare
|
|
||
| if (isSentryRequestUrl(handlerData.fetchData.url, client)) { | ||
| // Like Node's http and undici instrumentation: SDK-internal requests leave no breadcrumb. | ||
| if (isTracingSuppressed() || isSentryRequestUrl(handlerData.fetchData.url, client)) { |
There was a problem hiding this comment.
q: Is this related to this PR?
| /** | ||
| * Server only. DSNs to forward besides the server's own, e.g. a different browser DSN. | ||
| */ | ||
| allowedDsns?: string[]; |
There was a problem hiding this comment.
q: What would happen in the browser? Is it always a no-op on browsers?
| path?: string; | ||
|
|
||
| /** Path prefix the app is served under, e.g. `/docs`. Set the same value in the browser and the server `init`. */ | ||
| basePath?: string; |
There was a problem hiding this comment.
q: I would assume this will be automatically detected in meta frameworks?
| export type BeforeSendStaticSpanCallback = (span: SpanJSON) => SpanJSON; | ||
|
|
||
| /** Options for the managed tunnel, see `tunnel`. */ | ||
| export interface TunnelOptions { |
There was a problem hiding this comment.
l: Would be nice if we could define some defaults for each these options. It's quite obvious for allowedDsns, path and basePath. But trailingSlash is a 50/50 chance
| /** Enables the managed tunnel, e.g. from TanStack Start's `tunnelRoute` build option. */ | ||
| tunnel?: TunnelOptions; | ||
| /** The path prefix the app is served under. */ | ||
| basePath?: string; |
There was a problem hiding this comment.
q: How would that work. In TunnelOptions there is a basePath and here is a basePath too, what would be the difference and why is it here twice?
| expect(fetchSpy).toHaveBeenCalledWith( | ||
| 'https://o1.ingest.sentry.io/api/1/envelope/?sentry_version=7&sentry_key=public', | ||
| { | ||
| expect.objectContaining({ |
There was a problem hiding this comment.
q: Any reason why this moved to objectContaining?
| tunnelOptions.basePath === undefined && basePath ? { ...tunnelOptions, basePath } : tunnelOptions, | ||
| ); | ||
| options._managedTunnel = resolved; | ||
| const allowedDsns = resolveAllowedDsns(resolved.allowedDsns, client.getDsn()); |
There was a problem hiding this comment.
q: Is it 100% certain that this is not being undefined or '' in some behavior? I would assume if this would fail, for whatever reason, then the tunnel will be /, as I guess the hash of an empty string is an empty string.
| export function getTunnelPath(dsn: DsnLike): string | undefined { | ||
| const components = makeDsn(dsn); | ||
|
|
||
| return components ? `/${fnv1a(dsnToString(components)).toString(36)}` : undefined; |
There was a problem hiding this comment.
q: I would assume this hash is always the same? So it would also be consistent across deployments, right?
| export function getTunnelPath(dsn: DsnLike): string | undefined { | ||
| const components = makeDsn(dsn); | ||
|
|
||
| return components ? `/${fnv1a(dsnToString(components)).toString(36)}` : undefined; |
There was a problem hiding this comment.
q/m: Is this always being hashed? Maybe I read it a little wrong, but the PR description says, that one could choose /monitoring as path, which wouldn't work anymore?!
What if they want to keep a non hash as tunnel behind that there is an OTel collector, which only then gets send to Sentry (right now, I'm not sure if the OTel collector use case would work with a tunnel actually)
| export type { HandleTunnelRequestOptions } from './utils/tunnel'; | ||
| export { handleTunnelRequest } from './utils/tunnel'; | ||
| export { | ||
| applyTunnelDefaults, |
There was a problem hiding this comment.
l: Before we merge this it might make sense to verify if that would work for everything we support (e.g. Next.js + Cloudflare), not that we have a public API which might need some tweaking. Maybe exporting them as __INTERNAL__ or __EXPERIMENTAL__ first would make sense? Don't think the __EXPERIMENTAL__ is something we ever used, but then we can at least change it before it is 100% working (maybe it is)
Overview
SDK Tunnel is a feature that allows client SDKs (e.g: browser) to send their requests through an endpoint that lives on the user's own application that forwards to Sentry. This bypasses adblockers and similar extensions.
Tunnels could be configured as a fixed endpoint defined by the user (defaulted to
/monitoring) but that usually ends up in global adblocker lists. Alternatively, users can tell the SDK to use a random endpoint to bypass that.Previously the tunnel logic mostly lived in client bundles and in server frameworks build time, this had two issues:
This stack of PRs re-works the idea by moving to more stable yet random URL generation based on the client's DSN and moves the tunnel handling into the runtime of the core SDK.
Meaning tunneling now is a core SDK capability rather than a meta framework feature.
Here is how it looks for users:
And a tunneled request goes like this: the framework hook calls
handleTunnelRequestIfMatched, if the path matches it checks the envelope's DSN against the allowed ones and forwards it to Sentry, otherwise the request continues as normal.This PR
This sets up the core for the new tunnel mechanism that the rest of the stack will be implemented against, it does the following:
It also sets up a couple of key decisions that will carry over throughout the stack:
/appas a base path our tunnels handlers will expect that prefix.allowedDSNson the server, this correction only applies if the client and server share the same DSN, if there is a different DSN involved then it is on the user to configure that correctly. Will work on documenting this.Reviewing the stack
This PR and #25200 are where the actual behavior lives so these need the closest look, mainly
forwardEnvelopeand the base path handling intunnel.ts.The framework PRs all follow the same pattern: find the earliest request hook the framework gives us, call the core handler there, and pass the framework's base path along. They are small, so ideally whoever knows that framework best picks it up:
sentryHandleunder the app's base path #25415#25414 is tests only, and Next.js #25424 is on hold, I will split it up before it's ready.