-
-
Notifications
You must be signed in to change notification settings - Fork 1.7k
feat(core): Add multiplexed transport #7926
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
Merged
AbhiPrasad
merged 10 commits into
getsentry:develop
from
timfish:feat/multiplex-transport
Apr 25, 2023
Merged
Changes from all commits
Commits
Show all changes
10 commits
Select commit
Hold shift + click to select a range
89e06ea
feat(core): Add multiplexed transport
timfish 005a15d
add tests and export
timfish 3db9804
Merge branch 'develop' into feat/multiplex-transport
timfish 1da4baa
import TextEncoder
timfish f5150e4
Allow getting specific types of event and default to `event|transacti…
timfish 0951162
Use jsdoc
timfish 5aced59
Merge branch 'develop' into feat/multiplex-transport
timfish 89dbdd6
Add test case that can filter by event type
timfish bac6c0b
Merge branch 'feat/multiplex-transport' of https://github.com/timfish…
timfish 7271604
Changes from PR review
timfish File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,92 @@ | ||
import type { | ||
BaseTransportOptions, | ||
Envelope, | ||
EnvelopeItemType, | ||
Event, | ||
EventItem, | ||
Transport, | ||
TransportMakeRequestResponse, | ||
} from '@sentry/types'; | ||
import { dsnFromString, forEachEnvelopeItem } from '@sentry/utils'; | ||
|
||
import { getEnvelopeEndpointWithUrlEncodedAuth } from '../api'; | ||
|
||
interface MatchParam { | ||
/** The envelope to be sent */ | ||
envelope: Envelope; | ||
/** | ||
* A function that returns an event from the envelope if one exists. You can optionally pass an array of envelope item | ||
* types to filter by - only envelopes matching the given types will be multiplexed. | ||
* | ||
* @param types Defaults to ['event', 'transaction', 'profile', 'replay_event'] | ||
*/ | ||
getEvent(types?: EnvelopeItemType[]): Event | undefined; | ||
} | ||
|
||
type Matcher = (param: MatchParam) => string[]; | ||
|
||
function eventFromEnvelope(env: Envelope, types: EnvelopeItemType[]): Event | undefined { | ||
let event: Event | undefined; | ||
|
||
forEachEnvelopeItem(env, (item, type) => { | ||
if (types.includes(type)) { | ||
event = Array.isArray(item) ? (item as EventItem)[1] : undefined; | ||
} | ||
// bail out if we found an event | ||
return !!event; | ||
}); | ||
|
||
return event; | ||
} | ||
|
||
/** | ||
* Creates a transport that can send events to different DSNs depending on the envelope contents. | ||
*/ | ||
export function makeMultiplexedTransport<TO extends BaseTransportOptions>( | ||
createTransport: (options: TO) => Transport, | ||
matcher: Matcher, | ||
): (options: TO) => Transport { | ||
return options => { | ||
const fallbackTransport = createTransport(options); | ||
const otherTransports: Record<string, Transport> = {}; | ||
|
||
function getTransport(dsn: string): Transport { | ||
if (!otherTransports[dsn]) { | ||
const url = getEnvelopeEndpointWithUrlEncodedAuth(dsnFromString(dsn)); | ||
otherTransports[dsn] = createTransport({ ...options, url }); | ||
} | ||
|
||
return otherTransports[dsn]; | ||
} | ||
|
||
async function send(envelope: Envelope): Promise<void | TransportMakeRequestResponse> { | ||
function getEvent(types?: EnvelopeItemType[]): Event | undefined { | ||
const eventTypes: EnvelopeItemType[] = | ||
types && types.length ? types : ['event', 'transaction', 'profile', 'replay_event']; | ||
return eventFromEnvelope(envelope, eventTypes); | ||
} | ||
|
||
const transports = matcher({ envelope, getEvent }).map(dsn => getTransport(dsn)); | ||
|
||
// If we have no transports to send to, use the fallback transport | ||
if (transports.length === 0) { | ||
transports.push(fallbackTransport); | ||
} | ||
|
||
const results = await Promise.all(transports.map(transport => transport.send(envelope))); | ||
|
||
return results[0]; | ||
timfish marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
|
||
async function flush(timeout: number | undefined): Promise<boolean> { | ||
const allTransports = [...Object.keys(otherTransports).map(dsn => otherTransports[dsn]), fallbackTransport]; | ||
timfish marked this conversation as resolved.
Show resolved
Hide resolved
|
||
const results = await Promise.all(allTransports.map(transport => transport.flush(timeout))); | ||
return results.every(r => r); | ||
} | ||
|
||
return { | ||
send, | ||
flush, | ||
}; | ||
}; | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,162 @@ | ||
import type { BaseTransportOptions, ClientReport, EventEnvelope, EventItem, Transport } from '@sentry/types'; | ||
import { createClientReportEnvelope, createEnvelope, dsnFromString } from '@sentry/utils'; | ||
import { TextEncoder } from 'util'; | ||
|
||
import { createTransport, getEnvelopeEndpointWithUrlEncodedAuth, makeMultiplexedTransport } from '../../../src'; | ||
|
||
const DSN1 = 'https://[email protected]/4321'; | ||
const DSN1_URL = getEnvelopeEndpointWithUrlEncodedAuth(dsnFromString(DSN1)); | ||
|
||
const DSN2 = 'https://[email protected]/8765'; | ||
const DSN2_URL = getEnvelopeEndpointWithUrlEncodedAuth(dsnFromString(DSN2)); | ||
|
||
const ERROR_EVENT = { event_id: 'aa3ff046696b4bc6b609ce6d28fde9e2' }; | ||
const ERROR_ENVELOPE = createEnvelope<EventEnvelope>({ event_id: 'aa3ff046696b4bc6b609ce6d28fde9e2', sent_at: '123' }, [ | ||
[{ type: 'event' }, ERROR_EVENT] as EventItem, | ||
]); | ||
|
||
const TRANSACTION_ENVELOPE = createEnvelope<EventEnvelope>( | ||
{ event_id: 'aa3ff046696b4bc6b609ce6d28fde9e2', sent_at: '123' }, | ||
[[{ type: 'transaction' }, { event_id: 'aa3ff046696b4bc6b609ce6d28fde9e2' }] as EventItem], | ||
); | ||
|
||
const DEFAULT_DISCARDED_EVENTS: ClientReport['discarded_events'] = [ | ||
{ | ||
reason: 'before_send', | ||
category: 'error', | ||
quantity: 30, | ||
}, | ||
{ | ||
reason: 'network_error', | ||
category: 'transaction', | ||
quantity: 23, | ||
}, | ||
]; | ||
|
||
const CLIENT_REPORT_ENVELOPE = createClientReportEnvelope( | ||
DEFAULT_DISCARDED_EVENTS, | ||
'https://[email protected]/1337', | ||
123456, | ||
); | ||
|
||
type Assertion = (url: string, body: string | Uint8Array) => void; | ||
|
||
const createTestTransport = (...assertions: Assertion[]): ((options: BaseTransportOptions) => Transport) => { | ||
return (options: BaseTransportOptions) => | ||
createTransport(options, request => { | ||
return new Promise(resolve => { | ||
const assertion = assertions.shift(); | ||
if (!assertion) { | ||
throw new Error('No assertion left'); | ||
} | ||
assertion(options.url, request.body); | ||
resolve({ statusCode: 200 }); | ||
}); | ||
}); | ||
}; | ||
|
||
const transportOptions = { | ||
recordDroppedEvent: () => undefined, // noop | ||
textEncoder: new TextEncoder(), | ||
}; | ||
|
||
describe('makeMultiplexedTransport', () => { | ||
it('Falls back to options DSN when no match', async () => { | ||
expect.assertions(1); | ||
|
||
const makeTransport = makeMultiplexedTransport( | ||
createTestTransport(url => { | ||
expect(url).toBe(DSN1_URL); | ||
}), | ||
() => [], | ||
); | ||
|
||
const transport = makeTransport({ url: DSN1_URL, ...transportOptions }); | ||
await transport.send(ERROR_ENVELOPE); | ||
}); | ||
|
||
it('DSN can be overridden via match callback', async () => { | ||
expect.assertions(1); | ||
|
||
const makeTransport = makeMultiplexedTransport( | ||
createTestTransport(url => { | ||
expect(url).toBe(DSN2_URL); | ||
}), | ||
() => [DSN2], | ||
); | ||
|
||
const transport = makeTransport({ url: DSN1_URL, ...transportOptions }); | ||
await transport.send(ERROR_ENVELOPE); | ||
}); | ||
|
||
it('match callback can return multiple DSNs', async () => { | ||
expect.assertions(2); | ||
|
||
const makeTransport = makeMultiplexedTransport( | ||
createTestTransport( | ||
url => { | ||
expect(url).toBe(DSN1_URL); | ||
}, | ||
url => { | ||
expect(url).toBe(DSN2_URL); | ||
}, | ||
), | ||
() => [DSN1, DSN2], | ||
); | ||
|
||
const transport = makeTransport({ url: DSN1_URL, ...transportOptions }); | ||
await transport.send(ERROR_ENVELOPE); | ||
}); | ||
|
||
it('callback getEvent returns event', async () => { | ||
expect.assertions(3); | ||
|
||
const makeTransport = makeMultiplexedTransport( | ||
createTestTransport(url => { | ||
expect(url).toBe(DSN2_URL); | ||
}), | ||
({ envelope, getEvent }) => { | ||
expect(envelope).toBe(ERROR_ENVELOPE); | ||
expect(getEvent()).toBe(ERROR_EVENT); | ||
return [DSN2]; | ||
}, | ||
); | ||
|
||
const transport = makeTransport({ url: DSN1_URL, ...transportOptions }); | ||
await transport.send(ERROR_ENVELOPE); | ||
}); | ||
|
||
it('callback getEvent returns undefined if not event', async () => { | ||
expect.assertions(2); | ||
|
||
const makeTransport = makeMultiplexedTransport( | ||
createTestTransport(url => { | ||
expect(url).toBe(DSN2_URL); | ||
}), | ||
({ getEvent }) => { | ||
expect(getEvent()).toBeUndefined(); | ||
return [DSN2]; | ||
}, | ||
); | ||
|
||
const transport = makeTransport({ url: DSN1_URL, ...transportOptions }); | ||
await transport.send(CLIENT_REPORT_ENVELOPE); | ||
}); | ||
|
||
it('callback getEvent can ignore transactions', async () => { | ||
expect.assertions(2); | ||
|
||
const makeTransport = makeMultiplexedTransport( | ||
createTestTransport(url => { | ||
expect(url).toBe(DSN2_URL); | ||
}), | ||
({ getEvent }) => { | ||
expect(getEvent(['event'])).toBeUndefined(); | ||
return [DSN2]; | ||
}, | ||
); | ||
|
||
const transport = makeTransport({ url: DSN1_URL, ...transportOptions }); | ||
await transport.send(TRANSACTION_ENVELOPE); | ||
}); | ||
}); |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.