Skip to content

Commit f96edcd

Browse files
authored
Merge branch 'develop' into nh/bump-iitm
2 parents f29fb9f + 9c40849 commit f96edcd

10 files changed

Lines changed: 248 additions & 16 deletions

File tree

dev-packages/cloudflare-integration-tests/runner.ts

Lines changed: 41 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,9 @@ type StartResult = {
5757
export function createRunner(...paths: string[]) {
5858
const testPath = join(...paths);
5959

60+
// controls whether envelopes are expected in predefined order or not
61+
let unordered = false;
62+
6063
if (!existsSync(testPath)) {
6164
throw new Error(`Test scenario not found: ${testPath}`);
6265
}
@@ -76,6 +79,10 @@ export function createRunner(...paths: string[]) {
7679
}
7780
return this;
7881
},
82+
unordered: function () {
83+
unordered = true;
84+
return this;
85+
},
7986
ignore: function (...types: EnvelopeItemType[]) {
8087
types.forEach(t => ignored.add(t));
8188
return this;
@@ -102,6 +109,14 @@ export function createRunner(...paths: string[]) {
102109
}
103110
}
104111

112+
function assertEnvelopeMatches(expected: Expected, envelope: Envelope): void {
113+
if (typeof expected === 'function') {
114+
expected(envelope);
115+
} else {
116+
expect(envelope).toEqual(expected);
117+
}
118+
}
119+
105120
function newEnvelope(envelope: Envelope): void {
106121
if (process.env.DEBUG) log('newEnvelope', inspect(envelope, false, null, true));
107122

@@ -111,19 +126,36 @@ export function createRunner(...paths: string[]) {
111126
return;
112127
}
113128

114-
const expected = expectedEnvelopes.shift();
115-
116-
// Catch any error or failed assertions and pass them to done to end the test quickly
117129
try {
118-
if (!expected) {
119-
return;
120-
}
130+
if (unordered) {
131+
// find any matching expected envelope
132+
const matchIndex = expectedEnvelopes.findIndex(candidate => {
133+
try {
134+
assertEnvelopeMatches(candidate, envelope);
135+
return true;
136+
} catch {
137+
return false;
138+
}
139+
});
121140

122-
if (typeof expected === 'function') {
123-
expected(envelope);
141+
// no match found
142+
if (matchIndex < 0) {
143+
return;
144+
}
145+
146+
// remove the matching expected envelope
147+
expectedEnvelopes.splice(matchIndex, 1);
124148
} else {
125-
expect(envelope).toEqual(expected);
149+
// in ordered mode we just look at the next expected envelope
150+
const expected = expectedEnvelopes.shift();
151+
152+
if (!expected) {
153+
return;
154+
}
155+
156+
assertEnvelopeMatches(expected, envelope);
126157
}
158+
127159
expectCallbackCalled();
128160
} catch (e) {
129161
reject(e);

dev-packages/cloudflare-integration-tests/suites/hono/basic/test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ it('Hono app captures errors', async ({ signal }) => {
4747
}),
4848
);
4949
})
50+
.unordered()
5051
.start(signal);
5152
await runner.makeRequest('get', '/error', { expectError: true });
5253
await runner.completed();

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
"dependencies": {
1818
"@modelcontextprotocol/sdk": "^1.24.0",
1919
"@sentry/cloudflare": "latest || *",
20-
"agents": "^0.2.23",
20+
"agents": "0.2.32",
2121
"zod": "^3.25.76"
2222
},
2323
"devDependencies": {

dev-packages/e2e-tests/test-applications/solid-tanstack-router/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
"dependencies": {
1616
"@sentry/solid": "latest || *",
1717
"@tailwindcss/vite": "^4.0.6",
18-
"@tanstack/solid-router": "^1.132.25",
18+
"@tanstack/solid-router": "1.141.8",
1919
"@tanstack/solid-router-devtools": "^1.132.25",
2020
"@tanstack/solid-start": "^1.132.25",
2121
"solid-js": "^1.9.5",

dev-packages/e2e-tests/test-applications/vue-tanstack-router/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
},
1515
"dependencies": {
1616
"@sentry/vue": "latest || *",
17-
"@tanstack/vue-router": "^1.64.0",
17+
"@tanstack/vue-router": "1.141.8",
1818
"vue": "^3.4.15"
1919
},
2020
"devDependencies": {
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import * as Sentry from '@sentry/node';
2+
import pino from 'pino';
3+
4+
const logger = pino({
5+
name: 'myapp',
6+
messageKey: 'message', // Custom key instead of 'msg'
7+
errorKey: 'error', // Custom key instead of 'err'
8+
});
9+
10+
Sentry.withIsolationScope(() => {
11+
Sentry.startSpan({ name: 'custom-keys-test' }, () => {
12+
logger.info({ user: 'user-123', action: 'custom-key-test' }, 'Custom message key');
13+
});
14+
});
15+
16+
setTimeout(() => {
17+
Sentry.withIsolationScope(() => {
18+
Sentry.startSpan({ name: 'error-custom-key' }, () => {
19+
logger.error(new Error('Custom error key'));
20+
});
21+
});
22+
}, 500);

dev-packages/node-integration-tests/suites/pino/test.ts

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -295,4 +295,79 @@ conditionalTest({ min: 20 })('Pino integration', () => {
295295
.start()
296296
.completed();
297297
});
298+
299+
test('captures logs with custom messageKey and errorKey', async () => {
300+
const instrumentPath = join(__dirname, 'instrument.mjs');
301+
302+
await createRunner(__dirname, 'scenario-custom-keys.mjs')
303+
.withMockSentryServer()
304+
.withInstrument(instrumentPath)
305+
.ignore('transaction')
306+
.expect({
307+
event: {
308+
exception: {
309+
values: [
310+
{
311+
type: 'Error',
312+
value: 'Custom error key',
313+
mechanism: {
314+
type: 'pino',
315+
handled: true,
316+
},
317+
stacktrace: {
318+
frames: expect.arrayContaining([
319+
expect.objectContaining({
320+
function: '?',
321+
in_app: true,
322+
module: 'scenario-custom-keys',
323+
}),
324+
]),
325+
},
326+
},
327+
],
328+
},
329+
},
330+
})
331+
.expect({
332+
log: {
333+
items: [
334+
{
335+
timestamp: expect.any(Number),
336+
level: 'info',
337+
body: 'Custom message key',
338+
trace_id: expect.any(String),
339+
severity_number: 9,
340+
attributes: {
341+
name: { value: 'myapp', type: 'string' },
342+
'pino.logger.level': { value: 30, type: 'integer' },
343+
user: { value: 'user-123', type: 'string' },
344+
action: { value: 'custom-key-test', type: 'string' },
345+
message: { value: 'Custom message key', type: 'string' },
346+
'sentry.origin': { value: 'auto.log.pino', type: 'string' },
347+
'sentry.release': { value: '1.0', type: 'string' },
348+
'sentry.sdk.name': { value: 'sentry.javascript.node', type: 'string' },
349+
},
350+
},
351+
{
352+
timestamp: expect.any(Number),
353+
level: 'error',
354+
body: 'Custom error key',
355+
trace_id: expect.any(String),
356+
severity_number: 17,
357+
attributes: {
358+
name: { value: 'myapp', type: 'string' },
359+
'pino.logger.level': { value: 50, type: 'integer' },
360+
message: { value: 'Custom error key', type: 'string' },
361+
error: { value: expect.any(String), type: 'string' },
362+
'sentry.origin': { value: 'auto.log.pino', type: 'string' },
363+
'sentry.release': { value: '1.0', type: 'string' },
364+
'sentry.sdk.name': { value: 'sentry.javascript.node', type: 'string' },
365+
},
366+
},
367+
],
368+
},
369+
})
370+
.start()
371+
.completed();
372+
});
298373
});

packages/cloudflare/src/transport.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,18 @@ export function makeCloudflareTransport(options: CloudflareTransportOptions): Tr
8989
};
9090

