Skip to content

Commit b148eaf

Browse files
authored
Merge branch 'develop' into abhi-sentry-origin-logs
2 parents d132626 + 6a8a2c3 commit b148eaf

File tree

9 files changed

+110
-21
lines changed

9 files changed

+110
-21
lines changed

CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010

1111
- "You miss 100 percent of the chances you don't take. — Wayne Gretzky" — Michael Scott
1212

13-
Work in this release was contributed by @Page-. Thank you for your contribution!
13+
Work in this release was contributed by @Page- and @Fryuni. Thank you for your contributions!
1414

1515
## 9.11.0
1616

packages/astro/src/server/middleware.ts

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -184,12 +184,18 @@ async function instrumentRequest(
184184

185185
const newResponseStream = new ReadableStream({
186186
start: async controller => {
187-
for await (const chunk of originalBody) {
188-
const html = typeof chunk === 'string' ? chunk : decoder.decode(chunk, { stream: true });
189-
const modifiedHtml = addMetaTagToHead(html);
190-
controller.enqueue(new TextEncoder().encode(modifiedHtml));
187+
try {
188+
for await (const chunk of originalBody) {
189+
const html = typeof chunk === 'string' ? chunk : decoder.decode(chunk, { stream: true });
190+
const modifiedHtml = addMetaTagToHead(html);
191+
controller.enqueue(new TextEncoder().encode(modifiedHtml));
192+
}
193+
} catch (e) {
194+
sendErrorToSentry(e);
195+
controller.error(e);
196+
} finally {
197+
controller.close();
191198
}
192-
controller.close();
193199
},
194200
});
195201

packages/astro/test/server/middleware.test.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,49 @@ describe('sentryMiddleware', () => {
149149
});
150150
});
151151

