Skip to content

feat(node-experimental): Move integrations from node #10743

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 2 commits into from
Feb 21, 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
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ Sentry.init({
environment: 'qa', // dynamic sampling bias to keep transactions
dsn: process.env.E2E_TEST_DSN,
includeLocalVariables: true,
integrations: [new Sentry.Integrations.Hapi({ server })],
integrations: [Sentry.hapiIntegration({ server })],
debug: true,
tunnel: `http://localhost:3031/`, // proxy server
tracesSampleRate: 1,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ const init = async () => {
},
});

await Sentry.setupHapiErrorHandler(server);
await server.start();

sendPortToRunner(port);
Expand Down
1 change: 0 additions & 1 deletion packages/node-experimental/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,6 @@
"@opentelemetry/semantic-conventions": "1.21.0",
"@prisma/instrumentation": "5.9.0",
"@sentry/core": "7.100.0",
"@sentry/node": "7.100.0",
"@sentry/opentelemetry": "7.100.0",
"@sentry/types": "7.100.0",
"@sentry/utils": "7.100.0",
Expand Down
21 changes: 10 additions & 11 deletions packages/node-experimental/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,15 @@ export { errorHandler } from './sdk/handlers/errorHandler';

export { httpIntegration } from './integrations/http';
export { nativeNodeFetchIntegration } from './integrations/node-fetch';

export { consoleIntegration } from './integrations/console';
export { nodeContextIntegration } from './integrations/context';
export { contextLinesIntegration } from './integrations/contextlines';
export { localVariablesIntegration } from './integrations/local-variables';
export { modulesIntegration } from './integrations/modules';
export { onUncaughtExceptionIntegration } from './integrations/onuncaughtexception';
export { onUnhandledRejectionIntegration } from './integrations/onunhandledrejection';

export { expressIntegration } from './integrations/tracing/express';
export { fastifyIntegration } from './integrations/tracing/fastify';
export { graphqlIntegration } from './integrations/tracing/graphql';
Expand All @@ -12,6 +21,7 @@ export { mysql2Integration } from './integrations/tracing/mysql2';
export { nestIntegration } from './integrations/tracing/nest';
export { postgresIntegration } from './integrations/tracing/postgres';
export { prismaIntegration } from './integrations/tracing/prisma';
export { hapiIntegration, setupHapiErrorHandler } from './integrations/tracing/hapi';

