-
-
Notifications
You must be signed in to change notification settings - Fork 1.8k
feat(core): consolidate fetch integrations #24346
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
Open
isaacs
wants to merge
5
commits into
develop
Choose a base branch
from
isaacschlueter/js-3667-consolidate-the-global-fetch-integrations-into-sentryserver
base: develop
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
3b254be
feat(core): consolidate 4 fetch integrations
isaacs 5be3ad5
fixup! feat(core): consolidate 4 fetch integrations
isaacs a4cc50b
fix(core): redact query strings in fetch instrumentation
isaacs 627b5cc
fix: add fetch instrumentation to @sentry/core/server
isaacs f065fd6
fix: empty URL should not provide the breadcrumb "undefined"
isaacs 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
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 |
|---|---|---|
| @@ -1,166 +1,10 @@ | ||
| import type { | ||
| Client, | ||
| FetchBreadcrumbData, | ||
| FetchBreadcrumbHint, | ||
| HandlerDataFetch, | ||
| IntegrationFn, | ||
| Span, | ||
| } from '@sentry/core'; | ||
| import { | ||
| addBreadcrumb, | ||
| addFetchInstrumentationHandler, | ||
| defineIntegration, | ||
| getBreadcrumbLogLevelFromHttpStatusCode, | ||
| getClient, | ||
| instrumentFetchRequest, | ||
| isSentryRequestUrl, | ||
| LRUMap, | ||
| shouldPropagateTraceForUrl, | ||
| } from '@sentry/core'; | ||
|
|
||
| const INTEGRATION_NAME = 'Fetch' as const; | ||
|
|
||
| const HAS_CLIENT_MAP = new WeakMap<Client, boolean>(); | ||
|
|
||
| interface FetchOptions { | ||
| /** | ||
| * Whether breadcrumbs should be recorded for requests. | ||
| * Defaults to true. | ||
| */ | ||
| breadcrumbs?: boolean; | ||
|
|
||
| /** | ||
| * Function determining whether or not to create spans to track outgoing requests to the given URL. | ||
| * By default, spans will be created for all outgoing requests. | ||
| */ | ||
| shouldCreateSpanForRequest?: (url: string) => boolean; | ||
| } | ||
|
|
||
| const _fetchIntegration = ((options: FetchOptions = {}) => { | ||
| const breadcrumbs = options.breadcrumbs === undefined ? true : options.breadcrumbs; | ||
| const shouldCreateSpanForRequest = options.shouldCreateSpanForRequest; | ||
|
|
||
| const _createSpanUrlMap = new LRUMap<string, boolean>(100); | ||
| const _headersUrlMap = new LRUMap<string, boolean>(100); | ||
|
|
||
| const spans: Record<string, Span> = {}; | ||
|
|
||
| /** Decides whether to attach trace data to the outgoing fetch request */ | ||
| function _shouldAttachTraceData(url: string): boolean { | ||
| const client = getClient(); | ||
|
|
||
| if (!client) { | ||
| return false; | ||
| } | ||
|
|
||
| return shouldPropagateTraceForUrl(url, client.getOptions().tracePropagationTargets, _headersUrlMap); | ||
| } | ||
|
|
||
| /** Helper that wraps shouldCreateSpanForRequest option */ | ||
| function _shouldCreateSpan(url: string): boolean { | ||
| if (shouldCreateSpanForRequest === undefined) { | ||
| return true; | ||
| } | ||
|
|
||
| const cachedDecision = _createSpanUrlMap.get(url); | ||
| if (cachedDecision !== undefined) { | ||
| return cachedDecision; | ||
| } | ||
|
|
||
| const decision = shouldCreateSpanForRequest(url); | ||
| _createSpanUrlMap.set(url, decision); | ||
| return decision; | ||
| } | ||
|
|
||
| return { | ||
| name: INTEGRATION_NAME, | ||
| setupOnce() { | ||
| addFetchInstrumentationHandler(handlerData => { | ||
| const client = getClient(); | ||
| if (!client || !HAS_CLIENT_MAP.get(client)) { | ||
| return; | ||
| } | ||
| const { propagateTraceparent } = client.getOptions(); | ||
|
|
||
| if (isSentryRequestUrl(handlerData.fetchData.url, client)) { | ||
| return; | ||
| } | ||
|
|
||
| instrumentFetchRequest(handlerData, _shouldCreateSpan, _shouldAttachTraceData, spans, { | ||
| spanOrigin: 'auto.http.fetch', | ||
| propagateTraceparent, | ||
| }); | ||
|
|
||
| if (breadcrumbs) { | ||
| createBreadcrumb(handlerData); | ||
| } | ||
| }); | ||
| }, | ||
| setup(client) { | ||
| HAS_CLIENT_MAP.set(client, true); | ||
| }, | ||
| }; | ||
| }) satisfies IntegrationFn; | ||
| import { createFetchIntegration } from '@sentry/core'; | ||
|
|
||
| /** | ||
| * Instruments outgoing `fetch` requests in Bun: creates spans, records breadcrumbs and | ||
| * attaches trace propagation headers. | ||
| */ | ||
| export const fetchIntegration = defineIntegration(_fetchIntegration); | ||
|
|
||
| function createBreadcrumb(handlerData: HandlerDataFetch): void { | ||
| const { startTimestamp, endTimestamp } = handlerData; | ||
|
|
||
| // We only capture complete fetch requests | ||
| if (!endTimestamp) { | ||
| return; | ||
| } | ||
|
|
||
| const breadcrumbData: FetchBreadcrumbData = { | ||
| method: handlerData.fetchData.method, | ||
| url: handlerData.fetchData.url, | ||
| }; | ||
|
|
||
| if (handlerData.error) { | ||
| const hint: FetchBreadcrumbHint = { | ||
| data: handlerData.error, | ||
| input: handlerData.args, | ||
| startTimestamp, | ||
| endTimestamp, | ||
| }; | ||
|
|
||
| addBreadcrumb( | ||
| { | ||
| category: 'fetch', | ||
| data: breadcrumbData, | ||
| level: 'error', | ||
| type: 'http', | ||
| }, | ||
| hint, | ||
| ); | ||
| } else { | ||
| const response = handlerData.response as Response | undefined; | ||
|
|
||
| breadcrumbData.request_body_size = handlerData.fetchData.request_body_size; | ||
| breadcrumbData.response_body_size = handlerData.fetchData.response_body_size; | ||
| breadcrumbData.status_code = response?.status; | ||
|
|
||
| const hint: FetchBreadcrumbHint = { | ||
| input: handlerData.args, | ||
| response, | ||
| startTimestamp, | ||
| endTimestamp, | ||
| }; | ||
| const level = getBreadcrumbLogLevelFromHttpStatusCode(breadcrumbData.status_code); | ||
|
|
||
| addBreadcrumb( | ||
| { | ||
| category: 'fetch', | ||
| data: breadcrumbData, | ||
| type: 'http', | ||
| level, | ||
| }, | ||
| hint, | ||
| ); | ||
| } | ||
| } | ||
| export const fetchIntegration = createFetchIntegration({ | ||
| name: 'Fetch', | ||
| spanOrigin: 'auto.http.fetch', | ||
| }); |
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,100 @@ | ||
| import http from 'node:http'; | ||
| import type { TransactionEvent } from '@sentry/core'; | ||
| import { getCurrentScope, getIsolationScope, startSpan } from '@sentry/core'; | ||
| import { afterAll, beforeAll, describe, expect, test } from 'bun:test'; | ||
| import { init } from '../../src'; | ||
|
|
||
| async function startServer( | ||
| handler: (req: http.IncomingMessage, res: http.ServerResponse) => void, | ||
| ): Promise<{ port: number; close: () => Promise<void> }> { | ||
| const server = http.createServer(handler); | ||
| const port = await new Promise<number>(resolve => { | ||
| server.listen(0, () => resolve((server.address() as { port: number }).port)); | ||
| }); | ||
| return { | ||
| port, | ||
| close: () => new Promise<void>(resolve => server.close(() => resolve())), | ||
| }; | ||
| } | ||
|
|
||
| const transactions: TransactionEvent[] = []; | ||
|
|
||
| /** Bind on the real completion signal so a "never arrives" regression fails instead of hanging. */ | ||
| function waitForTransaction(name: string): Promise<TransactionEvent> { | ||
| return new Promise((resolve, reject) => { | ||
| const timer = setTimeout(() => reject(new Error(`Timed out waiting for the "${name}" transaction`)), 5000); | ||
| const poll = setInterval(() => { | ||
| const found = transactions.find(event => event.transaction === name); | ||
| if (found) { | ||
| clearTimeout(timer); | ||
| clearInterval(poll); | ||
| resolve(found); | ||
| } | ||
| }, 10); | ||
| }); | ||
| } | ||
|
|
||
| function header(headers: http.IncomingHttpHeaders | undefined, name: string): string | undefined { | ||
| const value = headers?.[name]; | ||
| return Array.isArray(value) ? value[0] : value; | ||
| } | ||
|
|
||
| describe('fetchIntegration', () => { | ||
| beforeAll(() => { | ||
| init({ | ||
| dsn: 'https://public@dsn.ingest.sentry.io/1337', | ||
| tracesSampleRate: 1.0, | ||
| traceLifecycle: 'static', | ||
| beforeSendTransaction(event) { | ||
| transactions.push(event); | ||
| return null; | ||
| }, | ||
| transport: () => ({ send: async () => ({}), flush: async () => true }), | ||
| }); | ||
| }); | ||
|
|
||
| afterAll(() => { | ||
| getCurrentScope().setClient(undefined); | ||
| }); | ||
|
|
||
| test('creates an http.client span and propagates trace headers', async () => { | ||
| let received: http.IncomingHttpHeaders | undefined; | ||
| const { port, close } = await startServer((req, res) => { | ||
| received = req.headers; | ||
| res.end('ok'); | ||
| }); | ||
|
|
||
| await startSpan({ name: 'parent', op: 'test' }, async () => { | ||
| await fetch(`http://localhost:${port}/downstream`).then(res => res.text()); | ||
| }); | ||
|
|
||
| const parent = await waitForTransaction('parent'); | ||
| await close(); | ||
|
|
||
| const clientSpan = parent.spans?.find(span => span.op === 'http.client'); | ||
| expect(clientSpan).toBeDefined(); | ||
| expect(clientSpan?.origin).toBe('auto.http.fetch'); | ||
|
|
||
| const traceId = parent.contexts?.trace?.trace_id; | ||
| const sentryTrace = header(received, 'sentry-trace'); | ||
| expect(sentryTrace).toBeDefined(); | ||
| expect(sentryTrace!.split('-')[0]).toBe(traceId!); | ||
| expect(sentryTrace!.split('-')[1]).toBe(clientSpan!.span_id!); | ||
| expect(header(received, 'baggage')).toContain(`sentry-trace_id=${traceId}`); | ||
| }); | ||
|
|
||
| test('records exactly one fetch breadcrumb', async () => { | ||
| const { port, close } = await startServer((_req, res) => res.end('ok')); | ||
| const url = `http://localhost:${port}/crumb`; | ||
|
|
||
| getIsolationScope().clearBreadcrumbs(); | ||
| await fetch(url).then(res => res.text()); | ||
| await close(); | ||
|
|
||
| const crumbs = getIsolationScope() | ||
| .getScopeData() | ||
| .breadcrumbs.filter(crumb => crumb.category === 'fetch' && crumb.data?.url === url); | ||
|
|
||
| expect(crumbs).toHaveLength(1); | ||
| }); | ||
| }); |
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
Oops, something went wrong.
Oops, something went wrong.
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.