Skip to content

Commit

Permalink
feat(core): Allow multiplexed transport to send to multiple releases (#…
Browse files Browse the repository at this point in the history
…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 `cool-internal-components@1.0.0-beta` or
`cool-internal-components@0.9.0` 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
  • Loading branch information
timfish committed Jul 17, 2023
1 parent 6b009c0 commit c2bd091
Show file tree
Hide file tree
Showing 2 changed files with 73 additions and 11 deletions.
57 changes: 50 additions & 7 deletions packages/core/src/transports/multiplexed.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand All @@ -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<TO extends BaseTransportOptions>(
createTransport: (options: TO) => Transport,
release: string,
): (options: TO) => Transport {
return options => {
const transport = createTransport(options);

return {
send: async (envelope: Envelope): Promise<void | TransportMakeRequestResponse> => {
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.
*/
Expand All @@ -51,17 +81,24 @@ export function makeMultiplexedTransport<TO extends BaseTransportOptions>(
const fallbackTransport = createTransport(options);
const otherTransports: Record<string, Transport> = {};

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<void | TransportMakeRequestResponse> {
Expand All @@ -71,7 +108,13 @@ export function makeMultiplexedTransport<TO extends BaseTransportOptions>(
}

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
Expand Down
27 changes: 23 additions & 4 deletions packages/core/test/lib/transports/multiplexed.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)!);
Expand Down Expand Up @@ -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) =>
Expand All @@ -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 });
});
});
Expand Down Expand Up @@ -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);

Expand Down

0 comments on commit c2bd091

Please sign in to comment.