export { init, getDefaultIntegrations } from './sdk/init';
export { getAutoPerformanceIntegrations } from './integrations/tracing';
Expand All @@ -28,17 +38,6 @@ export { startSpan, startSpanManual, startInactiveSpan, getActiveSpan, withActiv

export { addRequestDataToEvent, DEFAULT_USER_INCLUDES, extractRequestData } from '@sentry/utils';

export {
hapiErrorPlugin,
consoleIntegration,
onUncaughtExceptionIntegration,
onUnhandledRejectionIntegration,
modulesIntegration,
contextLinesIntegration,
nodeContextIntegration,
localVariablesIntegration,
} from '@sentry/node';

export {
addBreadcrumb,
isInitialized,
Expand Down
44 changes: 44 additions & 0 deletions packages/node-experimental/src/integrations/anr/common.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import type { Contexts, DsnComponents, Primitive, SdkMetadata } from '@sentry/types';

export interface AnrIntegrationOptions {
/**
* Interval to send heartbeat messages to the ANR worker.
*
* Defaults to 50ms.
*/
pollInterval: number;
/**
* Threshold in milliseconds to trigger an ANR event.
*
* Defaults to 5000ms.
*/
anrThreshold: number;
/**
* Whether to capture a stack trace when the ANR event is triggered.
*
* Defaults to `false`.
*
* This uses the node debugger which enables the inspector API and opens the required ports.
*/
captureStackTrace: boolean;
/**
* Tags to include with ANR events.
*/
staticTags: { [key: string]: Primitive };
/**
* @ignore Internal use only.
*
* If this is supplied, stack frame filenames will be rewritten to be relative to this path.
*/
appRootPath: string | undefined;
}

export interface WorkerStartData extends AnrIntegrationOptions {
debug: boolean;
sdkMetadata: SdkMetadata;
dsn: DsnComponents;
release: string | undefined;
environment: string;
dist: string | undefined;
contexts: Contexts;
}
160 changes: 160 additions & 0 deletions packages/node-experimental/src/integrations/anr/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
import { URL } from 'url';
import { defineIntegration, 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 { NODE_MAJOR, NODE_VERSION } from '../../nodeVersion';
import type { NodeClient } from '../../sdk/client';
import type { AnrIntegrationOptions, WorkerStartData } from './common';
import { base64WorkerScript } from './worker-script';

const DEFAULT_INTERVAL = 50;
const DEFAULT_HANG_THRESHOLD = 5000;

type WorkerNodeV14 = Worker & { new (filename: string | URL, options?: WorkerOptions): Worker };

type WorkerThreads = {
Worker: WorkerNodeV14;
};

function log(message: string, ...args: unknown[]): void {
logger.log(`[ANR] ${message}`, ...args);
}

/**
* We need to use dynamicRequire because worker_threads is not available in node < v12 and webpack error will when
* targeting those versions
*/
function getWorkerThreads(): WorkerThreads {
return dynamicRequire(module, 'worker_threads');
}

/**
* Gets contexts by calling all event processors. This relies on being called after all integrations are setup
*/
async function getContexts(client: NodeClient): Promise<Contexts> {
let event: Event | null = { message: 'ANR' };
const eventHint: EventHint = {};

for (const processor of client.getEventProcessors()) {
if (event === null) break;
event = await processor(event, eventHint);
}

return event?.contexts || {};
}

interface InspectorApi {
open: (port: number) => void;
url: () => string | undefined;
}

const INTEGRATION_NAME = 'Anr';

const _anrIntegration = ((options: Partial<AnrIntegrationOptions> = {}) => {
return {
name: INTEGRATION_NAME,
setup(client: NodeClient) {
if (NODE_MAJOR < 16 || (NODE_MAJOR === 16 && (NODE_VERSION.minor || 0) < 17)) {
throw new Error('ANR detection requires Node 16.17.0 or later');
}

// setImmediate is used to ensure that all other integrations have been setup
setImmediate(() => _startWorker(client, options));
},
};
}) satisfies IntegrationFn;

export const anrIntegration = defineIntegration(_anrIntegration);

/**
* Starts the ANR worker thread
*/
async function _startWorker(client: NodeClient, _options: Partial<AnrIntegrationOptions>): Promise<void> {
const contexts = await getContexts(client);
const dsn = client.getDsn();

if (!dsn) {
return;
}

// 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 options: WorkerStartData = {
debug: logger.isEnabled(),
dsn,
environment: initOptions.environment || 'production',
release: initOptions.release,
dist: initOptions.dist,
sdkMetadata,
appRootPath: _options.appRootPath,
pollInterval: _options.pollInterval || DEFAULT_INTERVAL,
anrThreshold: _options.anrThreshold || DEFAULT_HANG_THRESHOLD,
captureStackTrace: !!_options.captureStackTrace,
staticTags: _options.staticTags || {},
contexts,
};

if (options.captureStackTrace) {
// eslint-disable-next-line @typescript-eslint/no-var-requires
const inspector: InspectorApi = require('inspector');
if (!inspector.url()) {
inspector.open(0);
}
}

const { Worker } = getWorkerThreads();

const worker = new Worker(new URL(`data:application/javascript;base64,${base64WorkerScript}`), {
workerData: options,
});

process.on('exit', () => {
// eslint-disable-next-line @typescript-eslint/no-floating-promises
worker.terminate();
});

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);
// Timer should not block exit
timer.unref();

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);
});

// Ensure this thread can't block app exit
worker.unref();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
// This file is a placeholder that gets overwritten in the build directory.
export const base64WorkerScript = '';
Loading