diff --git a/packages/core/src/transports/multiplexed.ts b/packages/core/src/transports/multiplexed.ts index c7045f4b2686..a496a8adcd6f 100644 --- a/packages/core/src/transports/multiplexed.ts +++ b/packages/core/src/transports/multiplexed.ts @@ -24,9 +24,15 @@ interface MatchParam { getEvent(types?: EnvelopeItemType[]): Event | undefined; } -type Matcher = (param: MatchParam) => string[]; +type RouteTo = { dsn: string; release: string }; +type Matcher = (param: MatchParam) => (string | RouteTo)[]; -function eventFromEnvelope(env: Envelope, types: EnvelopeItemType[]): Event | undefined { +/** + * Gets an event from an envelope. + * + * This is only exported for use in the tests + */ +export function eventFromEnvelope(env: Envelope, types: EnvelopeItemType[]): Event | undefined { let event: Event | undefined; forEachEnvelopeItem(env, (item, type) => { @@ -40,6 +46,30 @@ function eventFromEnvelope(env: Envelope, types: EnvelopeItemType[]): Event | un return event; } +/** + * Creates a transport that overrides the release on all events. + */ +function makeOverrideReleaseTransport( + createTransport: (options: TO) => Transport, + release: string, +): (options: TO) => Transport { + return options => { + const transport = createTransport(options); + + return { + send: async (envelope: Envelope): Promise => { + const event = eventFromEnvelope(envelope, ['event', 'transaction', 'profile', 'replay_event']); + + if (event) { + event.release = release; + } + return transport.send(envelope); + }, + flush: timeout => transport.flush(timeout), + }; + }; +} + /** * Creates a transport that can send events to different DSNs depending on the envelope contents. */ @@ -51,17 +81,24 @@ export function makeMultiplexedTransport( const fallbackTransport = createTransport(options); const otherTransports: Record = {}; - function getTransport(dsn: string): Transport | undefined { - if (!otherTransports[dsn]) { + function getTransport(dsn: string, release: string | undefined): Transport | undefined { + // We create a transport for every unique dsn/release combination as there may be code from multiple releases in + // use at the same time + const key = release ? `${dsn}:${release}` : dsn; + + if (!otherTransports[key]) { const validatedDsn = dsnFromString(dsn); if (!validatedDsn) { return undefined; } const url = getEnvelopeEndpointWithUrlEncodedAuth(validatedDsn); - otherTransports[dsn] = createTransport({ ...options, url }); + + otherTransports[key] = release + ? makeOverrideReleaseTransport(createTransport, release)({ ...options, url }) + : createTransport({ ...options, url }); } - return otherTransports[dsn]; + return otherTransports[key]; } async function send(envelope: Envelope): Promise { @@ -71,7 +108,13 @@ export function makeMultiplexedTransport( } const transports = matcher({ envelope, getEvent }) - .map(dsn => getTransport(dsn)) + .map(result => { + if (typeof result === 'string') { + return getTransport(result, undefined); + } else { + return getTransport(result.dsn, result.release); + } + }) .filter((t): t is Transport => !!t); // If we have no transports to send to, use the fallback transport diff --git a/packages/core/test/lib/transports/multiplexed.test.ts b/packages/core/test/lib/transports/multiplexed.test.ts index 2d2dcb5ce46d..446d185f9301 100644 --- a/packages/core/test/lib/transports/multiplexed.test.ts +++ b/packages/core/test/lib/transports/multiplexed.test.ts @@ -6,10 +6,11 @@ import type { TransactionEvent, Transport, } from '@sentry/types'; -import { createClientReportEnvelope, createEnvelope, dsnFromString } from '@sentry/utils'; -import { TextEncoder } from 'util'; +import { createClientReportEnvelope, createEnvelope, dsnFromString, parseEnvelope } from '@sentry/utils'; +import { TextDecoder, TextEncoder } from 'util'; import { createTransport, getEnvelopeEndpointWithUrlEncodedAuth, makeMultiplexedTransport } from '../../../src'; +import { eventFromEnvelope } from '../../../src/transports/multiplexed'; const DSN1 = 'https://1234@5678.ingest.sentry.io/4321'; const DSN1_URL = getEnvelopeEndpointWithUrlEncodedAuth(dsnFromString(DSN1)!); @@ -47,7 +48,7 @@ const CLIENT_REPORT_ENVELOPE = createClientReportEnvelope( 123456, ); -type Assertion = (url: string, body: string | Uint8Array) => void; +type Assertion = (url: string, release: string | undefined, body: string | Uint8Array) => void; const createTestTransport = (...assertions: Assertion[]): ((options: BaseTransportOptions) => Transport) => { return (options: BaseTransportOptions) => @@ -57,7 +58,10 @@ const createTestTransport = (...assertions: Assertion[]): ((options: BaseTranspo if (!assertion) { throw new Error('No assertion left'); } - assertion(options.url, request.body); + + const event = eventFromEnvelope(parseEnvelope(request.body, new TextEncoder(), new TextDecoder()), ['event']); + + assertion(options.url, event?.release, request.body); resolve({ statusCode: 200 }); }); }); @@ -111,6 +115,21 @@ describe('makeMultiplexedTransport', () => { await transport.send(ERROR_ENVELOPE); }); + it('DSN and release can be overridden via match callback', async () => { + expect.assertions(2); + + const makeTransport = makeMultiplexedTransport( + createTestTransport((url, release) => { + expect(url).toBe(DSN2_URL); + expect(release).toBe('something@1.0.0'); + }), + () => [{ dsn: DSN2, release: 'something@1.0.0' }], + ); + + const transport = makeTransport({ url: DSN1_URL, ...transportOptions }); + await transport.send(ERROR_ENVELOPE); + }); + it('match callback can return multiple DSNs', async () => { expect.assertions(2);