9191
return suppressTracing(() => {
92-
return (options.fetch ?? fetch)(options.url, requestOptions).then(response => {
92+
return (options.fetch ?? fetch)(options.url, requestOptions).then(async response => {
93+
// Consume the response body to satisfy Cloudflare Workers' fetch requirements.
94+
// The runtime requires all fetch response bodies to be read or explicitly canceled
95+
// to prevent connection stalls and potential deadlocks. We read the body as text
96+
// even though we don't use the content, as Sentry's response information is in the headers.
97+
// See: https://github.com/getsentry/sentry-javascript/issues/18534
98+
try {
99+
await response.text();
100+
} catch {
101+
// no-op
102+
}
103+
93104
return {
94105
statusCode: response.status,
95106
headers: {

packages/cloudflare/test/transport.test.ts

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,78 @@ describe('Edge Transport', () => {
106106
...REQUEST_OPTIONS,
107107
});
108108
});
109+
110+
describe('Response body consumption (issue #18534)', () => {
111+
it('consumes the response body to prevent Cloudflare stalled connection warnings', async () => {
112+
const textMock = vi.fn(() => Promise.resolve('OK'));
113+
const headers = {
114+
get: vi.fn(),
115+
};
116+
mockFetch.mockImplementationOnce(() =>
117+
Promise.resolve({
118+
headers,
119+
status: 200,
120+
text: textMock,
121+
}),
122+
);
123+
124+
const transport = makeCloudflareTransport(DEFAULT_EDGE_TRANSPORT_OPTIONS);
125+
126+
await transport.send(ERROR_ENVELOPE);
127+
await transport.flush();
128+
129+
expect(textMock).toHaveBeenCalledTimes(1);
130+
expect(headers.get).toHaveBeenCalledTimes(2);
131+
expect(headers.get).toHaveBeenCalledWith('X-Sentry-Rate-Limits');
132+
expect(headers.get).toHaveBeenCalledWith('Retry-After');
133+
});
134+
135+
it('handles response body consumption errors gracefully', async () => {
136+
const textMock = vi.fn(() => Promise.reject(new Error('Body read error')));
137+
const headers = {
138+
get: vi.fn(),
139+
};
140+
141+
mockFetch.mockImplementationOnce(() =>
142+
Promise.resolve({
143+
headers,
144+
status: 200,
145+
text: textMock,
146+
}),
147+
);
148+
149+
const transport = makeCloudflareTransport(DEFAULT_EDGE_TRANSPORT_OPTIONS);
150+
151+
await expect(transport.send(ERROR_ENVELOPE)).resolves.toBeDefined();
152+
await expect(transport.flush()).resolves.toBeDefined();
153+
154+
expect(textMock).toHaveBeenCalledTimes(1);
155+
expect(headers.get).toHaveBeenCalledTimes(2);
156+
expect(headers.get).toHaveBeenCalledWith('X-Sentry-Rate-Limits');
157+
expect(headers.get).toHaveBeenCalledWith('Retry-After');
158+
});
159+
160+
it('handles a potential never existing use case of a non existing text method', async () => {
161+
const headers = {
162+
get: vi.fn(),
163+
};
164+
165+
mockFetch.mockImplementationOnce(() =>
166+
Promise.resolve({
167+
headers,
168+
status: 200,
169+
}),
170+
);
171+
172+
const transport = makeCloudflareTransport(DEFAULT_EDGE_TRANSPORT_OPTIONS);
173+
174+
await expect(transport.send(ERROR_ENVELOPE)).resolves.toBeDefined();
175+
await expect(transport.flush()).resolves.toBeDefined();
176+
expect(headers.get).toHaveBeenCalledTimes(2);
177+
expect(headers.get).toHaveBeenCalledWith('X-Sentry-Rate-Limits');
178+
expect(headers.get).toHaveBeenCalledWith('Retry-After');
179+
});
180+
});
109181
});
110182

111183
describe('IsolatedPromiseBuffer', () => {

packages/node-core/src/integrations/pino.ts

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,27 @@ type LevelMapping = {
1919
};
2020

2121
type Pino = {
22+
[key: symbol]: unknown;
2223
levels: LevelMapping;
2324
[SENTRY_TRACK_SYMBOL]?: 'track' | 'ignore';
2425
};
2526

27+
/**
28+
* Gets a custom Pino key from a logger instance by searching for the symbol.
29+
* Pino uses non-global symbols like Symbol('pino.messageKey'): https://github.com/pinojs/pino/blob/8a816c0b1f72de5ae9181f3bb402109b66f7d812/lib/symbols.js
30+
*/
31+
function getPinoKey(logger: Pino, symbolName: string, defaultKey: string): string {
32+
const symbols = Object.getOwnPropertySymbols(logger);
33+
const symbolString = `Symbol(${symbolName})`;
34+
for (const sym of symbols) {
35+
if (sym.toString() === symbolString) {
36+
const value = logger[sym];
37+
return typeof value === 'string' ? value : defaultKey;
38+
}
39+
}
40+
return defaultKey;
41+
}
42+
2643
type MergeObject = {
2744
[key: string]: unknown;
2845
err?: Error;
@@ -134,7 +151,8 @@ const _pinoIntegration = defineIntegration((userOptions: DeepPartial<PinoOptions
134151

135152
const [captureObj, message, levelNumber] = args;
136153
const level = self?.levels?.labels?.[levelNumber] || 'info';
137-
const logMessage = message || (resultObj?.msg as string | undefined) || '';
154+
const messageKey = getPinoKey(self, 'pino.messageKey', 'msg');
155+
const logMessage = message || (resultObj?.[messageKey] as string | undefined) || '';
138156

139157
if (enableLogs && options.log.levels.includes(level)) {
140158
const attributes: Record<string, unknown> = {
@@ -163,8 +181,9 @@ const _pinoIntegration = defineIntegration((userOptions: DeepPartial<PinoOptions
163181
return event;
164182
});
165183

166-
if (captureObj.err) {
167-
captureException(captureObj.err, captureContext);
184+
const error = captureObj[getPinoKey(self, 'pino.errorKey', 'err')];
185+
if (error) {
186+
captureException(error, captureContext);
168187
return;
169188
}
170189

0 commit comments

Comments
 (0)