Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
e05b822
feat(core): Add managed tunnel helpers that derive the tunnel path fr…
logaretm Oct 8, 2026
3efd40c
ref(core): Pass the whole request to the tunnel matcher and expose `g…
logaretm Oct 15, 2026
ce011e5
perf(core): Cache the tunnel path per DSN
logaretm Oct 15, 2026
1eb7e9a
feat(core): Serve a fixed managed tunnel path from a relative `tunnel…
logaretm Oct 15, 2026
a0e794c
feat(core): Drop the server spans of managed tunnel requests
logaretm Oct 15, 2026
1ed892e
fix(core): Skip fetch instrumentation while tracing is suppressed
logaretm Oct 15, 2026
4e63500
feat(core): Check the tunnel DSN before reading the envelope body
logaretm Oct 15, 2026
3dda6fa
fix(core): Cancel the body of a rejected tunnel request
logaretm Oct 15, 2026
6b21417
feat(core): Forward the client IP of tunnel requests to Sentry
logaretm Oct 15, 2026
30ff512
fix(core): Match a fixed tunnel path that ends with a slash
logaretm Oct 16, 2026
8bd6358
fix(core): Only answer the managed tunnel under its base path
logaretm Oct 16, 2026
d0fd956
feat(core): Always accept the server's own DSN in the managed tunnel
logaretm Oct 16, 2026
3ee7269
feat(core): Add `applyTunnelDefaults` so framework SDKs fill in the b…
logaretm Oct 16, 2026
786c90f
feat(core): Add `setManagedTunnelBasePath` for servers that learn the…
logaretm Oct 16, 2026
3f76c2a
feat(core): Mark the tunnel handler's responses so the browser can te…
logaretm Oct 16, 2026
8a5c355
fix(core): Normalize tunnel base paths with one util that doesn't bac…
logaretm Oct 16, 2026
1d62dfb
fix(core): Leave a fixed tunnel path to the app when there is no DSN …
logaretm Oct 18, 2026
1fd2f88
fix(core): Don't add the base path again to a tunnel path that alread…
logaretm Oct 19, 2026
dd8c671
fix(core): Don't report a client that drops the tunnel request as an …
logaretm Oct 19, 2026
6cb4e27
fix(core): Also drop tunnel spans that only carry `http.url`, like Sv…
logaretm Oct 19, 2026
34a938d
feat(core): Add enableManagedTunnel for framework options that enable…
logaretm Oct 19, 2026
ca87483
ref(core): Settle a server client's managed tunnel in one place, from…
logaretm Oct 19, 2026
64bc21b
test(nextjs): Read back the streamed body in the tunnel route test
logaretm Oct 19, 2026
52c892c
ref(core): Compute the managed tunnel paths once per server client
logaretm Oct 19, 2026
6fee43e
ref(core): Resolve the managed tunnel's DSNs once per server client
logaretm Oct 19, 2026
433fd64
fix(core): Don't forward the tunnel request's Content-Length with the…
logaretm Oct 19, 2026
688680f
ref(core): Keep the served tunnel paths and DSNs out of ClientOptions
logaretm Oct 19, 2026
830abd6
ref(core): Trim the managed tunnel comments
logaretm Oct 19, 2026
059e572
feat(core): Keep the tunnel body size checks with the streamed forward
logaretm Oct 19, 2026
41da293
ref(core): Rename withPathBelowBasePath to stripBasePathFromTunnelPath
logaretm Oct 19, 2026
d525290
ref(core): Normalize the default base path in applyTunnelDefaults and…
logaretm Oct 19, 2026
f32aca0
fix(core): Normalize allowedDsns like the envelope header's DSN, so o…
logaretm Oct 19, 2026
41d9ff8
ref(core): Export the tunnel helpers only SDKs use as `_INTERNAL_`
logaretm Oct 22, 2026
aa2b17b
ref(core): Split the tunnel options into browser and server variants
logaretm Oct 22, 2026
2774caf
ref(core): Add a documented `TunnelInit` type for the `tunnel` option
logaretm Oct 22, 2026
332a7c1
ref(core): Keep the defaults' base path in one place
logaretm Oct 22, 2026
f1a81bf
fix(core): Ignore a root fixed tunnel path instead of taking over POS…
logaretm Oct 22, 2026
98ff881
ref(core): Name the tunnel path hash `fnv1a32` and link its spec
logaretm Oct 22, 2026
6023ac8
feat(core): Forward the client's user agent, client hints, Origin and…
logaretm Oct 22, 2026
b29bdaf
ref(core): Drop the tunnel's body size cap and let Sentry's 413 decide
logaretm Oct 22, 2026
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
17 changes: 15 additions & 2 deletions packages/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,13 @@ export { handleCallbackErrors } from './utils/handleCallbackErrors';
export { safeCallback } from './utils/safeCallback';
export { parameterize, fmt } from './utils/parameterize';
export type { HandleTunnelRequestOptions } from './utils/tunnel';
export { handleTunnelRequest } from './utils/tunnel';
export {
applyTunnelDefaults as _INTERNAL_applyTunnelDefaults,
getTunnelPath,
handleTunnelRequest,
normalizeTunnelBasePath as _INTERNAL_normalizeTunnelBasePath,
TUNNEL_RESPONSE_HEADER,
} from './utils/tunnel';
export { addAutoIpAddressToSession } from './utils/ipAddress';
export {
convertSpanLinksForEnvelope,
Expand Down Expand Up @@ -415,7 +421,14 @@ export type {
HttpHeadersCollection,
ResolvedDataCollection,
} from './types/datacollection';
export type { ClientOptions, CoreOptions as Options } from './types/options';
export type {
BrowserTunnelOptions,
ClientOptions,
CoreOptions as Options,
ServerTunnelOptions,
TunnelInit,
TunnelOptions,
} from './types/options';
export type { Package } from './types/package';
export type { PolymorphicEvent, PolymorphicRequest } from './types/polymorphics';
export type {
Expand Down
4 changes: 3 additions & 1 deletion packages/core/src/integrations/fetch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { getClient } from '../currentScopes';
import { instrumentFetchRequest } from '../fetch';
import { defineIntegration } from '../integration';
import { addFetchInstrumentationHandler } from '../instrument/fetch';
import { isTracingSuppressed } from '../tracing/trace';
import type { FetchBreadcrumbData, FetchBreadcrumbHint } from '../types/breadcrumb';
import type { HandlerDataFetch } from '../types/instrument';
import type { Integration, IntegrationFn } from '../types/integration';
Expand Down Expand Up @@ -92,7 +93,8 @@ export function createFetchIntegration({
return;
}

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)) {
Comment thread
logaretm marked this conversation as resolved.
Comment thread
logaretm marked this conversation as resolved.
return;
}

Expand Down
2 changes: 2 additions & 0 deletions packages/core/src/server-runtime-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import { uuid4 } from './utils/misc';
import { makePromiseBuffer } from './utils/promisebuffer';
import { resolvedSyncPromise } from './utils/syncpromise';
import { _getTraceInfoFromScope } from './utils/trace-info';
import { applyManagedTunnelDefaults } from './utils/managedTunnelDefaults';

export interface ServerRuntimeClientOptions extends ClientOptions<BaseTransportOptions> {
platform?: string;
Expand Down Expand Up @@ -58,6 +59,7 @@ export class ServerRuntimeClient<

super(options);

applyManagedTunnelDefaults(this);
this._setUpMetricsProcessing();
}

Expand Down
11 changes: 11 additions & 0 deletions packages/core/src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,17 @@

export type { ServerRuntimeClientOptions } from './server-runtime-client';
export { ServerRuntimeClient } from './server-runtime-client';
export type { TunnelRequestLike } from './utils/tunnel';
export {
getTunnelPaths as _INTERNAL_getTunnelPaths,
handleTunnelRequestIfMatched,
isTunnelRequest,
resolveServerTunnelOption as _INTERNAL_resolveServerTunnelOption,
} from './utils/tunnel';
export {
_INTERNAL_resetManagedTunnelDefaults,
setManagedTunnelDefaults as _INTERNAL_setManagedTunnelDefaults,
} from './utils/managedTunnelDefaults';
export type { ServerRuntimeOptions } from './types/options';
export { trpcMiddleware } from './trpc';
export { wrapMcpServerWithSentry } from './integrations/mcp-server';
Expand Down
43 changes: 42 additions & 1 deletion packages/core/src/types/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ export type IgnoreSpanAttributeValue = string | boolean | number | string[] | bo
* At least one of the properties (`name`, `op`, or `attributes`) must be set.
* If multiple are set, all must match for the span to be ignored.
*/
type IgnoreSpanFilter =
export type IgnoreSpanFilter =
| {
/**
* Spans with a name matching this pattern will be ignored.
Expand Down Expand Up @@ -373,6 +373,13 @@ export interface ClientOptions<TO extends BaseTransportOptions = BaseTransportOp
*/
tunnel?: string;

/**
* Set by server SDKs when the managed tunnel is enabled.
*
* @internal This option is not part of the public API and is subject to change at any time.
*/
_managedTunnel?: ServerTunnelOptions;

/**
* Controls what data the SDK collects and sends to Sentry.
* All fields are optional — omitted fields use the documented defaults.
Expand Down Expand Up @@ -645,6 +652,40 @@ export type BeforeSendStreamedSpanCallback = (span: StreamedSpanJSON) => Streame
*/
export type BeforeSendStaticSpanCallback = (span: SpanJSON) => SpanJSON;

/** Managed tunnel options shared by the browser and the server `init`, see `tunnel`. */
export interface TunnelOptions {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Default is undefined in all of these cases, which defaults did you have in mind?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It must default to something internally. At least for trailing slash our internal default is true or false, right?

@logaretm logaretm Oct 22, 2026 •

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

undefined behaves the same as false for the trailing slash specifically. So I wouldn't bother with a default unless it is opt-out.

WDYT?

/** A fixed tunnel path instead of the one derived from the DSN. A relative `tunnel` string sets this. */
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;
Comment thread
logaretm marked this conversation as resolved.
}

/** Managed tunnel options for the browser `init`. */
export interface BrowserTunnelOptions extends TunnelOptions {
/** Append a trailing slash to the tunnel path, for servers that redirect paths without one. */
trailingSlash?: boolean;
}

/** Managed tunnel options for a server `init`. */
export interface ServerTunnelOptions extends TunnelOptions {
/** DSNs to forward besides the server's own, e.g. a different browser DSN. */
allowedDsns?: string[];
}

/**
* The `tunnel` option, where to send envelopes instead of the Sentry ingest URL:
*
* - A URL, e.g. `https://example.com/tunnel`, sends envelopes to your own tunnel endpoint.
* - A relative path, e.g. `/monitor`, enables the managed tunnel at that fixed path. The browser sends envelopes to
* it and the server SDK serves it, so set the same path in both `init`s.
* - `true` enables the managed tunnel at a same-origin path derived from the DSN. Set it in both the browser and the
* server `init`: the browser sends envelopes to that path and the server SDK forwards them to Sentry. This keeps
* ad-blockers from dropping events.
* - Options, like `true`, with the browser or server options of the managed tunnel.
*/
export type TunnelInit<T extends TunnelOptions = TunnelOptions> = string | true | T;

/** Base configuration options for every SDK. */
export interface CoreOptions<TO extends BaseTransportOptions = BaseTransportOptions> extends Omit<
Partial<ClientOptions<TO>>,
Expand Down
80 changes: 80 additions & 0 deletions packages/core/src/utils/managedTunnelDefaults.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import type { Client } from '../client';
import { getClient } from '../currentScopes';
import type { IgnoreSpanFilter, ServerTunnelOptions } from '../types/options';
import {
getTunnelIgnoreSpanFilters,
normalizeTunnelBasePath,
resolveAllowedDsns,
resolveTunnelPaths,
setServedTunnel,
stripBasePathFromTunnelPath,
} from './tunnel';

/**
* Managed tunnel settings a framework SDK knows outside `init`.
*/
interface ManagedTunnelDefaults {
/**
* Enables the managed tunnel when `init` didn't, e.g. from TanStack Start's `tunnelRoute` build option. Its base
* path is `basePath`, so it has none of its own.
*/
tunnel?: Omit<ServerTunnelOptions, 'basePath'>;
/** The path prefix the app is served under. Applies however the tunnel was enabled, unless `init` set its own. */
basePath?: string;
Comment thread
logaretm marked this conversation as resolved.
}

let managedTunnelDefaults: ManagedTunnelDefaults | undefined;

// So re-applying defaults replaces a client's filters instead of adding more.
const installedSpanFilters = new WeakMap<Client, IgnoreSpanFilter[]>();

/**
* Sets framework-known tunnel settings for the current and every later server client, so per-request clients
* (Cloudflare) start with them. Settings from `init` win. Framework SDKs call this once at load.
*/
export function setManagedTunnelDefaults(defaults: ManagedTunnelDefaults): void {
managedTunnelDefaults = {
...managedTunnelDefaults,
...(defaults.tunnel && { tunnel: defaults.tunnel }),
...(defaults.basePath !== undefined && { basePath: defaults.basePath }),
};
applyManagedTunnelDefaults(getClient());
}

/**
* Resolves a server client's managed tunnel (from `init` or the defaults), computes its paths and allowed DSNs, and
* installs its span filters. Runs when a server client is created and from {@link setManagedTunnelDefaults}.
*/
export function applyManagedTunnelDefaults(client: Client | undefined): void {
const options = client?.getOptions();
const tunnelOptions: ServerTunnelOptions | undefined = options?._managedTunnel ?? managedTunnelDefaults?.tunnel;
if (!client || !options || !tunnelOptions) {
return;
}

const basePath = normalizeTunnelBasePath(managedTunnelDefaults?.basePath);
const resolved = stripBasePathFromTunnelPath(
tunnelOptions.basePath === undefined && basePath ? { ...tunnelOptions, basePath } : tunnelOptions,
Comment thread
logaretm marked this conversation as resolved.
);
options._managedTunnel = resolved;
const allowedDsns = resolveAllowedDsns(resolved.allowedDsns, client.getDsn());
Comment thread
logaretm marked this conversation as resolved.
const paths = resolveTunnelPaths(resolved, allowedDsns);
setServedTunnel(client, { paths, allowedDsns });

const previousFilters = installedSpanFilters.get(client) ?? [];
const spanFilters = getTunnelIgnoreSpanFilters(resolved, paths);
options.ignoreSpans = [
...(options.ignoreSpans ?? []).filter(filter => !(previousFilters as unknown[]).includes(filter)),
...spanFilters,
];
installedSpanFilters.set(client, spanFilters);
}

/**
* Resets {@link setManagedTunnelDefaults}, for tests.
*
* @internal
*/
export function _INTERNAL_resetManagedTunnelDefaults(): void {
managedTunnelDefaults = undefined;
}
Loading
Loading