Skip to content

Commit ae5d7c8

Browse files
authored
feat(v7): Add tunnel support to multiplexed transport (#11851)
1 parent 9ffc7f4 commit ae5d7c8

File tree

4 files changed

+72
-21
lines changed

4 files changed

+72
-21
lines changed

packages/core/src/baseclient.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,7 @@ export abstract class BaseClient<O extends ClientOptions> implements Client<O> {
150150
if (this._dsn) {
151151
const url = getEnvelopeEndpointWithUrlEncodedAuth(this._dsn, options);
152152
this._transport = options.transport({
153+
tunnel: this._options.tunnel,
153154
recordDroppedEvent: this.recordDroppedEvent.bind(this),
154155
...options.transportOptions,
155156
url,

packages/core/src/transports/multiplexed.ts

Lines changed: 38 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import type {
77
Transport,
88
TransportMakeRequestResponse,
99
} from '@sentry/types';
10-
import { dsnFromString, forEachEnvelopeItem } from '@sentry/utils';
10+
import { createEnvelope, dsnFromString, forEachEnvelopeItem } from '@sentry/utils';
1111

1212
import { getEnvelopeEndpointWithUrlEncodedAuth } from '../api';
1313

@@ -57,6 +57,7 @@ function makeOverrideReleaseTransport<TO extends BaseTransportOptions>(
5757
const transport = createTransport(options);
5858

5959
return {
60+
...transport,
6061
send: async (envelope: Envelope): Promise<void | TransportMakeRequestResponse> => {
6162
const event = eventFromEnvelope(envelope, ['event', 'transaction', 'profile', 'replay_event']);
6263

@@ -65,11 +66,23 @@ function makeOverrideReleaseTransport<TO extends BaseTransportOptions>(
6566
}
6667
return transport.send(envelope);
6768
},
68-
flush: timeout => transport.flush(timeout),
6969
};
7070
};
7171
}
7272

73+
/** Overrides the DSN in the envelope header */
74+
function overrideDsn(envelope: Envelope, dsn: string): Envelope {
75+
return createEnvelope(
76+
dsn
77+
? {
78+
...envelope[0],
79+
dsn,
80+
}
81+
: envelope[0],
82+
envelope[1],
83+
);
84+
}
85+
7386
/**
7487
* Creates a transport that can send events to different DSNs depending on the envelope contents.
7588
*/
@@ -79,26 +92,31 @@ export function makeMultiplexedTransport<TO extends BaseTransportOptions>(
7992
): (options: TO) => Transport {
8093
return options => {
8194
const fallbackTransport = createTransport(options);
82-
const otherTransports: Record<string, Transport> = {};
95+
const otherTransports = new Map<string, Transport>();
8396

84-
function getTransport(dsn: string, release: string | undefined): Transport | undefined {
97+
function getTransport(dsn: string, release: string | undefined): [string, Transport] | undefined {
8598
// We create a transport for every unique dsn/release combination as there may be code from multiple releases in
8699
// use at the same time
87100
const key = release ? `${dsn}:${release}` : dsn;
88101

89-
if (!otherTransports[key]) {
102+
let transport = otherTransports.get(key);
103+
104+
if (!transport) {
90105
const validatedDsn = dsnFromString(dsn);
91106
if (!validatedDsn) {
92107
return undefined;
93108
}
94-
const url = getEnvelopeEndpointWithUrlEncodedAuth(validatedDsn);
95109

96-
otherTransports[key] = release
110+
const url = getEnvelopeEndpointWithUrlEncodedAuth(validatedDsn, options.tunnel);
111+
112+
transport = release
97113
? makeOverrideReleaseTransport(createTransport, release)({ ...options, url })
98114
: createTransport({ ...options, url });
115+
116+
otherTransports.set(key, transport);
99117
}
100118

101-
return otherTransports[key];
119+
return [dsn, transport];
102120
}
103121

104122
async function send(envelope: Envelope): Promise<void | TransportMakeRequestResponse> {
@@ -115,22 +133,28 @@ export function makeMultiplexedTransport<TO extends BaseTransportOptions>(
115133
return getTransport(result.dsn, result.release);
116134
}
117135
})
118-
.filter((t): t is Transport => !!t);
136+
.filter((t): t is [string, Transport] => !!t);
119137

120138
// If we have no transports to send to, use the fallback transport
121139
if (transports.length === 0) {
122-
transports.push(fallbackTransport);
140+
// Don't override the DSN in the header for the fallback transport. '' is falsy
141+
transports.push(['', fallbackTransport]);
123142
}
124143

125-
const results = await Promise.all(transports.map(transport => transport.send(envelope)));
144+
const results = await Promise.all(
145+
transports.map(([dsn, transport]) => transport.send(overrideDsn(envelope, dsn))),
146+
);
126147

127148
return results[0];
128149
}
129150

130151
async function flush(timeout: number | undefined): Promise<boolean> {
131-
const allTransports = [...Object.keys(otherTransports).map(dsn => otherTransports[dsn]), fallbackTransport];
132-
const results = await Promise.all(allTransports.map(transport => transport.flush(timeout)));
133-
return results.every(r => r);
152+
const promises = [await fallbackTransport.flush(timeout)];
153+
for (const [, transport] of otherTransports) {
154+
promises.push(await transport.flush(timeout));
155+
}
156+
157+
return promises.every(r => r);
134158
}
135159

136160
return {

packages/core/test/lib/transports/multiplexed.test.ts

Lines changed: 27 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { TextDecoder, TextEncoder } from 'util';
22
import type {
33
BaseTransportOptions,
44
ClientReport,
5+
Envelope,
56
EventEnvelope,
67
EventItem,
78
TransactionEvent,
@@ -48,7 +49,7 @@ const CLIENT_REPORT_ENVELOPE = createClientReportEnvelope(
4849
123456,
4950
);
5051

51-
type Assertion = (url: string, release: string | undefined, body: string | Uint8Array) => void;
52+
type Assertion = (url: string, release: string | undefined, body: Envelope) => void;
5253

5354
const createTestTransport = (...assertions: Assertion[]): ((options: BaseTransportOptions) => Transport) => {
5455
return (options: BaseTransportOptions) =>
@@ -59,9 +60,10 @@ const createTestTransport = (...assertions: Assertion[]): ((options: BaseTranspo
5960
throw new Error('No assertion left');
6061
}
6162

62-
const event = eventFromEnvelope(parseEnvelope(request.body, new TextEncoder(), new TextDecoder()), ['event']);
63+
const env = parseEnvelope(request.body, new TextEncoder(), new TextDecoder());
64+
const event = eventFromEnvelope(env, ['event']);
6365

64-
assertion(options.url, event?.release, request.body);
66+
assertion(options.url, event?.release, env);
6567
resolve({ statusCode: 200 });
6668
});
6769
});
@@ -107,11 +109,12 @@ describe('makeMultiplexedTransport', () => {
107109
});
108110

109111
it('DSN can be overridden via match callback', async () => {
110-
expect.assertions(1);
112+
expect.assertions(2);
111113

112114
const makeTransport = makeMultiplexedTransport(
113-
createTestTransport(url => {
115+
createTestTransport((url, _, env) => {
114116
expect(url).toBe(DSN2_URL);
117+
expect(env[0].dsn).toBe(DSN2);
115118
}),
116119
() => [DSN2],
117120
);
@@ -121,12 +124,13 @@ describe('makeMultiplexedTransport', () => {
121124
});
122125

123126
it('DSN and release can be overridden via match callback', async () => {
124-
expect.assertions(2);
127+
expect.assertions(3);
125128

126129
const makeTransport = makeMultiplexedTransport(
127-
createTestTransport((url, release) => {
130+
createTestTransport((url, release, env) => {
128131
expect(url).toBe(DSN2_URL);
129132
expect(release).toBe('something@1.0.0');
133+
expect(env[0].dsn).toBe(DSN2);
130134
}),
131135
() => [{ dsn: DSN2, release: 'something@1.0.0' }],
132136
);
@@ -135,6 +139,22 @@ describe('makeMultiplexedTransport', () => {
135139
await transport.send(ERROR_ENVELOPE);
136140
});
137141

142+
it('URL can be overridden by tunnel option', async () => {
143+
expect.assertions(3);
144+
145+
const makeTransport = makeMultiplexedTransport(
146+
createTestTransport((url, release, env) => {
147+
expect(url).toBe('http://google.com');
148+
expect(release).toBe('something@1.0.0');
149+
expect(env[0].dsn).toBe(DSN2);
150+
}),
151+
() => [{ dsn: DSN2, release: 'something@1.0.0' }],
152+
);
153+
154+
const transport = makeTransport({ url: DSN1_URL, ...transportOptions, tunnel: 'http://google.com' });
155+
await transport.send(ERROR_ENVELOPE);
156+
});
157+
138158
it('match callback can return multiple DSNs', async () => {
139159
expect.assertions(2);
140160

packages/types/src/transport.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,12 @@ export type TransportMakeRequestResponse = {
1616
};
1717

1818
export interface InternalBaseTransportOptions {
19+
/**
20+
* @ignore
21+
* Users should pass the tunnel property via the init/client options.
22+
* This is only used by the SDK to pass the tunnel to the transport.
23+
*/
24+
tunnel?: string;
1925
bufferSize?: number;
2026
recordDroppedEvent: Client['recordDroppedEvent'];
2127
textEncoder?: TextEncoderInternal;

0 commit comments

Comments
 (0)