Skip to content

Commit 878da7e

Browse files
authored
Merge branch 'develop' into fixing-outcome-flushing-replay
2 parents c80217d + d353444 commit 878da7e

File tree

3 files changed

+97
-3
lines changed

3 files changed

+97
-3
lines changed

dev-packages/e2e-tests/test-applications/cloudflare-hono/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
},
1313
"dependencies": {
1414
"@sentry/cloudflare": "latest || *",
15-
"hono": "4.10.3"
15+
"hono": "4.11.4"
1616
},
1717
"devDependencies": {
1818
"@cloudflare/vitest-pool-workers": "^0.8.31",

packages/core/src/fetch.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,9 @@ export function instrumentFetchRequest(
111111
if (shouldAttachHeaders(handlerData.fetchData.url)) {
112112
const request: string | Request = handlerData.args[0];
113113

114-
const options: { [key: string]: unknown } = handlerData.args[1] || {};
114+
// Shallow clone the options object to avoid mutating the original user-provided object
115+
// Examples: users re-using same options object for multiple fetch calls, frozen objects
116+
const options: { [key: string]: unknown } = { ...(handlerData.args[1] || {}) };
115117

116118
const headers = _addTracingHeadersToFetchRequest(
117119
request,

packages/core/test/lib/fetch.test.ts

Lines changed: 93 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { describe, expect, it, vi } from 'vitest';
2-
import { _addTracingHeadersToFetchRequest } from '../../src/fetch';
2+
import { _addTracingHeadersToFetchRequest, instrumentFetchRequest } from '../../src/fetch';
3+
import type { Span } from '../../src/types-hoist/span';
34

45
const { DEFAULT_SENTRY_TRACE, DEFAULT_BAGGAGE } = vi.hoisted(() => ({
56
DEFAULT_SENTRY_TRACE: 'defaultTraceId-defaultSpanId-1',
@@ -409,3 +410,94 @@ describe('_addTracingHeadersToFetchRequest', () => {
409410
});
410411
});
411412
});
413+
414+
describe('instrumentFetchRequest', () => {
415+
describe('options object mutation', () => {
416+
it('does not mutate the original options object', () => {
417+
const originalOptions = { method: 'POST', body: JSON.stringify({ data: 'test' }) };
418+
const originalOptionsSnapshot = { ...originalOptions };
419+
420+
const handlerData = {
421+
fetchData: { url: '/api/test', method: 'POST' },
422+
args: ['/api/test', originalOptions] as unknown[],
423+
startTimestamp: Date.now(),
424+
};
425+
426+
const spans: Record<string, Span> = {};
427+
428+
instrumentFetchRequest(
429+
handlerData,
430+
() => true,
431+
() => true,
432+
spans,
433+
{ spanOrigin: 'auto.http.browser' },
434+
);
435+
436+
// original options object was not mutated
437+
expect(originalOptions).toEqual(originalOptionsSnapshot);
438+
expect(originalOptions).not.toHaveProperty('headers');
439+
});
440+
441+
it('does not throw with a frozen options object', () => {
442+
const frozenOptions = Object.freeze({ method: 'POST', body: JSON.stringify({ data: 'test' }) });
443+
444+
const handlerData = {
445+
fetchData: { url: '/api/test', method: 'POST' },
446+
args: ['/api/test', frozenOptions] as unknown[],
447+
startTimestamp: Date.now(),
448+
};
449+
450+
const spans: Record<string, Span> = {};
451+
452+
// This should not throw, even though the original object is frozen
453+
expect(() => {
454+
instrumentFetchRequest(
455+
handlerData,
456+
() => true,
457+
() => true,
458+
spans,
459+
{ spanOrigin: 'auto.http.browser' },
460+
);
461+
}).not.toThrow();
462+
463+
// args[1] is a new object with headers (not the frozen one)
464+
const resultOptions = handlerData.args[1] as { headers?: unknown };
465+
expect(resultOptions).toHaveProperty('headers');
466+
expect(resultOptions).not.toBe(frozenOptions);
467+
});
468+
469+
it('preserves existing properties when cloning options', () => {
470+
const originalOptions = {
471+
method: 'POST',
472+
body: JSON.stringify({ data: 'test' }),
473+
credentials: 'include' as const,
474+
mode: 'cors' as const,
475+
};
476+
477+
const handlerData = {
478+
fetchData: { url: '/api/test', method: 'POST' },
479+
args: ['/api/test', originalOptions] as unknown[],
480+
startTimestamp: Date.now(),
481+
};
482+
483+
const spans: Record<string, Span> = {};
484+
485+
instrumentFetchRequest(
486+
handlerData,
487+
() => true,
488+
() => true,
489+
spans,
490+
{ spanOrigin: 'auto.http.browser' },
491+
);
492+
493+
const resultOptions = handlerData.args[1] as Record<string, unknown>;
494+
495+
// all original properties are preserved in the new object
496+
expect(resultOptions.method).toBe('POST');
497+
expect(resultOptions.body).toBe(originalOptions.body);
498+
expect(resultOptions.credentials).toBe('include');
499+
expect(resultOptions.mode).toBe('cors');
500+
expect(resultOptions).toHaveProperty('headers');
501+
});
502+
});
503+
});

0 commit comments

Comments
 (0)