Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
ed011c1
feat(core): Add managed tunnel helpers that derive the tunnel path fr…
logaretm Oct 8, 2026
1ed77e7
ref(core): Pass the whole request to the tunnel matcher and expose `g…
logaretm Oct 15, 2026
9f81b8d
perf(core): Cache the tunnel path per DSN
logaretm Oct 15, 2026
449f3f5
feat(core): Serve a fixed managed tunnel path from a relative `tunnel…
logaretm Oct 15, 2026
be49ea1
feat(core): Drop the server spans of managed tunnel requests
logaretm Oct 15, 2026
2b8e376
fix(core): Skip fetch instrumentation while tracing is suppressed
logaretm Oct 15, 2026
2b03c6a
feat(core): Check the tunnel DSN before reading the envelope body
logaretm Oct 15, 2026
4fcb0cb
fix(core): Cancel the body of a rejected tunnel request
logaretm Oct 15, 2026
2af4e5e
feat(core): Forward the client IP of tunnel requests to Sentry
logaretm Oct 15, 2026
fda805e
fix(core): Match a fixed tunnel path that ends with a slash
logaretm Oct 16, 2026
d348c1b
fix(core): Only answer the managed tunnel under its base path
logaretm Oct 16, 2026
69719b6
feat(core): Always accept the server's own DSN in the managed tunnel
logaretm Oct 16, 2026
95d228f
feat(core): Add `applyTunnelDefaults` so framework SDKs fill in the b…
logaretm Oct 16, 2026
f35b3a3
feat(core): Add `setManagedTunnelBasePath` for servers that learn the…
logaretm Oct 16, 2026
b9d60f8
feat(core): Mark the tunnel handler's responses so the browser can te…
logaretm Oct 16, 2026
0ad10d4
fix(core): Normalize tunnel base paths with one util that doesn't bac…
logaretm Oct 16, 2026
ee9dcd5
fix(core): Leave a fixed tunnel path to the app when there is no DSN …
logaretm Oct 18, 2026
5653798
fix(core): Don't add the base path again to a tunnel path that alread…
logaretm Oct 19, 2026
4a999a3
fix(core): Don't report a client that drops the tunnel request as an …
logaretm Oct 19, 2026
3017e68
fix(core): Also drop tunnel spans that only carry `http.url`, like Sv…
logaretm Oct 19, 2026
c7a419a
feat(core): Add enableManagedTunnel for framework options that enable…
logaretm Oct 19, 2026
c54586d
ref(core): Settle a server client's managed tunnel in one place, from…
logaretm Oct 19, 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
10 changes: 8 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,
getTunnelPath,
handleTunnelRequest,
normalizeTunnelBasePath,
TUNNEL_RESPONSE_HEADER,
} from './utils/tunnel';
export { addAutoIpAddressToSession } from './utils/ipAddress';
export {
convertSpanLinksForEnvelope,
Expand Down Expand Up @@ -415,7 +421,7 @@ export type {
HttpHeadersCollection,
ResolvedDataCollection,
} from './types/datacollection';
export type { ClientOptions, CoreOptions as Options } from './types/options';
export type { ClientOptions, CoreOptions as Options, 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)) {
// Matches Node's http and undici instrumentation, so SDK-internal requests (e.g. a tunnel forward) leave no breadcrumb.
if (isTracingSuppressed() || isSentryRequestUrl(handlerData.fetchData.url, client)) {
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
8 changes: 8 additions & 0 deletions packages/core/src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,14 @@

export type { ServerRuntimeClientOptions } from './server-runtime-client';
export { ServerRuntimeClient } from './server-runtime-client';
export type { TunnelRequestLike } from './utils/tunnel';
export {
getTunnelPaths,
handleTunnelRequestIfMatched,
isTunnelRequest,
resolveServerTunnelOption,
} from './utils/tunnel';
export { _INTERNAL_resetManagedTunnelDefaults, setManagedTunnelDefaults } from './utils/managedTunnelDefaults';
export type { ServerRuntimeOptions } from './types/options';
export { trpcMiddleware } from './trpc';
export { wrapMcpServerWithSentry } from './integrations/mcp-server';
Expand Down
28 changes: 27 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,14 @@ export interface ClientOptions<TO extends BaseTransportOptions = BaseTransportOp
*/
tunnel?: string;

/**
* Set by server SDKs when the managed tunnel is enabled. Server request handlers check it to decide whether to
* serve the managed tunnel.
*
* @internal This option is not part of the public API and is subject to change at any time.
*/
_managedTunnel?: TunnelOptions;

/**
* 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 +653,24 @@ export type BeforeSendStreamedSpanCallback = (span: StreamedSpanJSON) => Streame
*/
export type BeforeSendStaticSpanCallback = (span: SpanJSON) => SpanJSON;

/** Options for the managed tunnel, see `tunnel`. */
export interface TunnelOptions {
/**
* Server only. DSNs whose envelopes the tunnel forwards, in addition to the server's own DSN. List the browser DSN
* here when the browser uses a different one.
*/
allowedDsns?: string[];

/** 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;

/** Browser only. Append a trailing slash to the tunnel path, for servers that redirect paths without one. */
trailingSlash?: boolean;
}

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

/**
* Managed tunnel settings a framework SDK knows outside `init`.
*/
interface ManagedTunnelDefaults {
/** 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;
}

let managedTunnelDefaults: ManagedTunnelDefaults | undefined;

// The span filters each client got for its managed tunnel, so applying new defaults replaces them.
const installedSpanFilters = new WeakMap<Client, IgnoreSpanFilter[]>();

/**
* Sets the managed tunnel settings a framework SDK knows outside `init`, for every server client created afterwards
* and for the current one. Clients created per request, like on Cloudflare, then start with the tunnel and its span
* filters, so not even their first tunnel request's span gets through. A managed tunnel or `basePath` that `init`
* set wins.
*
* Framework SDKs call this once, when their server code loads.
*/
export function setManagedTunnelDefaults(defaults: ManagedTunnelDefaults): void {
managedTunnelDefaults = {
...managedTunnelDefaults,
...(defaults.tunnel && { tunnel: defaults.tunnel }),
...(defaults.basePath !== undefined && { basePath: defaults.basePath }),
};
applyManagedTunnelDefaults(getClient());
}

/**
* Settles a server client's managed tunnel: the one its `init` enabled, or else the defaults, under the default base
* path, and installs the span filters for it. Server clients call this when they're created, and
* {@link setManagedTunnelDefaults} calls it again for a client that already exists, replacing the filters it added.
*/
export function applyManagedTunnelDefaults(client: Client | undefined): void {
const options = client?.getOptions();
const tunnelOptions = options?._managedTunnel ?? managedTunnelDefaults?.tunnel;
if (!client || !options || !tunnelOptions) {
return;
}

const basePath = normalizeTunnelBasePath(managedTunnelDefaults?.basePath);
const resolved = withPathBelowBasePath(
tunnelOptions.basePath === undefined && basePath ? { ...tunnelOptions, basePath } : tunnelOptions,
);
options._managedTunnel = resolved;

const previousFilters = installedSpanFilters.get(client) ?? [];
const spanFilters = getTunnelIgnoreSpanFilters(resolved, client.getDsn());
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