Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 20s
Build & Deploy / 🧪 QA (push) Failing after 34s
Build & Deploy / 🏗️ Build (push) Has started running
Build & Deploy / 🚀 Deploy (push) Has been cancelled
Build & Deploy / 🧪 Smoke Test (push) Has been cancelled
Build & Deploy / ⚡ Lighthouse (push) Has been cancelled
Build & Deploy / 🔔 Notify (push) Has been cancelled
163 lines
5.4 KiB
Plaintext
163 lines
5.4 KiB
Plaintext
import * as os from 'node:os';
|
|
import { trace } from '@opentelemetry/api';
|
|
import { registerInstrumentations } from '@opentelemetry/instrumentation';
|
|
import { ServerRuntimeClient, applySdkMetadata, debug, _INTERNAL_flushLogsBuffer, SDK_VERSION, _INTERNAL_clearAiProviderSkips } from '@sentry/core';
|
|
import { getTraceContextForScope } from '@sentry/opentelemetry';
|
|
import { threadId, isMainThread } from 'worker_threads';
|
|
import { DEBUG_BUILD } from '../debug-build.js';
|
|
|
|
const DEFAULT_CLIENT_REPORT_FLUSH_INTERVAL_MS = 60000; // 60s was chosen arbitrarily
|
|
|
|
/** A client for using Sentry with Node & OpenTelemetry. */
|
|
class NodeClient extends ServerRuntimeClient {
|
|
|
|
constructor(options) {
|
|
const serverName =
|
|
options.includeServerName === false
|
|
? undefined
|
|
: options.serverName || global.process.env.SENTRY_NAME || os.hostname();
|
|
|
|
const clientOptions = {
|
|
...options,
|
|
platform: 'node',
|
|
// Use provided runtime or default to 'node' with current process version
|
|
runtime: options.runtime || { name: 'node', version: global.process.version },
|
|
serverName,
|
|
};
|
|
|
|
if (options.openTelemetryInstrumentations) {
|
|
registerInstrumentations({
|
|
instrumentations: options.openTelemetryInstrumentations,
|
|
});
|
|
}
|
|
|
|
applySdkMetadata(clientOptions, 'node');
|
|
|
|
debug.log(`Initializing Sentry: process: ${process.pid}, thread: ${isMainThread ? 'main' : `worker-${threadId}`}.`);
|
|
|
|
super(clientOptions);
|
|
|
|
if (this.getOptions().enableLogs) {
|
|
this._logOnExitFlushListener = () => {
|
|
_INTERNAL_flushLogsBuffer(this);
|
|
};
|
|
|
|
if (serverName) {
|
|
this.on('beforeCaptureLog', log => {
|
|
log.attributes = {
|
|
...log.attributes,
|
|
'server.address': serverName,
|
|
};
|
|
});
|
|
}
|
|
|
|
process.on('beforeExit', this._logOnExitFlushListener);
|
|
}
|
|
}
|
|
|
|
/** Get the OTEL tracer. */
|
|
get tracer() {
|
|
if (this._tracer) {
|
|
return this._tracer;
|
|
}
|
|
|
|
const name = '@sentry/node';
|
|
const version = SDK_VERSION;
|
|
const tracer = trace.getTracer(name, version);
|
|
this._tracer = tracer;
|
|
|
|
return tracer;
|
|
}
|
|
|
|
/** @inheritDoc */
|
|
// @ts-expect-error - PromiseLike is a subset of Promise
|
|
async flush(timeout) {
|
|
await this.traceProvider?.forceFlush();
|
|
|
|
if (this.getOptions().sendClientReports) {
|
|
this._flushOutcomes();
|
|
}
|
|
|
|
return super.flush(timeout);
|
|
}
|
|
|
|
/** @inheritDoc */
|
|
// @ts-expect-error - PromiseLike is a subset of Promise
|
|
async close(timeout) {
|
|
if (this._clientReportInterval) {
|
|
clearInterval(this._clientReportInterval);
|
|
}
|
|
|
|
if (this._clientReportOnExitFlushListener) {
|
|
process.off('beforeExit', this._clientReportOnExitFlushListener);
|
|
}
|
|
|
|
if (this._logOnExitFlushListener) {
|
|
process.off('beforeExit', this._logOnExitFlushListener);
|
|
}
|
|
|
|
const allEventsSent = await super.close(timeout);
|
|
if (this.traceProvider) {
|
|
await this.traceProvider.shutdown();
|
|
}
|
|
|
|
return allEventsSent;
|
|
}
|
|
|
|
/**
|
|
* Will start tracking client reports for this client.
|
|
*
|
|
* NOTICE: This method will create an interval that is periodically called and attach a `process.on('beforeExit')`
|
|
* hook. To clean up these resources, call `.close()` when you no longer intend to use the client. Not doing so will
|
|
* result in a memory leak.
|
|
*/
|
|
// The reason client reports need to be manually activated with this method instead of just enabling them in a
|
|
// constructor, is that if users periodically and unboundedly create new clients, we will create more and more
|
|
// intervals and beforeExit listeners, thus leaking memory. In these situations, users are required to call
|
|
// `client.close()` in order to dispose of the acquired resources.
|
|
// We assume that calling this method in Sentry.init() is a sensible default, because calling Sentry.init() over and
|
|
// over again would also result in memory leaks.
|
|
// Note: We have experimented with using `FinalizationRegisty` to clear the interval when the client is garbage
|
|
// collected, but it did not work, because the cleanup function never got called.
|
|
startClientReportTracking() {
|
|
const clientOptions = this.getOptions();
|
|
if (clientOptions.sendClientReports) {
|
|
this._clientReportOnExitFlushListener = () => {
|
|
this._flushOutcomes();
|
|
};
|
|
|
|
this._clientReportInterval = setInterval(() => {
|
|
DEBUG_BUILD && debug.log('Flushing client reports based on interval.');
|
|
this._flushOutcomes();
|
|
}, clientOptions.clientReportFlushInterval ?? DEFAULT_CLIENT_REPORT_FLUSH_INTERVAL_MS)
|
|
// Unref is critical for not preventing the process from exiting because the interval is active.
|
|
.unref();
|
|
|
|
process.on('beforeExit', this._clientReportOnExitFlushListener);
|
|
}
|
|
}
|
|
|
|
/** @inheritDoc */
|
|
_setupIntegrations() {
|
|
// Clear AI provider skip registrations before setting up integrations
|
|
// This ensures a clean state between different client initializations
|
|
// (e.g., when LangChain skips OpenAI in one client, but a subsequent client uses OpenAI standalone)
|
|
_INTERNAL_clearAiProviderSkips();
|
|
super._setupIntegrations();
|
|
}
|
|
|
|
/** Custom implementation for OTEL, so we can handle scope-span linking. */
|
|
_getTraceInfoFromScope(
|
|
scope,
|
|
) {
|
|
if (!scope) {
|
|
return [undefined, undefined];
|
|
}
|
|
|
|
return getTraceContextForScope(this, scope);
|
|
}
|
|
}
|
|
|
|
export { NodeClient };
|
|
//# sourceMappingURL=client.js.map
|