Skip to content

ref(node): Refactor node integrations to functional syntax #9959

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jan 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions packages/core/src/integration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,7 @@ function findIndex<T>(arr: T[], callback: (item: T) => boolean): number {
export function convertIntegrationFnToClass<Fn extends IntegrationFn>(
name: string,
fn: Fn,
): {
): Integration & {
id: string;
new (...args: Parameters<Fn>): Integration &
ReturnType<Fn> & {
Expand All @@ -182,7 +182,7 @@ export function convertIntegrationFnToClass<Fn extends IntegrationFn>(
};
},
{ id: name },
) as unknown as {
) as unknown as Integration & {
id: string;
new (...args: Parameters<Fn>): Integration &
ReturnType<Fn> & {
Expand Down
19 changes: 18 additions & 1 deletion packages/node/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,14 +86,31 @@ 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';
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<Integration>;
Http: typeof NodeIntegrations.Http;
OnUncaughtException: IntegrationClass<Integration>;
OnUnhandledRejection: IntegrationClass<Integration>;
Modules: IntegrationClass<Integration>;
ContextLines: IntegrationClass<Integration>;
Context: IntegrationClass<Integration>;
RequestData: IntegrationClass<Integration>;
LocalVariables: IntegrationClass<Integration>;
Undici: typeof NodeIntegrations.Undici;
Spotlight: IntegrationClass<Integration>;
Anr: IntegrationClass<Integration>;
Hapi: IntegrationClass<Integration>;
}),
...TracingIntegrations,
};

Expand Down
176 changes: 87 additions & 89 deletions packages/node/src/integrations/anr/index.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -50,108 +50,106 @@ interface InspectorApi {
url: () => string | undefined;
}

const INTEGRATION_NAME = 'Anr';

const anrIntegration = ((options: Partial<Options> = {}) => {
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<Options> = {}) {}
/**
* Starts the ANR worker thread
*/
async function _startWorker(client: NodeClient, _options: Partial<Options>): Promise<void> {
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<void> {
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);
});
}
68 changes: 29 additions & 39 deletions packages/node/src/integrations/console.ts
Original file line number Diff line number Diff line change
@@ -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);
Loading