152+
it('throws and sends an error to sentry if response streaming throws', async () => {
153+
const captureExceptionSpy = vi.spyOn(SentryNode, 'captureException');
154+
155+
const middleware = handleRequest();
156+
const ctx = {
157+
request: {
158+
method: 'GET',
159+
url: '/users',
160+
headers: new Headers(),
161+
},
162+
url: new URL('https://myDomain.io/users/'),
163+
params: {},
164+
};
165+
166+
const error = new Error('Something went wrong');
167+
168+
const faultyStream = new ReadableStream({
169+
pull: controller => {
170+
controller.error(error);
171+
controller.close();
172+
},
173+
});
174+
175+
const next = vi.fn(() =>
176+
Promise.resolve(
177+
new Response(faultyStream, {
178+
headers: new Headers({ 'content-type': 'text/html' }),
179+
}),
180+
),
181+
);
182+
183+
// @ts-expect-error, a partial ctx object is fine here
184+
const resultFromNext = await middleware(ctx, next);
185+
186+
expect(resultFromNext?.headers.get('content-type')).toEqual('text/html');
187+
188+
await expect(() => resultFromNext!.text()).rejects.toThrowError();
189+
190+
expect(captureExceptionSpy).toHaveBeenCalledWith(error, {
191+
mechanism: { handled: false, type: 'astro', data: { function: 'astroMiddleware' } },
192+
});
193+
});
194+
152195
describe('track client IP address', () => {
153196
it('attaches client IP if `trackClientIp=true` when handling dynamic page requests', async () => {
154197
const middleware = handleRequest({ trackClientIp: true });

packages/cloudflare/README.md

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -22,16 +22,21 @@ To get started, first install the `@sentry/cloudflare` package:
2222
npm install @sentry/cloudflare
2323
```
2424

25-
Then set either the `nodejs_compat` or `nodejs_als` compatibility flags in your `wrangler.toml`. This is because the SDK
26-
needs access to the `AsyncLocalStorage` API to work correctly.
27-
28-
```toml
29-
compatibility_flags = ["nodejs_compat"]
30-
# compatibility_flags = ["nodejs_als"]
25+
Then either set the `nodejs_als` or `nodejs_compat` compatibility flags in your `wrangler.jsonc`/`wrangler.toml` config. This is because the SDK needs access to the `AsyncLocalStorage` API to work correctly.
26+
27+
```jsonc {tabTitle:JSON} {filename:wrangler.jsonc}
28+
{
29+
"compatibility_flags": [
30+
"nodejs_als"
31+
// "nodejs_compat"
32+
]
33+
}
3134
```
3235

33-
Then you can either setup up the SDK for [Cloudflare Pages](#setup-cloudflare-pages) or
34-
[Cloudflare Workers](#setup-cloudflare-workers).
36+
```toml {tabTitle:Toml} {filename:wrangler.toml}
37+
compatibility_flags = ["nodejs_als"]
38+
# compatibility_flags = ["nodejs_compat"]
39+
```
3540

3641
## Setup (Cloudflare Pages)
3742

packages/core/src/client.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -670,6 +670,13 @@ export abstract class Client<O extends ClientOptions = ClientOptions> {
670670
*/
671671
public on(hook: 'afterCaptureLog', callback: (log: Log) => void): () => void;
672672

673+
/**
674+
* A hook that is called when the client is flushing logs
675+
*
676+
* @returns {() => void} A function that, when executed, removes the registered callback.
677+
*/
678+
public on(hook: 'flushLogs', callback: () => void): () => void;
679+
673680
/**
674681
* Register a hook on this client.
675682
*/
@@ -827,6 +834,11 @@ export abstract class Client<O extends ClientOptions = ClientOptions> {
827834
*/
828835
public emit(hook: 'afterCaptureLog', log: Log): void;
829836

837+
/**
838+
* Emit a hook event for client flush logs
839+
*/
840+
public emit(hook: 'flushLogs'): void;
841+
830842
/**
831843
* Emit a hook that was previously registered via `on()`.
832844
*/

packages/core/src/logs/exports.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,8 @@ export function _INTERNAL_flushLogsBuffer(client: Client, maybeLogBuffer?: Array
163163
// Clear the log buffer after envelopes have been constructed.
164164
logBuffer.length = 0;
165165

166+
client.emit('flushLogs');
167+
166168
// sendEnvelope should not throw
167169
// eslint-disable-next-line @typescript-eslint/no-floating-promises
168170
client.sendEnvelope(envelope);

packages/core/src/server-runtime-client.ts

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,9 @@ import { resolvedSyncPromise } from './utils-hoist/syncpromise';
2525
import { _INTERNAL_flushLogsBuffer } from './logs/exports';
2626
import { isPrimitive } from './utils-hoist';
2727

28+
// TODO: Make this configurable
29+
const DEFAULT_LOG_FLUSH_INTERVAL = 5000;
30+
2831
export interface ServerRuntimeClientOptions extends ClientOptions<BaseTransportOptions> {
2932
platform?: string;
3033
runtime?: { name: string; version?: string };
@@ -37,6 +40,7 @@ export interface ServerRuntimeClientOptions extends ClientOptions<BaseTransportO
3740
export class ServerRuntimeClient<
3841
O extends ClientOptions & ServerRuntimeClientOptions = ServerRuntimeClientOptions,
3942
> extends Client<O> {
43+
private _logFlushIdleTimeout: ReturnType<typeof setTimeout> | undefined;
4044
private _logWeight: number;
4145

4246
/**
@@ -54,9 +58,9 @@ export class ServerRuntimeClient<
5458
if (this._options._experiments?.enableLogs) {
5559
// eslint-disable-next-line @typescript-eslint/no-this-alias
5660
const client = this;
57-
client.on('flush', () => {
58-
_INTERNAL_flushLogsBuffer(client);
61+
client.on('flushLogs', () => {
5962
client._logWeight = 0;
63+
clearTimeout(client._logFlushIdleTimeout);
6064
});
6165

6266
client.on('afterCaptureLog', log => {
@@ -67,7 +71,11 @@ export class ServerRuntimeClient<
6771
// the payload gets too big.
6872
if (client._logWeight >= 800_000) {
6973
_INTERNAL_flushLogsBuffer(client);
70-
client._logWeight = 0;
74+
} else {
75+
// start an idle timeout to flush the logs buffer if no logs are captured for a while
76+
client._logFlushIdleTimeout = setTimeout(() => {
77+
_INTERNAL_flushLogsBuffer(client);
78+
}, DEFAULT_LOG_FLUSH_INTERVAL);
7179
}
7280
});
7381
}

packages/core/test/lib/server-runtime-client.test.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { describe, expect, it, test, vi } from 'vitest';
44
import { Scope, createTransport } from '../../src';
55
import type { ServerRuntimeClientOptions } from '../../src/server-runtime-client';
66
import { ServerRuntimeClient } from '../../src/server-runtime-client';
7-
import { _INTERNAL_captureLog } from '../../src/logs/exports';
7+
import { _INTERNAL_captureLog, _INTERNAL_flushLogsBuffer } from '../../src/logs/exports';
88

99
const PUBLIC_DSN = 'https://username@domain/123';
1010

@@ -256,8 +256,8 @@ describe('ServerRuntimeClient', () => {
256256
_INTERNAL_captureLog({ message: 'test1', level: 'info' }, client);
257257
_INTERNAL_captureLog({ message: 'test2', level: 'info' }, client);
258258

259-
// Trigger flush event
260-
client.emit('flush');
259+
// Trigger flush directly
260+
_INTERNAL_flushLogsBuffer(client);
261261

262262
expect(sendEnvelopeSpy).toHaveBeenCalledTimes(1);
263263
expect(client['_logWeight']).toBe(0); // Weight should be reset after flush

packages/node/src/sdk/client.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { trace } from '@opentelemetry/api';
44
import { registerInstrumentations } from '@opentelemetry/instrumentation';
55
import type { BasicTracerProvider } from '@opentelemetry/sdk-trace-base';
66
import type { DynamicSamplingContext, Scope, ServerRuntimeClientOptions, TraceContext } from '@sentry/core';
7-
import { SDK_VERSION, ServerRuntimeClient, applySdkMetadata, logger } from '@sentry/core';
7+
import { _INTERNAL_flushLogsBuffer, SDK_VERSION, ServerRuntimeClient, applySdkMetadata, logger } from '@sentry/core';
88
import { getTraceContextForScope } from '@sentry/opentelemetry';
99
import { isMainThread, threadId } from 'worker_threads';
1010
import { DEBUG_BUILD } from '../debug-build';
@@ -18,6 +18,7 @@ export class NodeClient extends ServerRuntimeClient<NodeClientOptions> {
1818
private _tracer: Tracer | undefined;
1919
private _clientReportInterval: NodeJS.Timeout | undefined;
2020
private _clientReportOnExitFlushListener: (() => void) | undefined;
21+
private _logOnExitFlushListener: (() => void) | undefined;
2122

2223
public constructor(options: NodeClientOptions) {
2324
const clientOptions: ServerRuntimeClientOptions = {
@@ -40,6 +41,14 @@ export class NodeClient extends ServerRuntimeClient<NodeClientOptions> {
4041
);
4142

4243
super(clientOptions);
44+
45+
if (this.getOptions()._experiments?.enableLogs) {
46+
this._logOnExitFlushListener = () => {
47+
_INTERNAL_flushLogsBuffer(this);
48+
};
49+
50+
process.on('beforeExit', this._logOnExitFlushListener);
51+
}
4352
}
4453

4554
/** Get the OTEL tracer. */
@@ -84,6 +93,10 @@ export class NodeClient extends ServerRuntimeClient<NodeClientOptions> {
8493
process.off('beforeExit', this._clientReportOnExitFlushListener);
8594
}
8695

96+
if (this._logOnExitFlushListener) {
97+
process.off('beforeExit', this._logOnExitFlushListener);
98+
}
99+
87100
return super.close(timeout);
88101
}
89102

0 commit comments

Comments
 (0)