From 9d70bd52304e6df96efe8857e90e4a04334875f2 Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Wed, 3 Jan 2024 09:30:22 +0100 Subject: [PATCH] ref(node): Refactor node integrations to functional syntax --- packages/core/src/integration.ts | 4 +- packages/node/src/index.ts | 19 +- packages/node/src/integrations/anr/index.ts | 176 +++++++++--------- packages/node/src/integrations/console.ts | 68 +++---- packages/node/src/integrations/context.ts | 93 ++++----- .../node/src/integrations/contextlines.ts | 150 +++++++-------- packages/node/src/integrations/hapi/index.ts | 68 +++---- packages/node/src/integrations/modules.ts | 48 ++--- .../src/integrations/onuncaughtexception.ts | 59 ++---- .../src/integrations/onunhandledrejection.ts | 43 ++--- packages/node/src/integrations/spotlight.ts | 52 +++--- .../contextlines.test.ts} | 12 +- 12 files changed, 346 insertions(+), 446 deletions(-) rename packages/node/test/{context-lines.test.ts => integrations/contextlines.test.ts} (88%) diff --git a/packages/core/src/integration.ts b/packages/core/src/integration.ts index d587ddb55ce8..f9be8b325782 100644 --- a/packages/core/src/integration.ts +++ b/packages/core/src/integration.ts @@ -165,7 +165,7 @@ function findIndex(arr: T[], callback: (item: T) => boolean): number { export function convertIntegrationFnToClass( name: string, fn: Fn, -): { +): Integration & { id: string; new (...args: Parameters): Integration & ReturnType & { @@ -182,7 +182,7 @@ export function convertIntegrationFnToClass( }; }, { id: name }, - ) as unknown as { + ) as unknown as Integration & { id: string; new (...args: Parameters): Integration & ReturnType & { diff --git a/packages/node/src/index.ts b/packages/node/src/index.ts index 36d2d8beac53..c1db9de54194 100644 --- a/packages/node/src/index.ts +++ b/packages/node/src/index.ts @@ -86,6 +86,7 @@ export { getModuleFromFilename } from './module'; export { enableAnrDetection } from './integrations/anr/legacy'; import { Integrations as CoreIntegrations } from '@sentry/core'; +import type { Integration, IntegrationClass } from '@sentry/types'; import * as Handlers from './handlers'; import * as NodeIntegrations from './integrations'; @@ -93,7 +94,23 @@ import * as TracingIntegrations from './tracing/integrations'; const INTEGRATIONS = { ...CoreIntegrations, - ...NodeIntegrations, + // This typecast is somehow needed for now, probably because of the convertIntegrationFnToClass TS shenanigans + // This is OK for now but should be resolved in v8 when we just pass the functional integrations directly + ...(NodeIntegrations as { + Console: IntegrationClass; + Http: typeof NodeIntegrations.Http; + OnUncaughtException: IntegrationClass; + OnUnhandledRejection: IntegrationClass; + Modules: IntegrationClass; + ContextLines: IntegrationClass; + Context: IntegrationClass; + RequestData: IntegrationClass; + LocalVariables: IntegrationClass; + Undici: typeof NodeIntegrations.Undici; + Spotlight: IntegrationClass; + Anr: IntegrationClass; + Hapi: IntegrationClass; + }), ...TracingIntegrations, }; diff --git a/packages/node/src/integrations/anr/index.ts b/packages/node/src/integrations/anr/index.ts index 70c20962fca2..47bc13a34ecd 100644 --- a/packages/node/src/integrations/anr/index.ts +++ b/packages/node/src/integrations/anr/index.ts @@ -1,7 +1,7 @@ // TODO (v8): This import can be removed once we only support Node with global URL import { URL } from 'url'; -import { getCurrentScope } from '@sentry/core'; -import type { Contexts, Event, EventHint, Integration } from '@sentry/types'; +import { convertIntegrationFnToClass, getCurrentScope } from '@sentry/core'; +import type { Contexts, Event, EventHint, IntegrationFn } from '@sentry/types'; import { dynamicRequire, logger } from '@sentry/utils'; import type { Worker, WorkerOptions } from 'worker_threads'; import type { NodeClient } from '../../client'; @@ -50,108 +50,106 @@ interface InspectorApi { url: () => string | undefined; } +const INTEGRATION_NAME = 'Anr'; + +const anrIntegration = ((options: Partial = {}) => { + return { + name: INTEGRATION_NAME, + setup(client: NodeClient) { + if (NODE_VERSION.major < 16) { + throw new Error('ANR detection requires Node 16 or later'); + } + + // setImmediate is used to ensure that all other integrations have been setup + setImmediate(() => _startWorker(client, options)); + }, + }; +}) satisfies IntegrationFn; + /** * Starts a thread to detect App Not Responding (ANR) events */ -export class Anr implements Integration { - public name: string = 'Anr'; +// eslint-disable-next-line deprecation/deprecation +export const Anr = convertIntegrationFnToClass(INTEGRATION_NAME, anrIntegration); - public constructor(private readonly _options: Partial = {}) {} +/** + * Starts the ANR worker thread + */ +async function _startWorker(client: NodeClient, _options: Partial): Promise { + const contexts = await getContexts(client); + const dsn = client.getDsn(); - /** @inheritdoc */ - public setupOnce(): void { - // Do nothing + if (!dsn) { + return; } - /** @inheritdoc */ - public setup(client: NodeClient): void { - if (NODE_VERSION.major < 16) { - throw new Error('ANR detection requires Node 16 or later'); - } + // These will not be accurate if sent later from the worker thread + delete contexts.app?.app_memory; + delete contexts.device?.free_memory; - // setImmediate is used to ensure that all other integrations have been setup - setImmediate(() => this._startWorker(client)); - } + const initOptions = client.getOptions(); - /** - * Starts the ANR worker thread - */ - private async _startWorker(client: NodeClient): Promise { - const contexts = await getContexts(client); - const dsn = client.getDsn(); + const sdkMetadata = client.getSdkMetadata() || {}; + if (sdkMetadata.sdk) { + sdkMetadata.sdk.integrations = initOptions.integrations.map(i => i.name); + } - if (!dsn) { - return; + const options: WorkerStartData = { + debug: logger.isEnabled(), + dsn, + environment: initOptions.environment || 'production', + release: initOptions.release, + dist: initOptions.dist, + sdkMetadata, + pollInterval: _options.pollInterval || DEFAULT_INTERVAL, + anrThreshold: _options.anrThreshold || DEFAULT_HANG_THRESHOLD, + captureStackTrace: !!_options.captureStackTrace, + contexts, + }; + + if (options.captureStackTrace) { + // eslint-disable-next-line @typescript-eslint/no-var-requires + const inspector: InspectorApi = require('inspector'); + if (!inspector.url()) { + inspector.open(0); } + } - // These will not be accurate if sent later from the worker thread - delete contexts.app?.app_memory; - delete contexts.device?.free_memory; - - const initOptions = client.getOptions(); - - const sdkMetadata = client.getSdkMetadata() || {}; - if (sdkMetadata.sdk) { - sdkMetadata.sdk.integrations = initOptions.integrations.map(i => i.name); + const { Worker } = getWorkerThreads(); + + const worker = new Worker(new URL(`data:application/javascript;base64,${base64WorkerScript}`), { + workerData: options, + }); + // Ensure this thread can't block app exit + worker.unref(); + + const timer = setInterval(() => { + try { + const currentSession = getCurrentScope().getSession(); + // We need to copy the session object and remove the toJSON method so it can be sent to the worker + // serialized without making it a SerializedSession + const session = currentSession ? { ...currentSession, toJSON: undefined } : undefined; + // message the worker to tell it the main event loop is still running + worker.postMessage({ session }); + } catch (_) { + // } + }, options.pollInterval); - const options: WorkerStartData = { - debug: logger.isEnabled(), - dsn, - environment: initOptions.environment || 'production', - release: initOptions.release, - dist: initOptions.dist, - sdkMetadata, - pollInterval: this._options.pollInterval || DEFAULT_INTERVAL, - anrThreshold: this._options.anrThreshold || DEFAULT_HANG_THRESHOLD, - captureStackTrace: !!this._options.captureStackTrace, - contexts, - }; - - if (options.captureStackTrace) { - // eslint-disable-next-line @typescript-eslint/no-var-requires - const inspector: InspectorApi = require('inspector'); - if (!inspector.url()) { - inspector.open(0); - } + worker.on('message', (msg: string) => { + if (msg === 'session-ended') { + log('ANR event sent from ANR worker. Clearing session in this thread.'); + getCurrentScope().setSession(undefined); } + }); - const { Worker } = getWorkerThreads(); - - const worker = new Worker(new URL(`data:application/javascript;base64,${base64WorkerScript}`), { - workerData: options, - }); - // Ensure this thread can't block app exit - worker.unref(); - - const timer = setInterval(() => { - try { - const currentSession = getCurrentScope().getSession(); - // We need to copy the session object and remove the toJSON method so it can be sent to the worker - // serialized without making it a SerializedSession - const session = currentSession ? { ...currentSession, toJSON: undefined } : undefined; - // message the worker to tell it the main event loop is still running - worker.postMessage({ session }); - } catch (_) { - // - } - }, options.pollInterval); + worker.once('error', (err: Error) => { + clearInterval(timer); + log('ANR worker error', err); + }); - worker.on('message', (msg: string) => { - if (msg === 'session-ended') { - log('ANR event sent from ANR worker. Clearing session in this thread.'); - getCurrentScope().setSession(undefined); - } - }); - - worker.once('error', (err: Error) => { - clearInterval(timer); - log('ANR worker error', err); - }); - - worker.once('exit', (code: number) => { - clearInterval(timer); - log('ANR worker exit', code); - }); - } + worker.once('exit', (code: number) => { + clearInterval(timer); + log('ANR worker exit', code); + }); } diff --git a/packages/node/src/integrations/console.ts b/packages/node/src/integrations/console.ts index 008d3fba591b..d0243d7e3985 100644 --- a/packages/node/src/integrations/console.ts +++ b/packages/node/src/integrations/console.ts @@ -1,45 +1,35 @@ import * as util from 'util'; -import { addBreadcrumb, getClient } from '@sentry/core'; -import type { Client, Integration } from '@sentry/types'; +import { addBreadcrumb, convertIntegrationFnToClass, getClient } from '@sentry/core'; +import type { IntegrationFn } from '@sentry/types'; import { addConsoleInstrumentationHandler, severityLevelFromString } from '@sentry/utils'; -/** Console module integration */ -export class Console implements Integration { - /** - * @inheritDoc - */ - public static id: string = 'Console'; - - /** - * @inheritDoc - */ - public name: string = Console.id; +const INTEGRATION_NAME = 'Console'; - /** - * @inheritDoc - */ - public setupOnce(): void { - // noop - } +const consoleIntegration = (() => { + return { + name: INTEGRATION_NAME, + setup(client) { + addConsoleInstrumentationHandler(({ args, level }) => { + if (getClient() !== client) { + return; + } - /** @inheritdoc */ - public setup(client: Client): void { - addConsoleInstrumentationHandler(({ args, level }) => { - if (getClient() !== client) { - return; - } + addBreadcrumb( + { + category: 'console', + level: severityLevelFromString(level), + message: util.format.apply(undefined, args), + }, + { + input: [...args], + level, + }, + ); + }); + }, + }; +}) satisfies IntegrationFn; - addBreadcrumb( - { - category: 'console', - level: severityLevelFromString(level), - message: util.format.apply(undefined, args), - }, - { - input: [...args], - level, - }, - ); - }); - } -} +/** Console module integration */ +// eslint-disable-next-line deprecation/deprecation +export const Console = convertIntegrationFnToClass(INTEGRATION_NAME, consoleIntegration); diff --git a/packages/node/src/integrations/context.ts b/packages/node/src/integrations/context.ts index ee565c8676e7..2b16ec6a4527 100644 --- a/packages/node/src/integrations/context.ts +++ b/packages/node/src/integrations/context.ts @@ -1,9 +1,10 @@ +/* eslint-disable max-lines */ import { execFile } from 'child_process'; import { readFile, readdir } from 'fs'; import * as os from 'os'; import { join } from 'path'; import { promisify } from 'util'; -/* eslint-disable max-lines */ +import { convertIntegrationFnToClass } from '@sentry/core'; import type { AppContext, CloudResourceContext, @@ -11,7 +12,7 @@ import type { CultureContext, DeviceContext, Event, - Integration, + IntegrationFn, OsContext, } from '@sentry/types'; @@ -19,6 +20,8 @@ import type { export const readFileAsync = promisify(readFile); export const readDirAsync = promisify(readdir); +const INTEGRATION_NAME = 'Context'; + interface DeviceContextOptions { cpu?: boolean; memory?: boolean; @@ -32,52 +35,24 @@ interface ContextOptions { cloudResource?: boolean; } -/** Add node modules / packages to the event */ -export class Context implements Integration { - /** - * @inheritDoc - */ - public static id: string = 'Context'; - - /** - * @inheritDoc - */ - public name: string = Context.id; - - /** - * Caches context so it's only evaluated once - */ - private _cachedContext: Promise | undefined; - - public constructor( - private readonly _options: ContextOptions = { - app: true, - os: true, - device: true, - culture: true, - cloudResource: true, - }, - ) {} - - /** @inheritDoc */ - public setupOnce(_addGlobaleventProcessor: unknown, _getCurrentHub: unknown): void { - // noop - } +const contextIntegration = ((options: ContextOptions = {}) => { + let cachedContext: Promise | undefined; - /** @inheritDoc */ - public processEvent(event: Event): Promise { - return this.addContext(event); - } + const _options = { + app: true, + os: true, + device: true, + culture: true, + cloudResource: true, + ...options, + }; - /** - * Processes an event and adds context. - */ - public async addContext(event: Event): Promise { - if (this._cachedContext === undefined) { - this._cachedContext = this._getContexts(); + async function addContext(event: Event): Promise { + if (cachedContext === undefined) { + cachedContext = _getContexts(); } - const updatedContext = _updateContext(await this._cachedContext); + const updatedContext = _updateContext(await cachedContext); event.contexts = { ...event.contexts, @@ -91,25 +66,22 @@ export class Context implements Integration { return event; } - /** - * Gets the contexts for the current environment - */ - private async _getContexts(): Promise { + async function _getContexts(): Promise { const contexts: Contexts = {}; - if (this._options.os) { + if (_options.os) { contexts.os = await getOsContext(); } - if (this._options.app) { + if (_options.app) { contexts.app = getAppContext(); } - if (this._options.device) { - contexts.device = getDeviceContext(this._options.device); + if (_options.device) { + contexts.device = getDeviceContext(_options.device); } - if (this._options.culture) { + if (_options.culture) { const culture = getCultureContext(); if (culture) { @@ -117,13 +89,24 @@ export class Context implements Integration { } } - if (this._options.cloudResource) { + if (_options.cloudResource) { contexts.cloud_resource = getCloudResourceContext(); } return contexts; } -} + + return { + name: INTEGRATION_NAME, + processEvent(event) { + return addContext(event); + }, + }; +}) satisfies IntegrationFn; + +/** Add node modules / packages to the event */ +// eslint-disable-next-line deprecation/deprecation +export const Context = convertIntegrationFnToClass(INTEGRATION_NAME, contextIntegration); /** * Updates the context with dynamic values that can change diff --git a/packages/node/src/integrations/contextlines.ts b/packages/node/src/integrations/contextlines.ts index 2cc9375a879a..0a961ddbc259 100644 --- a/packages/node/src/integrations/contextlines.ts +++ b/packages/node/src/integrations/contextlines.ts @@ -1,9 +1,11 @@ import { readFile } from 'fs'; -import type { Event, EventProcessor, Hub, Integration, StackFrame } from '@sentry/types'; +import { convertIntegrationFnToClass } from '@sentry/core'; +import type { Event, IntegrationFn, StackFrame } from '@sentry/types'; import { LRUMap, addContextToFrame } from '@sentry/utils'; const FILE_CONTENT_CACHE = new LRUMap(100); const DEFAULT_LINES_OF_CONTEXT = 7; +const INTEGRATION_NAME = 'ContextLines'; // TODO: Replace with promisify when minimum supported node >= v8 function readTextFileAsync(path: string): Promise { @@ -33,102 +35,80 @@ interface ContextLinesOptions { frameContextLines?: number; } -/** Add node modules / packages to the event */ -export class ContextLines implements Integration { - /** - * @inheritDoc - */ - public static id: string = 'ContextLines'; - - /** - * @inheritDoc - */ - public name: string = ContextLines.id; - - public constructor(private readonly _options: ContextLinesOptions = {}) {} +const contextLinesIntegration = ((options: ContextLinesOptions = {}) => { + const contextLines = options.frameContextLines !== undefined ? options.frameContextLines : DEFAULT_LINES_OF_CONTEXT; - /** Get's the number of context lines to add */ - private get _contextLines(): number { - return this._options.frameContextLines !== undefined ? this._options.frameContextLines : DEFAULT_LINES_OF_CONTEXT; - } - - /** - * @inheritDoc - */ - public setupOnce(_addGlobalEventProcessor: (callback: EventProcessor) => void, getCurrentHub: () => Hub): void { - // noop - } - - /** @inheritDoc */ - public processEvent(event: Event): Promise { - return this.addSourceContext(event); - } + return { + name: INTEGRATION_NAME, + processEvent(event) { + return addSourceContext(event, contextLines); + }, + }; +}) satisfies IntegrationFn; - /** Processes an event and adds context lines */ - public async addSourceContext(event: Event): Promise { - // keep a lookup map of which files we've already enqueued to read, - // so we don't enqueue the same file multiple times which would cause multiple i/o reads - const enqueuedReadSourceFileTasks: Record = {}; - const readSourceFileTasks: Promise[] = []; - - if (this._contextLines > 0 && event.exception?.values) { - for (const exception of event.exception.values) { - if (!exception.stacktrace?.frames) { - continue; - } +/** Add node modules / packages to the event */ +// eslint-disable-next-line deprecation/deprecation +export const ContextLines = convertIntegrationFnToClass(INTEGRATION_NAME, contextLinesIntegration); + +async function addSourceContext(event: Event, contextLines: number): Promise { + // keep a lookup map of which files we've already enqueued to read, + // so we don't enqueue the same file multiple times which would cause multiple i/o reads + const enqueuedReadSourceFileTasks: Record = {}; + const readSourceFileTasks: Promise[] = []; + + if (contextLines > 0 && event.exception?.values) { + for (const exception of event.exception.values) { + if (!exception.stacktrace?.frames) { + continue; + } - // We want to iterate in reverse order as calling cache.get will bump the file in our LRU cache. - // This ends up prioritizes source context for frames at the top of the stack instead of the bottom. - for (let i = exception.stacktrace.frames.length - 1; i >= 0; i--) { - const frame = exception.stacktrace.frames[i]; - // Call cache.get to bump the file to the top of the cache and ensure we have not already - // enqueued a read operation for this filename - if ( - frame.filename && - !enqueuedReadSourceFileTasks[frame.filename] && - !FILE_CONTENT_CACHE.get(frame.filename) - ) { - readSourceFileTasks.push(_readSourceFile(frame.filename)); - enqueuedReadSourceFileTasks[frame.filename] = 1; - } + // We want to iterate in reverse order as calling cache.get will bump the file in our LRU cache. + // This ends up prioritizes source context for frames at the top of the stack instead of the bottom. + for (let i = exception.stacktrace.frames.length - 1; i >= 0; i--) { + const frame = exception.stacktrace.frames[i]; + // Call cache.get to bump the file to the top of the cache and ensure we have not already + // enqueued a read operation for this filename + if (frame.filename && !enqueuedReadSourceFileTasks[frame.filename] && !FILE_CONTENT_CACHE.get(frame.filename)) { + readSourceFileTasks.push(_readSourceFile(frame.filename)); + enqueuedReadSourceFileTasks[frame.filename] = 1; } } } + } - // check if files to read > 0, if so, await all of them to be read before adding source contexts. - // Normally, Promise.all here could be short circuited if one of the promises rejects, but we - // are guarding from that by wrapping the i/o read operation in a try/catch. - if (readSourceFileTasks.length > 0) { - await Promise.all(readSourceFileTasks); - } + // check if files to read > 0, if so, await all of them to be read before adding source contexts. + // Normally, Promise.all here could be short circuited if one of the promises rejects, but we + // are guarding from that by wrapping the i/o read operation in a try/catch. + if (readSourceFileTasks.length > 0) { + await Promise.all(readSourceFileTasks); + } - // Perform the same loop as above, but this time we can assume all files are in the cache - // and attempt to add source context to frames. - if (this._contextLines > 0 && event.exception?.values) { - for (const exception of event.exception.values) { - if (exception.stacktrace && exception.stacktrace.frames) { - await this.addSourceContextToFrames(exception.stacktrace.frames); - } + // Perform the same loop as above, but this time we can assume all files are in the cache + // and attempt to add source context to frames. + if (contextLines > 0 && event.exception?.values) { + for (const exception of event.exception.values) { + if (exception.stacktrace && exception.stacktrace.frames) { + await addSourceContextToFrames(exception.stacktrace.frames, contextLines); } } - - return event; } - /** Adds context lines to frames */ - public addSourceContextToFrames(frames: StackFrame[]): void { - for (const frame of frames) { - // Only add context if we have a filename and it hasn't already been added - if (frame.filename && frame.context_line === undefined) { - const sourceFileLines = FILE_CONTENT_CACHE.get(frame.filename); - - if (sourceFileLines) { - try { - addContextToFrame(sourceFileLines, frame, this._contextLines); - } catch (e) { - // anomaly, being defensive in case - // unlikely to ever happen in practice but can definitely happen in theory - } + return event; +} + +/** Adds context lines to frames */ +function addSourceContextToFrames(frames: StackFrame[], contextLines: number): void { + for (const frame of frames) { + // Only add context if we have a filename and it hasn't already been added + if (frame.filename && frame.context_line === undefined) { + const sourceFileLines = FILE_CONTENT_CACHE.get(frame.filename); + + if (sourceFileLines) { + try { + addContextToFrame(sourceFileLines, frame, contextLines); + } catch (e) { + // anomaly, being defensive in case + // unlikely to ever happen in practice but can definitely happen in theory } } } diff --git a/packages/node/src/integrations/hapi/index.ts b/packages/node/src/integrations/hapi/index.ts index d63b831da4e2..15ff63be5f70 100644 --- a/packages/node/src/integrations/hapi/index.ts +++ b/packages/node/src/integrations/hapi/index.ts @@ -2,11 +2,12 @@ import { SDK_VERSION, captureException, continueTrace, + convertIntegrationFnToClass, getActiveTransaction, getCurrentScope, startTransaction, } from '@sentry/core'; -import type { Integration } from '@sentry/types'; +import type { IntegrationFn } from '@sentry/types'; import { dynamicSamplingContextToSentryBaggageHeader, fill } from '@sentry/utils'; import type { Boom, RequestEvent, ResponseObject, Server } from './types'; @@ -128,45 +129,32 @@ export type HapiOptions = { server?: Record; }; +const INTEGRATION_NAME = 'Hapi'; + +const hapiIntegration = ((options: HapiOptions = {}) => { + const server = options.server as undefined | Server; + + return { + name: INTEGRATION_NAME, + setupOnce() { + if (!server) { + return; + } + + fill(server, 'start', (originalStart: () => void) => { + return async function (this: Server) { + await this.register(hapiTracingPlugin); + await this.register(hapiErrorPlugin); + const result = originalStart.apply(this); + return result; + }; + }); + }, + }; +}) satisfies IntegrationFn; + /** * Hapi Framework Integration */ -export class Hapi implements Integration { - /** - * @inheritDoc - */ - public static id: string = 'Hapi'; - - /** - * @inheritDoc - */ - public name: string; - - public _hapiServer: Server | undefined; - - public constructor(options?: HapiOptions) { - if (options?.server) { - const server = options.server as unknown as Server; - - this._hapiServer = server; - } - - this.name = Hapi.id; - } - - /** @inheritDoc */ - public setupOnce(): void { - if (!this._hapiServer) { - return; - } - - fill(this._hapiServer, 'start', (originalStart: () => void) => { - return async function (this: Server) { - await this.register(hapiTracingPlugin); - await this.register(hapiErrorPlugin); - const result = originalStart.apply(this); - return result; - }; - }); - } -} +// eslint-disable-next-line deprecation/deprecation +export const Hapi = convertIntegrationFnToClass(INTEGRATION_NAME, hapiIntegration); diff --git a/packages/node/src/integrations/modules.ts b/packages/node/src/integrations/modules.ts index cc8ecf621bb9..ad6549dd3b3b 100644 --- a/packages/node/src/integrations/modules.ts +++ b/packages/node/src/integrations/modules.ts @@ -1,9 +1,12 @@ import { existsSync, readFileSync } from 'fs'; import { dirname, join } from 'path'; -import type { Event, EventProcessor, Hub, Integration } from '@sentry/types'; +import { convertIntegrationFnToClass } from '@sentry/core'; +import type { IntegrationFn } from '@sentry/types'; let moduleCache: { [key: string]: string }; +const INTEGRATION_NAME = 'Modules'; + /** Extract information about paths */ function getPaths(): string[] { try { @@ -73,33 +76,20 @@ function _getModules(): { [key: string]: string } { return moduleCache; } -/** Add node modules / packages to the event */ -export class Modules implements Integration { - /** - * @inheritDoc - */ - public static id: string = 'Modules'; - - /** - * @inheritDoc - */ - public name: string = Modules.id; - - /** - * @inheritDoc - */ - public setupOnce(_addGlobalEventProcessor: (callback: EventProcessor) => void, getCurrentHub: () => Hub): void { - // noop - } - - /** @inheritdoc */ - public processEvent(event: Event): Event { - return { - ...event, - modules: { +const modulesIntegration = (() => { + return { + name: INTEGRATION_NAME, + processEvent(event) { + event.modules = { ...event.modules, ..._getModules(), - }, - }; - } -} + }; + + return event; + }, + }; +}) satisfies IntegrationFn; + +/** Add node modules / packages to the event */ +// eslint-disable-next-line deprecation/deprecation +export const Modules = convertIntegrationFnToClass(INTEGRATION_NAME, modulesIntegration); diff --git a/packages/node/src/integrations/onuncaughtexception.ts b/packages/node/src/integrations/onuncaughtexception.ts index d6e1b50a4eb9..e9c724f89e7f 100644 --- a/packages/node/src/integrations/onuncaughtexception.ts +++ b/packages/node/src/integrations/onuncaughtexception.ts @@ -1,6 +1,6 @@ -import { captureException } from '@sentry/core'; +import { captureException, convertIntegrationFnToClass } from '@sentry/core'; import { getClient } from '@sentry/core'; -import type { Integration } from '@sentry/types'; +import type { IntegrationFn } from '@sentry/types'; import { logger } from '@sentry/utils'; import type { NodeClient } from '../client'; @@ -38,50 +38,25 @@ interface OnUncaughtExceptionOptions { onFatalError?(this: void, firstError: Error, secondError?: Error): void; } -/** Global Exception handler */ -export class OnUncaughtException implements Integration { - /** - * @inheritDoc - */ - public static id: string = 'OnUncaughtException'; +const INTEGRATION_NAME = 'OnUncaughtException'; - /** - * @inheritDoc - */ - public name: string = OnUncaughtException.id; - - // CAREFUL: Please think twice before updating the way _options looks because the Next.js SDK depends on it in `index.server.ts` - private readonly _options: OnUncaughtExceptionOptions; - - /** - * @inheritDoc - */ - public constructor(options: Partial = {}) { - this._options = { - exitEvenIfOtherHandlersAreRegistered: true, - ...options, - }; - } +const onUncaughtExceptionIntegration = ((options: Partial = {}) => { + const _options = { + exitEvenIfOtherHandlersAreRegistered: true, + ...options, + }; - /** - * @deprecated This does nothing anymore. - */ - public readonly handler: (error: Error) => void = () => { - // noop + return { + name: INTEGRATION_NAME, + setup(client: NodeClient) { + global.process.on('uncaughtException', makeErrorHandler(client, _options)); + }, }; +}) satisfies IntegrationFn; - /** - * @inheritDoc - */ - public setupOnce(): void { - // noop - } - - /** @inheritdoc */ - public setup(client: NodeClient): void { - global.process.on('uncaughtException', makeErrorHandler(client, this._options)); - } -} +/** Global Exception handler */ +// eslint-disable-next-line deprecation/deprecation +export const OnUncaughtException = convertIntegrationFnToClass(INTEGRATION_NAME, onUncaughtExceptionIntegration); type ErrorHandler = { _errorHandler: boolean } & ((error: Error) => void); diff --git a/packages/node/src/integrations/onunhandledrejection.ts b/packages/node/src/integrations/onunhandledrejection.ts index cc5209233761..85caecf9cafc 100644 --- a/packages/node/src/integrations/onunhandledrejection.ts +++ b/packages/node/src/integrations/onunhandledrejection.ts @@ -1,5 +1,5 @@ -import { captureException, getClient } from '@sentry/core'; -import type { Client, Integration } from '@sentry/types'; +import { captureException, convertIntegrationFnToClass, getClient } from '@sentry/core'; +import type { Client, IntegrationFn } from '@sentry/types'; import { consoleSandbox } from '@sentry/utils'; import { logAndExitProcess } from './utils/errorhandling'; @@ -14,35 +14,22 @@ interface OnUnhandledRejectionOptions { mode: UnhandledRejectionMode; } -/** Global Promise Rejection handler */ -export class OnUnhandledRejection implements Integration { - /** - * @inheritDoc - */ - public static id: string = 'OnUnhandledRejection'; - - /** - * @inheritDoc - */ - public name: string = OnUnhandledRejection.id; +const INTEGRATION_NAME = 'OnUnhandledRejection'; - /** - * @inheritDoc - */ - public constructor(private readonly _options: OnUnhandledRejectionOptions = { mode: 'warn' }) {} +const onUnhandledRejectionIntegration = ((options: Partial = {}) => { + const mode = options.mode || 'warn'; - /** - * @inheritDoc - */ - public setupOnce(): void { - // noop - } + return { + name: INTEGRATION_NAME, + setup(client) { + global.process.on('unhandledRejection', makeUnhandledPromiseHandler(client, { mode })); + }, + }; +}) satisfies IntegrationFn; - /** @inheritdoc */ - public setup(client: Client): void { - global.process.on('unhandledRejection', makeUnhandledPromiseHandler(client, this._options)); - } -} +/** Global Promise Rejection handler */ +// eslint-disable-next-line deprecation/deprecation +export const OnUnhandledRejection = convertIntegrationFnToClass(INTEGRATION_NAME, onUnhandledRejectionIntegration); /** * Send an exception with reason diff --git a/packages/node/src/integrations/spotlight.ts b/packages/node/src/integrations/spotlight.ts index 55fabc284cad..10afd2245e9a 100644 --- a/packages/node/src/integrations/spotlight.ts +++ b/packages/node/src/integrations/spotlight.ts @@ -1,6 +1,7 @@ import * as http from 'http'; import { URL } from 'url'; -import type { Client, Envelope, Integration } from '@sentry/types'; +import { convertIntegrationFnToClass } from '@sentry/core'; +import type { Client, Envelope, IntegrationFn } from '@sentry/types'; import { logger, serializeEnvelope } from '@sentry/utils'; type SpotlightConnectionOptions = { @@ -11,6 +12,24 @@ type SpotlightConnectionOptions = { sidecarUrl?: string; }; +const INTEGRATION_NAME = 'Spotlight'; + +const spotlightIntegration = ((options: Partial = {}) => { + const _options = { + sidecarUrl: options.sidecarUrl || 'http://localhost:8969/stream', + }; + + return { + name: INTEGRATION_NAME, + setup(client) { + if (typeof process === 'object' && process.env && process.env.NODE_ENV !== 'development') { + logger.warn("[Spotlight] It seems you're not in dev mode. Do you really want to have Spotlight enabled?"); + } + connectToSpotlight(client, _options); + }, + }; +}) satisfies IntegrationFn; + /** * Use this integration to send errors and transactions to Spotlight. * @@ -18,35 +37,8 @@ type SpotlightConnectionOptions = { * * Important: This integration only works with Node 18 or newer */ -export class Spotlight implements Integration { - public static id = 'Spotlight'; - public name = Spotlight.id; - - private readonly _options: Required; - - public constructor(options?: SpotlightConnectionOptions) { - this._options = { - sidecarUrl: options?.sidecarUrl || 'http://localhost:8969/stream', - }; - } - - /** - * JSDoc - */ - public setupOnce(): void { - // empty but otherwise TS complains - } - - /** - * Sets up forwarding envelopes to the Spotlight Sidecar - */ - public setup(client: Client): void { - if (typeof process === 'object' && process.env && process.env.NODE_ENV !== 'development') { - logger.warn("[Spotlight] It seems you're not in dev mode. Do you really want to have Spotlight enabled?"); - } - connectToSpotlight(client, this._options); - } -} +// eslint-disable-next-line deprecation/deprecation +export const Spotlight = convertIntegrationFnToClass(INTEGRATION_NAME, spotlightIntegration); function connectToSpotlight(client: Client, options: Required): void { const spotlightUrl = parseSidecarUrl(options.sidecarUrl); diff --git a/packages/node/test/context-lines.test.ts b/packages/node/test/integrations/contextlines.test.ts similarity index 88% rename from packages/node/test/context-lines.test.ts rename to packages/node/test/integrations/contextlines.test.ts index c65b8db295d5..dda78689e711 100644 --- a/packages/node/test/context-lines.test.ts +++ b/packages/node/test/integrations/contextlines.test.ts @@ -1,17 +1,17 @@ import * as fs from 'fs'; -import type { StackFrame } from '@sentry/types'; +import type { Event, Integration, StackFrame } from '@sentry/types'; import { parseStackFrames } from '@sentry/utils'; -import { ContextLines, resetFileContentCache } from '../src/integrations/contextlines'; -import { defaultStackParser } from '../src/sdk'; -import { getError } from './helper/error'; +import { ContextLines, resetFileContentCache } from '../../src/integrations/contextlines'; +import { defaultStackParser } from '../../src/sdk'; +import { getError } from '../helper/error'; describe('ContextLines', () => { let readFileSpy: jest.SpyInstance; - let contextLines: ContextLines; + let contextLines: Integration & { processEvent: (event: Event) => Promise }; async function addContext(frames: StackFrame[]): Promise { - await contextLines.addSourceContext({ exception: { values: [{ stacktrace: { frames } }] } }); + await contextLines.processEvent({ exception: { values: [{ stacktrace: { frames } }] } }); } beforeEach(() => {