Skip to content

Commit 1fd1951

Browse files
authored
Merge branch 'develop' into fix/cloudflare-do-rpc-untraced-telemetry
2 parents 1b726fc + 471157a commit 1fd1951

31 files changed

Lines changed: 1040 additions & 1131 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,12 @@
66

77
Work in this release was contributed by @psh4607, @thijsw, @trinitiwowka, @nehaprasad-dev, @JealousGx, @Jxxunnn, @eddie333016, @davidmurdoch, @yashschandra, @atharv-sys32, @AG0708, @birkskyum, @mkly, @mcbbugu, @suhailopensource, @zkasuran, @mohd-akram, @RealBhupesh, @halillusion, @psang39, @hafzism, @JosephDoUrden, @Tyagiquamar, and @Andarist. Thank you for your contributions!
88

9+
- feat(core): Add `createFetchIntegration`, the shared implementation behind the global-`fetch` integrations in `@sentry/bun`, `@sentry/cloudflare`, `@sentry/deno` and `@sentry/vercel-edge`. Those four packages carried four copies of it; they now share one. Two changes come out of that:
10+
- All four gain a `tracePropagation` option (default `true`). Turn it off to stop injecting `sentry-trace` and `baggage` without also turning off spans. To scope propagation to specific URLs, keep using `tracePropagationTargets` in the client options.
11+
- Integration options now follow the client. Previously a second `Sentry.init()` in the same process silently reused the options of the first one.
12+
- fix(bun, cloudflare, deno, vercel-edge): Outgoing `fetch` breadcrumbs now route their URL through the data-collection filters, matching the `node:http` breadcrumb. `data.url` is sanitized (credentials stripped, query and fragment removed) and the query moves to `url.query`, where `dataCollection.urlQueryParams` applies to it. Previously the raw URL was recorded, so sensitive query values reached Sentry even with query collection turned off.
13+
- fix(vercel-edge): `winterCGFetchIntegration` now honors the client's `propagateTraceparent` option. It was the one copy of the fetch integration that never forwarded it, so the `traceparent` header was never sent.
14+
- feat(deno)!: Fetch breadcrumbs are now recorded by `fetchIntegration` rather than `breadcrumbsIntegration`, matching the other runtime SDKs. Disable them with `fetchIntegration({ breadcrumbs: false })`. `breadcrumbsIntegration({ fetch: false })` is deprecated, no longer has any effect, and will be removed in a future major version.
915
- feat(core): Accept a `CollectBehavior` shorthand for `dataCollection.httpHeaders`. Passing `true`, `false`, `{ allow: [...] }` or `{ deny: [...] }` now applies to both request and response headers; `{ request, response }` still controls each direction independently.
1016
- feat(langchain)!: Emit `gen_ai.pipeline.name` instead of `langchain.chain.name` on LangChain chain spans. The attribute is omitted when the chain is unnamed.
1117
- feat(deno)!: Rename several default integrations to match the other SDKs ([#22404](https://github.com/getsentry/sentry-javascript/pull/22404)). The `deno*Integration` exports are kept as deprecated aliases. If you were relying on the names (for example, to disable them), then note that these have changed:

‎packages/bun/src/index.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -215,5 +215,6 @@ export { bunServerIntegration } from './integrations/bunserver';
215215
export type { BunServerIntegrationOptions } from './integrations/bunserver';
216216
export { bunHttpServerIntegration } from './integrations/bunHttpServer';
217217
export { fetchIntegration } from './integrations/fetch';
218+
export type { FetchIntegrationOptions } from '@sentry/core';
218219
export { bunRuntimeMetricsIntegration, type BunRuntimeMetricsOptions } from './integrations/bunRuntimeMetrics';
219220
export { makeFetchTransport } from './transports';
Lines changed: 5 additions & 161 deletions
Original file line numberDiff line numberDiff line change
@@ -1,166 +1,10 @@
1-
import type {
2-
Client,
3-
FetchBreadcrumbData,
4-
FetchBreadcrumbHint,
5-
HandlerDataFetch,
6-
IntegrationFn,
7-
Span,
8-
} from '@sentry/core';
9-
import {
10-
addBreadcrumb,
11-
addFetchInstrumentationHandler,
12-
defineIntegration,
13-
getBreadcrumbLogLevelFromHttpStatusCode,
14-
getClient,
15-
instrumentFetchRequest,
16-
isSentryRequestUrl,
17-
LRUMap,
18-
shouldPropagateTraceForUrl,
19-
} from '@sentry/core';
20-
21-
const INTEGRATION_NAME = 'Fetch' as const;
22-
23-
const HAS_CLIENT_MAP = new WeakMap<Client, boolean>();
24-
25-
interface FetchOptions {
26-
/**
27-
* Whether breadcrumbs should be recorded for requests.
28-
* Defaults to true.
29-
*/
30-
breadcrumbs?: boolean;
31-
32-
/**
33-
* Function determining whether or not to create spans to track outgoing requests to the given URL.
34-
* By default, spans will be created for all outgoing requests.
35-
*/
36-
shouldCreateSpanForRequest?: (url: string) => boolean;
37-
}
38-
39-
const _fetchIntegration = ((options: FetchOptions = {}) => {
40-
const breadcrumbs = options.breadcrumbs === undefined ? true : options.breadcrumbs;
41-
const shouldCreateSpanForRequest = options.shouldCreateSpanForRequest;
42-
43-
const _createSpanUrlMap = new LRUMap<string, boolean>(100);
44-
const _headersUrlMap = new LRUMap<string, boolean>(100);
45-
46-
const spans: Record<string, Span> = {};
47-
48-
/** Decides whether to attach trace data to the outgoing fetch request */
49-
function _shouldAttachTraceData(url: string): boolean {
50-
const client = getClient();
51-
52-
if (!client) {
53-
return false;
54-
}
55-
56-
return shouldPropagateTraceForUrl(url, client.getOptions().tracePropagationTargets, _headersUrlMap);
57-
}
58-
59-
/** Helper that wraps shouldCreateSpanForRequest option */
60-
function _shouldCreateSpan(url: string): boolean {
61-
if (shouldCreateSpanForRequest === undefined) {
62-
return true;
63-
}
64-
65-
const cachedDecision = _createSpanUrlMap.get(url);
66-
if (cachedDecision !== undefined) {
67-
return cachedDecision;
68-
}
69-
70-
const decision = shouldCreateSpanForRequest(url);
71-
_createSpanUrlMap.set(url, decision);
72-
return decision;
73-
}
74-
75-
return {
76-
name: INTEGRATION_NAME,
77-
setupOnce() {
78-
addFetchInstrumentationHandler(handlerData => {
79-
const client = getClient();
80-
if (!client || !HAS_CLIENT_MAP.get(client)) {
81-
return;
82-
}
83-
const { propagateTraceparent } = client.getOptions();
84-
85-
if (isSentryRequestUrl(handlerData.fetchData.url, client)) {
86-
return;
87-
}
88-
89-
instrumentFetchRequest(handlerData, _shouldCreateSpan, _shouldAttachTraceData, spans, {
90-
spanOrigin: 'auto.http.fetch',
91-
propagateTraceparent,
92-
});
93-
94-
if (breadcrumbs) {
95-
createBreadcrumb(handlerData);
96-
}
97-
});
98-
},
99-
setup(client) {
100-
HAS_CLIENT_MAP.set(client, true);
101-
},
102-
};
103-
}) satisfies IntegrationFn;
1+
import { createFetchIntegration } from '@sentry/core';
1042

1053
/**
1064
* Instruments outgoing `fetch` requests in Bun: creates spans, records breadcrumbs and
1075
* attaches trace propagation headers.
1086
*/
109-
export const fetchIntegration = defineIntegration(_fetchIntegration);
110-
111-
function createBreadcrumb(handlerData: HandlerDataFetch): void {
112-
const { startTimestamp, endTimestamp } = handlerData;
113-
114-
// We only capture complete fetch requests
115-
if (!endTimestamp) {
116-
return;
117-
}
118-
119-
const breadcrumbData: FetchBreadcrumbData = {
120-
method: handlerData.fetchData.method,
121-
url: handlerData.fetchData.url,
122-
};
123-
124-
if (handlerData.error) {
125-
const hint: FetchBreadcrumbHint = {
126-
data: handlerData.error,
127-
input: handlerData.args,
128-
startTimestamp,
129-
endTimestamp,
130-
};
131-
132-
addBreadcrumb(
133-
{
134-
category: 'fetch',
135-
data: breadcrumbData,
136-
level: 'error',
137-
type: 'http',
138-
},
139-
hint,
140-
);
141-
} else {
142-
const response = handlerData.response as Response | undefined;
143-
144-
breadcrumbData.request_body_size = handlerData.fetchData.request_body_size;
145-
breadcrumbData.response_body_size = handlerData.fetchData.response_body_size;
146-
breadcrumbData.status_code = response?.status;
147-
148-
const hint: FetchBreadcrumbHint = {
149-
input: handlerData.args,
150-
response,
151-
startTimestamp,
152-
endTimestamp,
153-
};
154-
const level = getBreadcrumbLogLevelFromHttpStatusCode(breadcrumbData.status_code);
155-
156-
addBreadcrumb(
157-
{
158-
category: 'fetch',
159-
data: breadcrumbData,
160-
type: 'http',
161-
level,
162-
},
163-
hint,
164-
);
165-
}
166-
}
7+
export const fetchIntegration = createFetchIntegration({
8+
name: 'Fetch',
9+
spanOrigin: 'auto.http.fetch',
10+
});
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
import http from 'node:http';
2+
import type { TransactionEvent } from '@sentry/core';
3+
import { getCurrentScope, getIsolationScope, startSpan } from '@sentry/core';
4+
import { afterAll, beforeAll, describe, expect, test } from 'bun:test';
5+
import { init } from '../../src';
6+
7+
async function startServer(
8+
handler: (req: http.IncomingMessage, res: http.ServerResponse) => void,
9+
): Promise<{ port: number; close: () => Promise<void> }> {
10+
const server = http.createServer(handler);
11+
const port = await new Promise<number>(resolve => {
12+
server.listen(0, () => resolve((server.address() as { port: number }).port));
13+
});
14+
return {
15+
port,
16+
close: () => new Promise<void>(resolve => server.close(() => resolve())),
17+
};
18+
}
19+
20+
const transactions: TransactionEvent[] = [];
21+
22+
/** Bind on the real completion signal so a "never arrives" regression fails instead of hanging. */
23+
function waitForTransaction(name: string): Promise<TransactionEvent> {
24+
return new Promise((resolve, reject) => {
25+
const timer = setTimeout(() => reject(new Error(`Timed out waiting for the "${name}" transaction`)), 5000);
26+
const poll = setInterval(() => {
27+
const found = transactions.find(event => event.transaction === name);
28+
if (found) {
29+
clearTimeout(timer);
30+
clearInterval(poll);
31+
resolve(found);
32+
}
33+
}, 10);
34+
});
35+
}
36+
37+
function header(headers: http.IncomingHttpHeaders | undefined, name: string): string | undefined {
38+
const value = headers?.[name];
39+
return Array.isArray(value) ? value[0] : value;
40+
}
41+
42+
describe('fetchIntegration', () => {
43+
beforeAll(() => {
44+
init({
45+
dsn: 'https://public@dsn.ingest.sentry.io/1337',
46+
tracesSampleRate: 1.0,
47+
traceLifecycle: 'static',
48+
beforeSendTransaction(event) {
49+
transactions.push(event);
50+
return null;
51+
},
52+
transport: () => ({ send: async () => ({}), flush: async () => true }),
53+
});
54+
});
55+
56+
afterAll(() => {
57+
getCurrentScope().setClient(undefined);
58+
});
59+
60+
test('creates an http.client span and propagates trace headers', async () => {
61+
let received: http.IncomingHttpHeaders | undefined;
62+
const { port, close } = await startServer((req, res) => {
63+
received = req.headers;
64+
res.end('ok');
65+
});
66+
67+
await startSpan({ name: 'parent', op: 'test' }, async () => {
68+
await fetch(`http://localhost:${port}/downstream`).then(res => res.text());
69+
});
70+
71+
const parent = await waitForTransaction('parent');
72+
await close();
73+
74+
const clientSpan = parent.spans?.find(span => span.op === 'http.client');
75+
expect(clientSpan).toBeDefined();
76+
expect(clientSpan?.origin).toBe('auto.http.fetch');
77+
78+
const traceId = parent.contexts?.trace?.trace_id;
79+
const sentryTrace = header(received, 'sentry-trace');
80+
expect(sentryTrace).toBeDefined();
81+
expect(sentryTrace!.split('-')[0]).toBe(traceId!);
82+
expect(sentryTrace!.split('-')[1]).toBe(clientSpan!.span_id!);
83+
expect(header(received, 'baggage')).toContain(`sentry-trace_id=${traceId}`);
84+
});
85+
86+
test('records exactly one fetch breadcrumb', async () => {
87+
const { port, close } = await startServer((_req, res) => res.end('ok'));
88+
const url = `http://localhost:${port}/crumb`;
89+
90+
getIsolationScope().clearBreadcrumbs();
91+
await fetch(url).then(res => res.text());
92+
await close();
93+
94+
const crumbs = getIsolationScope()
95+
.getScopeData()
96+
.breadcrumbs.filter(crumb => crumb.category === 'fetch' && crumb.data?.url === url);
97+
98+
expect(crumbs).toHaveLength(1);
99+
});
100+
});

‎packages/bundler-plugins/package.json‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,7 @@
115115
"dotenv": "^17.4.2",
116116
"glob": "^13.0.6",
117117
"magic-string": "~0.30.8",
118+
"oxc-parser": "^0.143.0",
118119
"sentry": "^0.44.0",
119120
"supports-color": "^8.1.1"
120121
},

packages/bundler-plugins/src/core/component-annotation-vite-ast.ts renamed to packages/bundler-plugins/src/core/component-annotation-oxc-ast.ts

File renamed without changes.

packages/bundler-plugins/src/core/component-annotation-vite-fragments.ts renamed to packages/bundler-plugins/src/core/component-annotation-oxc-fragments.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
import type { AstNode, FragmentContext } from './component-annotation-vite-ast';
2-
import { isAstNode, isObjectLike, walkAst } from './component-annotation-vite-ast';
3-
import { getStringName } from './component-annotation-vite-jsx';
1+
import type { AstNode, FragmentContext } from './component-annotation-oxc-ast';
2+
import { isAstNode, isObjectLike, walkAst } from './component-annotation-oxc-ast';
3+
import { getStringName } from './component-annotation-oxc-jsx';
44

55
export function collectFragmentContext(ast: AstNode): FragmentContext {
66
const context: FragmentContext = {

packages/bundler-plugins/src/core/component-annotation-vite-jsx.ts renamed to packages/bundler-plugins/src/core/component-annotation-oxc-jsx.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@ import type {
1212
JSXFragmentNode,
1313
JSXOpeningElementNode,
1414
JSXRootNode,
15-
} from './component-annotation-vite-ast';
16-
import { isAstNode, isObjectLike } from './component-annotation-vite-ast';
15+
} from './component-annotation-oxc-ast';
16+
import { isAstNode, isObjectLike } from './component-annotation-oxc-ast';
1717

1818
const UNKNOWN_ELEMENT_NAME = 'unknown';
1919
const WEB_ATTRIBUTE_NAMES = [WEB_ELEMENT_NAME, WEB_COMPONENT_NAME, WEB_SOURCE_FILE_NAME] as const;

packages/bundler-plugins/src/core/component-annotation-vite-walk.ts renamed to packages/bundler-plugins/src/core/component-annotation-oxc-walk.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,17 @@
1-
import type { AstNode, AttributeInsertion, FragmentContext, JSXRootNode } from './component-annotation-vite-ast';
1+
import type { AstNode, AttributeInsertion, FragmentContext, JSXRootNode } from './component-annotation-oxc-ast';
22
import {
33
addPendingAttributes,
44
getStringName,
55
isJSXElement,
66
isJSXRoot,
77
toAttributeInsertions,
8-
} from './component-annotation-vite-jsx';
9-
import { isAstNode, isObjectLike, walkAst } from './component-annotation-vite-ast';
10-
import { collectFragmentContext } from './component-annotation-vite-fragments';
8+
} from './component-annotation-oxc-jsx';
9+
import { isAstNode, isObjectLike, walkAst } from './component-annotation-oxc-ast';
10+
import { collectFragmentContext } from './component-annotation-oxc-fragments';
1111

1212
type ComponentJSXRoots = { name: string; roots: JSXRootNode[] };
1313

14-
export function collectViteComponentAnnotationInsertions(
14+
export function collectOxcComponentAnnotationInsertions(
1515
code: string,
1616
ast: AstNode,
1717
ignoredComponents: string[],

0 commit comments

Comments
 (0)