From cb72eacfec2840be46cf5576f6c4bdbbd95363fa Mon Sep 17 00:00:00 2001 From: Tim Fish Date: Fri, 26 Apr 2024 12:09:14 +0200 Subject: [PATCH 1/2] feat: Add `tunnel` support to multiplexed transport --- packages/core/src/baseclient.ts | 1 + packages/core/src/transports/multiplexed.ts | 44 +++++++++++++++------ packages/types/src/transport.ts | 6 +++ 3 files changed, 39 insertions(+), 12 deletions(-) diff --git a/packages/core/src/baseclient.ts b/packages/core/src/baseclient.ts index 0bc1e0cbb718..d08a0be3d850 100644 --- a/packages/core/src/baseclient.ts +++ b/packages/core/src/baseclient.ts @@ -140,6 +140,7 @@ export abstract class BaseClient implements Client { options._metadata ? options._metadata.sdk : undefined, ); 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 faeb5a0fbd81..d740f1fde3ec 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,30 @@ export function makeMultiplexedTransport( ): (options: TO) => Transport { return options => { const fallbackTransport = createTransport(options); - const otherTransports: Record = {}; + const otherTransports: Map = 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); + const url = getEnvelopeEndpointWithUrlEncodedAuth(validatedDsn, options.tunnel); - otherTransports[key] = release + 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,20 +132,23 @@ 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 allTransports = [...otherTransports.values(), fallbackTransport]; const results = await Promise.all(allTransports.map(transport => transport.flush(timeout))); return results.every(r => r); } diff --git a/packages/types/src/transport.ts b/packages/types/src/transport.ts index 07186b141420..39741bf111de 100644 --- a/packages/types/src/transport.ts +++ b/packages/types/src/transport.ts @@ -15,6 +15,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']; } From a4e4bb982799e61440d5d95c490d24d29eeb1dd3 Mon Sep 17 00:00:00 2001 From: Tim Fish Date: Mon, 29 Apr 2024 18:24:32 +0100 Subject: [PATCH 2/2] Add tests for tunnel and check DSN in header --- .../test/lib/transports/multiplexed.test.ts | 31 +++++++++++++++---- 1 file changed, 25 insertions(+), 6 deletions(-) diff --git a/packages/core/test/lib/transports/multiplexed.test.ts b/packages/core/test/lib/transports/multiplexed.test.ts index c2d0d2f1d318..59e71e31304f 100644 --- a/packages/core/test/lib/transports/multiplexed.test.ts +++ b/packages/core/test/lib/transports/multiplexed.test.ts @@ -1,6 +1,7 @@ import type { BaseTransportOptions, ClientReport, + Envelope, EventEnvelope, EventItem, TransactionEvent, @@ -47,7 +48,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) => @@ -60,7 +61,7 @@ const createTestTransport = (...assertions: Assertion[]): ((options: BaseTranspo const event = eventFromEnvelope(parseEnvelope(request.body), ['event']); - assertion(options.url, event?.release, request.body); + assertion(options.url, event?.release, parseEnvelope(request.body)); resolve({ statusCode: 200 }); }); }); @@ -105,11 +106,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], ); @@ -119,12 +121,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' }], ); @@ -133,6 +136,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);