Repository navigation
feat(core): Add managed tunnel helpers that derive the tunnel path from the DSN #25199
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
cceccb5
2d4bfff
4eb575c
3bb40d5
1981673
14be6ef
3a32058
de7df0f
9801a34
470d661
732cad5
ad34dcb
b8705d2
2381531
42246b3
eb53646
817fcbf
d6eccb3
18e7f56
6823022
f3e458e
41d33fd
f14ef30
09e46cd
7a572e0
595ee6e
09a8919
34b0a79
1ae606b
93e68bd
994cab5
feae0ed
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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'; | ||
|
|
@@ -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)) { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. q: Is this related to this PR? |
||
| return; | ||
| } | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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. | ||
|
|
@@ -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?: TunnelOptions; | ||
|
|
||
| /** | ||
| * Controls what data the SDK collects and sends to Sentry. | ||
| * All fields are optional — omitted fields use the documented defaults. | ||
|
|
@@ -645,6 +652,23 @@ export type BeforeSendStreamedSpanCallback = (span: StreamedSpanJSON) => Streame | |
| */ | ||
| export type BeforeSendStaticSpanCallback = (span: SpanJSON) => SpanJSON; | ||
|
|
||
| /** Options for the managed tunnel, see `tunnel`. */ | ||
| export interface TunnelOptions { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. l: Would be nice if we could define some |
||
| /** | ||
| * Server only. DSNs to forward besides the server's own, e.g. a different browser DSN. | ||
| */ | ||
| allowedDsns?: string[]; | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. q: What would happen in the browser? Is it always a no-op on browsers? |
||
|
|
||
| /** 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; | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. q: I would assume this will be automatically detected in meta frameworks? |
||
|
|
||
| /** 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>>, | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,77 @@ | ||
| import type { Client } from '../client'; | ||
| import { getClient } from '../currentScopes'; | ||
| import type { IgnoreSpanFilter, TunnelOptions } 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, e.g. from TanStack Start's `tunnelRoute` build option. */ | ||
| tunnel?: TunnelOptions; | ||
| /** The path prefix the app is served under. */ | ||
| basePath?: string; | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. q: How would that work. In |
||
| } | ||
|
|
||
| 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 = options?._managedTunnel ?? managedTunnelDefaults?.tunnel; | ||
| if (!client || !options || !tunnelOptions) { | ||
| return; | ||
| } | ||
|
|
||
| const basePath = normalizeTunnelBasePath(managedTunnelDefaults?.basePath); | ||
| const resolved = stripBasePathFromTunnelPath( | ||
| tunnelOptions.basePath === undefined && basePath ? { ...tunnelOptions, basePath } : tunnelOptions, | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. m: Here there is a logic for the duplication of |
||
| ); | ||
| options._managedTunnel = resolved; | ||
| const allowedDsns = resolveAllowedDsns(resolved.allowedDsns, client.getDsn()); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. q: Is it 100% certain that this is not being |
||
| 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; | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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)