Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(v7): Add tunnel support to multiplexed transport #11851

Merged
merged 2 commits into from
May 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/core/src/baseclient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,7 @@ export abstract class BaseClient<O extends ClientOptions> implements Client<O> {
if (this._dsn) {
const url = getEnvelopeEndpointWithUrlEncodedAuth(this._dsn, options);
this._transport = options.transport({
tunnel: this._options.tunnel,
recordDroppedEvent: this.recordDroppedEvent.bind(this),
...options.transportOptions,
url,
Expand Down
52 changes: 38 additions & 14 deletions packages/core/src/transports/multiplexed.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import type {
Transport,
TransportMakeRequestResponse,
} from '@sentry/types';
import { dsnFromString, forEachEnvelopeItem } from '@sentry/utils';
import { createEnvelope, dsnFromString, forEachEnvelopeItem } from '@sentry/utils';

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

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

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

Expand All @@ -65,11 +66,23 @@ function makeOverrideReleaseTransport<TO extends BaseTransportOptions>(
}
return transport.send(envelope);
},
flush: timeout => transport.flush(timeout),
};
};
}

/** Overrides the DSN in the envelope header */
function overrideDsn(envelope: Envelope, dsn: string): Envelope {
return createEnvelope(
dsn
? {
...envelope[0],
dsn,
}
: envelope[0],
envelope[1],
);
}

/**
* Creates a transport that can send events to different DSNs depending on the envelope contents.
*/
Expand All @@ -79,26 +92,31 @@ export function makeMultiplexedTransport<TO extends BaseTransportOptions>(
): (options: TO) => Transport {
return options => {
const fallbackTransport = createTransport(options);
const otherTransports: Record<string, Transport> = {};
const otherTransports = new Map<string, Transport>();

function getTransport(dsn: string, release: string | undefined): Transport | undefined {
function getTransport(dsn: string, release: string | undefined): [string, 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]) {
let transport = otherTransports.get(key);

if (!transport) {
const validatedDsn = dsnFromString(dsn);
if (!validatedDsn) {
return undefined;
}
const url = getEnvelopeEndpointWithUrlEncodedAuth(validatedDsn);

otherTransports[key] = release
const url = getEnvelopeEndpointWithUrlEncodedAuth(validatedDsn, options.tunnel);

transport = release
? makeOverrideReleaseTransport(createTransport, release)({ ...options, url })
: createTransport({ ...options, url });

otherTransports.set(key, transport);
}

return otherTransports[key];
return [dsn, transport];
}

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

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

const results = await Promise.all(transports.map(transport => transport.send(envelope)));
const results = await Promise.all(
transports.map(([dsn, transport]) => transport.send(overrideDsn(envelope, dsn))),
);

return results[0];
}

async function flush(timeout: number | undefined): Promise<boolean> {
const allTransports = [...Object.keys(otherTransports).map(dsn => otherTransports[dsn]), fallbackTransport];
const results = await Promise.all(allTransports.map(transport => transport.flush(timeout)));
return results.every(r => r);
const promises = [await fallbackTransport.flush(timeout)];
for (const [, transport] of otherTransports) {
promises.push(await transport.flush(timeout));
}

return promises.every(r => r);
}

return {
Expand Down
34 changes: 27 additions & 7 deletions packages/core/test/lib/transports/multiplexed.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { TextDecoder, TextEncoder } from 'util';
import type {
BaseTransportOptions,
ClientReport,
Envelope,
EventEnvelope,
EventItem,
TransactionEvent,
Expand Down Expand Up @@ -48,7 +49,7 @@ const CLIENT_REPORT_ENVELOPE = createClientReportEnvelope(
123456,
);

type Assertion = (url: string, release: string | undefined, body: string | Uint8Array) => void;
type Assertion = (url: string, release: string | undefined, body: Envelope) => void;

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

const event = eventFromEnvelope(parseEnvelope(request.body, new TextEncoder(), new TextDecoder()), ['event']);
const env = parseEnvelope(request.body, new TextEncoder(), new TextDecoder());
const event = eventFromEnvelope(env, ['event']);

assertion(options.url, event?.release, request.body);
assertion(options.url, event?.release, env);
resolve({ statusCode: 200 });
});
});
Expand Down Expand Up @@ -107,11 +109,12 @@ describe('makeMultiplexedTransport', () => {
});

it('DSN can be overridden via match callback', async () => {
expect.assertions(1);
expect.assertions(2);

const makeTransport = makeMultiplexedTransport(
createTestTransport(url => {
createTestTransport((url, _, env) => {
expect(url).toBe(DSN2_URL);
expect(env[0].dsn).toBe(DSN2);
}),
() => [DSN2],
);
Expand All @@ -121,12 +124,13 @@ describe('makeMultiplexedTransport', () => {
});

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

const makeTransport = makeMultiplexedTransport(
createTestTransport((url, release) => {
createTestTransport((url, release, env) => {
expect(url).toBe(DSN2_URL);
expect(release).toBe('something@1.0.0');
expect(env[0].dsn).toBe(DSN2);
}),
() => [{ dsn: DSN2, release: 'something@1.0.0' }],
);
Expand All @@ -135,6 +139,22 @@ describe('makeMultiplexedTransport', () => {
await transport.send(ERROR_ENVELOPE);
});

it('URL can be overridden by tunnel option', async () => {
expect.assertions(3);

const makeTransport = makeMultiplexedTransport(
createTestTransport((url, release, env) => {
expect(url).toBe('http://google.com');
expect(release).toBe('something@1.0.0');
expect(env[0].dsn).toBe(DSN2);
}),
() => [{ dsn: DSN2, release: 'something@1.0.0' }],
);

const transport = makeTransport({ url: DSN1_URL, ...transportOptions, tunnel: 'http://google.com' });
await transport.send(ERROR_ENVELOPE);
});

it('match callback can return multiple DSNs', async () => {
expect.assertions(2);

Expand Down
6 changes: 6 additions & 0 deletions packages/types/src/transport.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,12 @@ export type TransportMakeRequestResponse = {
};

export interface InternalBaseTransportOptions {
/**
* @ignore
* Users should pass the tunnel property via the init/client options.
* This is only used by the SDK to pass the tunnel to the transport.
*/
tunnel?: string;
bufferSize?: number;
recordDroppedEvent: Client['recordDroppedEvent'];
textEncoder?: TextEncoderInternal;
Expand Down
Loading