diff --git a/packages/core/src/baseclient.ts b/packages/core/src/baseclient.ts index b7f00e14baef..5a5c748b4282 100644 --- a/packages/core/src/baseclient.ts +++ b/packages/core/src/baseclient.ts @@ -150,6 +150,7 @@ export abstract class BaseClient implements Client { 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, diff --git a/packages/core/src/transports/multiplexed.ts b/packages/core/src/transports/multiplexed.ts index a496a8adcd6f..cfd86c6bfa73 100644 --- a/packages/core/src/transports/multiplexed.ts +++ b/packages/core/src/transports/multiplexed.ts @@ -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'; @@ -57,6 +57,7 @@ function makeOverrideReleaseTransport( const transport = createTransport(options); return { + ...transport, send: async (envelope: Envelope): Promise => { const event = eventFromEnvelope(envelope, ['event', 'transaction', 'profile', 'replay_event']); @@ -65,11 +66,23 @@ function makeOverrideReleaseTransport( } 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. */ @@ -79,26 +92,31 @@ export function makeMultiplexedTransport( ): (options: TO) => Transport { return options => { const fallbackTransport = createTransport(options); - const otherTransports: Record = {}; + const otherTransports = new Map(); - 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 { @@ -115,22 +133,28 @@ export function makeMultiplexedTransport( 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 { - 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 { diff --git a/packages/core/test/lib/transports/multiplexed.test.ts b/packages/core/test/lib/transports/multiplexed.test.ts index 097b3428805b..d3d5c922a5c4 100644 --- a/packages/core/test/lib/transports/multiplexed.test.ts +++ b/packages/core/test/lib/transports/multiplexed.test.ts @@ -2,6 +2,7 @@ import { TextDecoder, TextEncoder } from 'util'; import type { BaseTransportOptions, ClientReport, + Envelope, EventEnvelope, EventItem, TransactionEvent, @@ -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) => @@ -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 }); }); }); @@ -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], ); @@ -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' }], ); @@ -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); diff --git a/packages/types/src/transport.ts b/packages/types/src/transport.ts index 05638b67228e..3b587835afa4 100644 --- a/packages/types/src/transport.ts +++ b/packages/types/src/transport.ts @@ -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;