Skip to content

Commit e77835f

Browse files
committed
perf(core): Cache the tunnel path per DSN
Servers derive the tunnel paths on every POST, so each DSN is now parsed and hashed once. An invalid DSN in `allowedDsns` also logs once instead of on every request.
1 parent 9f8a9c0 commit e77835f

2 files changed

Lines changed: 29 additions & 2 deletions

File tree

‎packages/core/src/utils/tunnel.ts‎

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,10 @@ export interface HandleTunnelRequestOptions {
1515
allowedDsns?: Array<string>;
1616
}
1717

18+
// Servers derive the paths on every POST, so parse and hash each DSN once. This also keeps an invalid DSN in
19+
// `allowedDsns` from logging on every request. Keys come from config, never from requests, so the map stays small.
20+
const tunnelPathCache = new Map<string, string | undefined>();
21+
1822
/**
1923
* Splits the `tunnel` option a server SDK's `init` received. A URL stays the transport tunnel for the server's own
2024
* envelopes. `true` or {@link TunnelOptions} enables serving the managed tunnel instead.
@@ -37,9 +41,17 @@ export function resolveServerTunnelOption(tunnel: string | true | TunnelOptions
3741
* between them. The path is a hash so it carries nothing an ad-blocker filter list could match on.
3842
*/
3943
export function getTunnelPath(dsn: DsnLike): string | undefined {
44+
if (typeof dsn === 'string' && tunnelPathCache.has(dsn)) {
45+
return tunnelPathCache.get(dsn);
46+
}
47+
4048
const components = makeDsn(dsn);
49+
const path = components ? `/${fnv1a(dsnToString(components)).toString(36)}` : undefined;
50+
if (typeof dsn === 'string') {
51+
tunnelPathCache.set(dsn, path);
52+
}
4153

42-
return components ? `/${fnv1a(dsnToString(components)).toString(36)}` : undefined;
54+
return path;
4355
}
4456

4557
/**
@@ -193,7 +205,12 @@ function stripBasePath(pathname: string, basePath: string | undefined): string {
193205
return pathname === base || pathname.startsWith(`${base}/`) ? pathname.slice(base.length) : pathname;
194206
}
195207

196-
/** 32-bit FNV-1a. Synchronous, so the browser can derive the path before its client is constructed. */
208+
/**
209+
* 32-bit FNV-1a, because the browser needs the path synchronously in `init` (`crypto.subtle.digest` is async) and it
210+
* runs in every runtime without dependencies. It needn't be cryptographic since the DSN is public.
211+
*
212+
* Browser and server SDKs compute the path separately, so changing the algorithm breaks mixed SDK versions.
213+
*/
197214
function fnv1a(input: string): number {
198215
let hash = 0x811c9dc5;
199216
for (let i = 0; i < input.length; i++) {

‎packages/core/test/lib/utils/tunnel.test.ts‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,16 @@ describe('getTunnelPath', () => {
202202
it('returns undefined for an invalid DSN', () => {
203203
expect(getTunnelPath('not-a-dsn')).toBeUndefined();
204204
});
205+
206+
it('parses each DSN string once, so an invalid DSN logs once', () => {
207+
const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
208+
209+
getTunnelPath('also-not-a-dsn');
210+
getTunnelPath('also-not-a-dsn');
211+
212+
expect(consoleErrorSpy).toHaveBeenCalledOnce();
213+
consoleErrorSpy.mockRestore();
214+
});
205215
});
206216

207217
describe('isTunnelRequest', () => {

0 commit comments

Comments
 (0)