Skip to content

Commit d95a40a

Browse files
committed
feat(core): Add tunnel server helper
1 parent b6dbde8 commit d95a40a

File tree

2 files changed

+77
-0
lines changed

2 files changed

+77
-0
lines changed

packages/core/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,7 @@ export { getMetricSummaryJsonForSpan } from './metrics/metric-summary';
108108
export { addTracingHeadersToFetchRequest, instrumentFetchRequest } from './fetch';
109109
export { trpcMiddleware } from './trpc';
110110
export { captureFeedback } from './feedback';
111+
export { handleTunnelEnvelope } from './tunnel';
111112

112113
// eslint-disable-next-line deprecation/deprecation
113114
export { getCurrentHubShim, getCurrentHub } from './getCurrentHubShim';

packages/core/src/tunnel.ts

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import type { Client, Transport } from '@sentry/types';
2+
import { createEnvelope, dsnFromString, parseEnvelope } from '@sentry/utils';
3+
import { getEnvelopeEndpointWithUrlEncodedAuth } from './api';
4+
import { getClient } from './currentScopes';
5+
6+
interface HandleTunnelOptions {
7+
/**
8+
* A list of DSNs that are allowed to be passed through the server.
9+
*
10+
* Defaults to only server DSN.
11+
*/
12+
dsnAllowList?: string[];
13+
/**
14+
* The client instance to use
15+
*
16+
* Defaults to the global instance.
17+
*/
18+
client?: Client;
19+
}
20+
21+
let CACHED_TRANSPORTS: Map<string, Transport> | undefined;
22+
23+
/**
24+
* Handles envelopes sent from the browser client via the tunnel option.
25+
*/
26+
export async function handleTunnelEnvelope(
27+
envelopeBytes: Uint8Array,
28+
options: HandleTunnelOptions = {},
29+
): Promise<void> {
30+
const client = (options && options.client) || getClient();
31+
32+
if (!client) {
33+
throw new Error('No server client');
34+
}
35+
36+
const [headers, items] = parseEnvelope(envelopeBytes);
37+
38+
if (!headers.dsn) {
39+
throw new Error('DSN missing from envelope headers');
40+
}
41+
42+
// If the DSN in the envelope headers matches the server DSN, we can send it directly.
43+
const clientOptions = client.getOptions();
44+
if (headers.dsn === clientOptions.dsn) {
45+
await client.sendEnvelope(createEnvelope(headers, items));
46+
return;
47+
}
48+
49+
if (!options.dsnAllowList || !options.dsnAllowList.includes(headers.dsn)) {
50+
throw new Error('DSN does not match server DSN or allow list');
51+
}
52+
53+
if (!CACHED_TRANSPORTS) {
54+
CACHED_TRANSPORTS = new Map();
55+
}
56+
57+
let transport = CACHED_TRANSPORTS.get(headers.dsn);
58+
59+
if (!transport) {
60+
const dsn = dsnFromString(headers.dsn);
61+
if (!dsn) {
62+
throw new Error('Invalid DSN in envelope headers');
63+
}
64+
const url = getEnvelopeEndpointWithUrlEncodedAuth(dsn);
65+
66+
const createTransport = clientOptions.transport;
67+
transport = createTransport({
68+
...clientOptions.transportOptions,
69+
recordDroppedEvent: client.recordDroppedEvent.bind(client),
70+
url,
71+
});
72+
CACHED_TRANSPORTS.set(headers.dsn, transport);
73+
}
74+
75+
await transport.send(createEnvelope(headers, items));
76+
}

0 commit comments

Comments
 (0)