Skip to content

Commit c2bd091

Browse files
authored
feat(core): Allow multiplexed transport to send to multiple releases (#8559)
The multiplexed transport can already route events to different or multiple DSNs but we also need to be able to route to specific releases too. In a page with micro-frontends, it's possible (and probably even quite common) to be using the same dependency multiple times but different versions (ie. different releases). Depending on where an error occurs we might want to send an event to `[email protected]` or `[email protected]` at the same DSN. This PR: - Adds a private `makeOverrideReleaseTransport` which can used to wrap a transport and override the release on all events - Modifies `makeMultiplexedTransport` so it now creates a transport for each unique dsn/release pair - And uses `makeOverrideReleaseTransport` whenever a release is returned from the callback
1 parent 6b009c0 commit c2bd091

File tree

2 files changed

+73
-11
lines changed

2 files changed

+73
-11
lines changed

packages/core/src/transports/multiplexed.ts

Lines changed: 50 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,15 @@ interface MatchParam {
2424
getEvent(types?: EnvelopeItemType[]): Event | undefined;
2525
}
2626

27-
type Matcher = (param: MatchParam) => string[];
27+
type RouteTo = { dsn: string; release: string };
28+
type Matcher = (param: MatchParam) => (string | RouteTo)[];
2829

29-
function eventFromEnvelope(env: Envelope, types: EnvelopeItemType[]): Event | undefined {
30+
/**
31+
* Gets an event from an envelope.
32+
*
33+
* This is only exported for use in the tests
34+
*/
35+
export function eventFromEnvelope(env: Envelope, types: EnvelopeItemType[]): Event | undefined {
3036
let event: Event | undefined;
3137

3238
forEachEnvelopeItem(env, (item, type) => {
@@ -40,6 +46,30 @@ function eventFromEnvelope(env: Envelope, types: EnvelopeItemType[]): Event | un
4046
return event;
4147
}
4248

49+
/**
50+
* Creates a transport that overrides the release on all events.
51+
*/
52+
function makeOverrideReleaseTransport<TO extends BaseTransportOptions>(
53+
createTransport: (options: TO) => Transport,
54+
release: string,
55+
): (options: TO) => Transport {
56+
return options => {
57+
const transport = createTransport(options);
58+
59+
return {
60+
send: async (envelope: Envelope): Promise<void | TransportMakeRequestResponse> => {
61+
const event = eventFromEnvelope(envelope, ['event', 'transaction', 'profile', 'replay_event']);
62+
63+
if (event) {
64+
event.release = release;
65+
}
66+
return transport.send(envelope);
67+
},
68+
flush: timeout => transport.flush(timeout),
69+
};
70+
};
71+
}
72+
4373
/**
4474
* Creates a transport that can send events to different DSNs depending on the envelope contents.
4575
*/
@@ -51,17 +81,24 @@ export function makeMultiplexedTransport<TO extends BaseTransportOptions>(
5181
const fallbackTransport = createTransport(options);
5282
const otherTransports: Record<string, Transport> = {};
5383

54-
function getTransport(dsn: string): Transport | undefined {
55-
if (!otherTransports[dsn]) {
84+
function getTransport(dsn: string, release: string | undefined): Transport | undefined {
85+
// We create a transport for every unique dsn/release combination as there may be code from multiple releases in
86+
// use at the same time
87+
const key = release ? `${dsn}:${release}` : dsn;
88+
89+
if (!otherTransports[key]) {
5690
const validatedDsn = dsnFromString(dsn);
5791
if (!validatedDsn) {
5892
return undefined;
5993
}
6094
const url = getEnvelopeEndpointWithUrlEncodedAuth(validatedDsn);
61-
otherTransports[dsn] = createTransport({ ...options, url });
95+
96+
otherTransports[key] = release
97+
? makeOverrideReleaseTransport(createTransport, release)({ ...options, url })
98+
: createTransport({ ...options, url });
6299
}
63100

64-
return otherTransports[dsn];
101+
return otherTransports[key];
65102
}
66103

67104
async function send(envelope: Envelope): Promise<void | TransportMakeRequestResponse> {
@@ -71,7 +108,13 @@ export function makeMultiplexedTransport<TO extends BaseTransportOptions>(
71108
}
72109

73110
const transports = matcher({ envelope, getEvent })
74-
.map(dsn => getTransport(dsn))
111+
.map(result => {
112+
if (typeof result === 'string') {
113+
return getTransport(result, undefined);
114+
} else {
115+
return getTransport(result.dsn, result.release);
116+
}
117+
})
75118
.filter((t): t is Transport => !!t);
76119

77120
// If we have no transports to send to, use the fallback transport

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

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,11 @@ import type {
66
TransactionEvent,
77
Transport,
88
} from '@sentry/types';
9-
import { createClientReportEnvelope, createEnvelope, dsnFromString } from '@sentry/utils';
10-
import { TextEncoder } from 'util';
9+
import { createClientReportEnvelope, createEnvelope, dsnFromString, parseEnvelope } from '@sentry/utils';
10+
import { TextDecoder, TextEncoder } from 'util';
1111

1212
import { createTransport, getEnvelopeEndpointWithUrlEncodedAuth, makeMultiplexedTransport } from '../../../src';
13+
import { eventFromEnvelope } from '../../../src/transports/multiplexed';
1314

1415
const DSN1 = 'https://[email protected]/4321';
1516
const DSN1_URL = getEnvelopeEndpointWithUrlEncodedAuth(dsnFromString(DSN1)!);
@@ -47,7 +48,7 @@ const CLIENT_REPORT_ENVELOPE = createClientReportEnvelope(
4748
123456,
4849
);
4950

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

5253
const createTestTransport = (...assertions: Assertion[]): ((options: BaseTransportOptions) => Transport) => {
5354
return (options: BaseTransportOptions) =>
@@ -57,7 +58,10 @@ const createTestTransport = (...assertions: Assertion[]): ((options: BaseTranspo
5758
if (!assertion) {
5859
throw new Error('No assertion left');
5960
}
60-
assertion(options.url, request.body);
61+
62+
const event = eventFromEnvelope(parseEnvelope(request.body, new TextEncoder(), new TextDecoder()), ['event']);
63+
64+
assertion(options.url, event?.release, request.body);
6165
resolve({ statusCode: 200 });
6266
});
6367
});
@@ -111,6 +115,21 @@ describe('makeMultiplexedTransport', () => {
111115
await transport.send(ERROR_ENVELOPE);
112116
});
113117

118+
it('DSN and release can be overridden via match callback', async () => {
119+
expect.assertions(2);
120+
121+
const makeTransport = makeMultiplexedTransport(
122+
createTestTransport((url, release) => {
123+
expect(url).toBe(DSN2_URL);
124+
expect(release).toBe('[email protected]');
125+
}),
126+
() => [{ dsn: DSN2, release: '[email protected]' }],
127+
);
128+
129+
const transport = makeTransport({ url: DSN1_URL, ...transportOptions });
130+
await transport.send(ERROR_ENVELOPE);
131+
});
132+
114133
it('match callback can return multiple DSNs', async () => {
115134
expect.assertions(2);
116135

0 commit comments

Comments
 (0)