Skip to content

Commit 1296252

Browse files
author
Luca Forstner
committed
feat(node): Send client reports
1 parent 36d1d43 commit 1296252

File tree

3 files changed

+59
-30
lines changed

3 files changed

+59
-30
lines changed

packages/browser/src/client.ts

Lines changed: 1 addition & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import type {
1212
SeverityLevel,
1313
UserFeedback,
1414
} from '@sentry/types';
15-
import { createClientReportEnvelope, dsnToString, getSDKSource, logger } from '@sentry/utils';
15+
import { getSDKSource, logger } from '@sentry/utils';
1616

1717
import { DEBUG_BUILD } from './debug-build';
1818
import { eventFromException, eventFromMessage } from './eventbuilder';
@@ -118,30 +118,4 @@ export class BrowserClient extends BaseClient<BrowserClientOptions> {
118118
event.platform = event.platform || 'javascript';
119119
return super._prepareEvent(event, hint, scope);
120120
}
121-
122-
/**
123-
* Sends client reports as an envelope.
124-
*/
125-
private _flushOutcomes(): void {
126-
const outcomes = this._clearOutcomes();
127-
128-
if (outcomes.length === 0) {
129-
DEBUG_BUILD && logger.log('No outcomes to send');
130-
return;
131-
}
132-
133-
// This is really the only place where we want to check for a DSN and only send outcomes then
134-
if (!this._dsn) {
135-
DEBUG_BUILD && logger.log('No dsn provided, will not send outcomes');
136-
return;
137-
}
138-
139-
DEBUG_BUILD && logger.log('Sending outcomes:', outcomes);
140-
141-
const envelope = createClientReportEnvelope(outcomes, this._options.tunnel && dsnToString(this._dsn));
142-
143-
// sendEnvelope should not throw
144-
// eslint-disable-next-line @typescript-eslint/no-floating-promises
145-
this.sendEnvelope(envelope);
146-
}
147121
}

packages/core/src/baseclient.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,9 @@ import {
3636
addItemToEnvelope,
3737
checkOrSetAlreadyCaught,
3838
createAttachmentEnvelopeItem,
39+
createClientReportEnvelope,
3940
dropUndefinedKeys,
41+
dsnToString,
4042
isParameterizedString,
4143
isPlainObject,
4244
isPrimitive,
@@ -871,6 +873,32 @@ export abstract class BaseClient<O extends ClientOptions> implements Client<O> {
871873
});
872874
}
873875

876+
/**
877+
* Sends client reports as an envelope.
878+
*/
879+
protected _flushOutcomes(): void {
880+
const outcomes = this._clearOutcomes();
881+
882+
if (outcomes.length === 0) {
883+
DEBUG_BUILD && logger.log('No outcomes to send');
884+
return;
885+
}
886+
887+
// This is really the only place where we want to check for a DSN and only send outcomes then
888+
if (!this._dsn) {
889+
DEBUG_BUILD && logger.log('No dsn provided, will not send outcomes');
890+
return;
891+
}
892+
893+
DEBUG_BUILD && logger.log('Sending outcomes:', outcomes);
894+
895+
const envelope = createClientReportEnvelope(outcomes, this._options.tunnel && dsnToString(this._dsn));
896+
897+
// sendEnvelope should not throw
898+
// eslint-disable-next-line @typescript-eslint/no-floating-promises
899+
this.sendEnvelope(envelope);
900+
}
901+
874902
/**
875903
* @inheritDoc
876904
*/

packages/node/src/sdk/client.ts

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,16 @@ import type { ServerRuntimeClientOptions } from '@sentry/core';
66
import { SDK_VERSION, ServerRuntimeClient, applySdkMetadata } from '@sentry/core';
77
import { logger } from '@sentry/utils';
88
import { isMainThread, threadId } from 'worker_threads';
9+
import { DEBUG_BUILD } from '../debug-build';
910
import type { NodeClientOptions } from '../types';
1011

12+
const CLIENT_REPORT_FLUSH_INTERVAL_MS = 60_000; // 60s was chosen arbitrarily
13+
1114
/** A client for using Sentry with Node & OpenTelemetry. */
1215
export class NodeClient extends ServerRuntimeClient<NodeClientOptions> {
1316
public traceProvider: BasicTracerProvider | undefined;
1417
private _tracer: Tracer | undefined;
18+
private _clientReportInterval: NodeJS.Timeout | undefined;
1519

1620
public constructor(options: NodeClientOptions) {
1721
const clientOptions: ServerRuntimeClientOptions = {
@@ -28,6 +32,18 @@ export class NodeClient extends ServerRuntimeClient<NodeClientOptions> {
2832
);
2933

3034
super(clientOptions);
35+
36+
if (clientOptions.sendClientReports !== false) {
37+
// There is one mild concern here, being that if users periodically and unboundedly create new clients, we will
38+
// create more and more intervals, which may leak memory. In these situations, users are required to
39+
// call `client.close()` in order to dispose of the client resource.
40+
this._clientReportInterval = setInterval(() => {
41+
DEBUG_BUILD && logger.log('Flushing client reports based on interval.');
42+
this._flushOutcomes();
43+
}, CLIENT_REPORT_FLUSH_INTERVAL_MS)
44+
// Unref is critical, otherwise we stop the process from exiting by itself
45+
.unref();
46+
}
3147
}
3248

3349
/** Get the OTEL tracer. */
@@ -44,9 +60,8 @@ export class NodeClient extends ServerRuntimeClient<NodeClientOptions> {
4460
return tracer;
4561
}
4662

47-
/**
48-
* @inheritDoc
49-
*/
63+
// Eslint ignore explanation: This is already documented in super.
64+
// eslint-disable-next-line jsdoc/require-jsdoc
5065
public async flush(timeout?: number): Promise<boolean> {
5166
const provider = this.traceProvider;
5267
const spanProcessor = provider?.activeSpanProcessor;
@@ -55,6 +70,18 @@ export class NodeClient extends ServerRuntimeClient<NodeClientOptions> {
5570
await spanProcessor.forceFlush();
5671
}
5772

73+
this._flushOutcomes();
74+
5875
return super.flush(timeout);
5976
}
77+
78+
// Eslint ignore explanation: This is already documented in super.
79+
// eslint-disable-next-line jsdoc/require-jsdoc
80+
public close(timeout?: number | undefined): PromiseLike<boolean> {
81+
if (this._clientReportInterval) {
82+
clearInterval(this._clientReportInterval);
83+
}
84+
85+
return super.close(timeout);
86+
}
6087
}

0 commit comments

Comments
 (0)