diff --git a/packages/nextjs/test/integration/package.json b/packages/nextjs/test/integration/package.json index ea62b01992af..8ac72bc27f13 100644 --- a/packages/nextjs/test/integration/package.json +++ b/packages/nextjs/test/integration/package.json @@ -27,7 +27,7 @@ "resolutions": { "@sentry/browser": "file:../../../browser", "@sentry/core": "file:../../../core", - "@sentry/node": "file:../../../node-experimental", + "@sentry/node": "file:../../../node", "@sentry/opentelemetry": "file:../../../opentelemetry", "@sentry/react": "file:../../../react", "@sentry-internal/replay": "file:../../../replay-internal", diff --git a/packages/node-experimental/LICENSE b/packages/node-experimental/LICENSE index d11896ba1181..535ef0561e1b 100644 --- a/packages/node-experimental/LICENSE +++ b/packages/node-experimental/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2023 Sentry (https://sentry.io) and individual contributors. All rights reserved. +Copyright (c) 2019 Sentry (https://sentry.io) and individual contributors. All rights reserved. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the diff --git a/packages/node-experimental/README.md b/packages/node-experimental/README.md index 324700199b53..af6819c9a4ce 100644 --- a/packages/node-experimental/README.md +++ b/packages/node-experimental/README.md @@ -4,28 +4,36 @@

-# Official Sentry SDK for Node +# Legacy Sentry SDK for NodeJS -[![npm version](https://img.shields.io/npm/v/@sentry/node.svg)](https://www.npmjs.com/package/@sentry/node) -[![npm dm](https://img.shields.io/npm/dm/@sentry/node.svg)](https://www.npmjs.com/package/@sentry/node) -[![npm dt](https://img.shields.io/npm/dt/@sentry/node.svg)](https://www.npmjs.com/package/@sentry/node) +[![npm version](https://img.shields.io/npm/v/@sentry/node-experimental.svg)](https://www.npmjs.com/package/@sentry/node-experimental) +[![npm dm](https://img.shields.io/npm/dm/@sentry/node-experimental.svg)](https://www.npmjs.com/package/@sentry/node-experimental) +[![npm dt](https://img.shields.io/npm/dt/@sentry/node-experimental.svg)](https://www.npmjs.com/package/@sentry/node-experimental) -## Installation +## Links -```bash -npm install @sentry/node +- [Official SDK Docs](https://docs.sentry.io/quickstart/) +- [TypeDoc](http://getsentry.github.io/sentry-javascript/) -# Or yarn -yarn add @sentry/node -``` +## Status + +Since v8, this is the _legacy_ SDK, and it will most likely be completely removed before v8 is fully stable. It only +exists so that Meta-SDKs like `@sentry/nextjs` or `@sentry/sveltekit` can be migrated to the new `@sentry/node` +step-by-step. + +You should instead use [@sentry/node](./../node-experimental/). ## Usage -```js -// CJS Syntax -const Sentry = require('@sentry/node'); -// ESM Syntax -import * as Sentry from '@sentry/node'; +To use this SDK, call `init(options)` as early as possible in the main entry module. This will initialize the SDK and +hook into the environment. Note that you can turn off almost all side effects using the respective options. Minimum +supported Node version is Node 14. + +```javascript +// CJS syntax +const Sentry = require('@sentry/node-experimental'); +// ESM syntax +import * as Sentry from '@sentry/node-experimental'; Sentry.init({ dsn: '__DSN__', @@ -33,27 +41,28 @@ Sentry.init({ }); ``` -Note that it is necessary to initialize Sentry **before you import any package that may be instrumented by us**. - -[More information on how to set up Sentry for Node in v8.](./../../docs/v8-node.md) +To set context information or send manual events, use the exported functions of `@sentry/node-experimental`. Note that +these functions will not perform any action before you have called `init()`: -### ESM Support +```javascript +// Set user information, as well as tags and further extras +Sentry.setExtra('battery', 0.7); +Sentry.setTag('user_mode', 'admin'); +Sentry.setUser({ id: '4711' }); -Due to the way OpenTelemetry handles instrumentation, this only works out of the box for CommonJS (`require`) -applications. - -There is experimental support for running OpenTelemetry with ESM (`"type": "module"`): +// Add a breadcrumb for future events +Sentry.addBreadcrumb({ + message: 'My Breadcrumb', + // ... +}); -```bash -node --experimental-loader=@opentelemetry/instrumentation/hook.mjs ./app.js +// Capture exceptions, messages or manual events +Sentry.captureMessage('Hello, world!'); +Sentry.captureException(new Error('Good bye')); +Sentry.captureEvent({ + message: 'Manual', + stacktrace: [ + // ... + ], +}); ``` - -You'll need to install `@opentelemetry/instrumentation` in your app to ensure this works. - -See -[OpenTelemetry Instrumentation Docs](https://github.com/open-telemetry/opentelemetry-js/tree/main/experimental/packages/opentelemetry-instrumentation#instrumentation-for-es-modules-in-nodejs-experimental) -for details on this - but note that this is a) experimental, and b) does not work with all integrations. - -## Links - -- [Official SDK Docs](https://docs.sentry.io/quickstart/) diff --git a/packages/node-experimental/package.json b/packages/node-experimental/package.json index 042ca4dd1460..882c4bc52308 100644 --- a/packages/node-experimental/package.json +++ b/packages/node-experimental/package.json @@ -1,7 +1,7 @@ { - "name": "@sentry/node", + "name": "@sentry/node-experimental", "version": "8.0.0-alpha.5", - "description": "Sentry Node SDK using OpenTelemetry for performance instrumentation", + "description": "The old version of Sentry SDK for Node.js, without OpenTelemetry support.", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/node-experimental", "author": "Sentry", @@ -42,39 +42,22 @@ "access": "public" }, "dependencies": { - "@opentelemetry/api": "1.7.0", - "@opentelemetry/context-async-hooks": "1.21.0", - "@opentelemetry/core": "1.21.0", - "@opentelemetry/instrumentation": "0.48.0", - "@opentelemetry/instrumentation-express": "0.35.0", - "@opentelemetry/instrumentation-fastify": "0.33.0", - "@opentelemetry/instrumentation-graphql": "0.37.0", - "@opentelemetry/instrumentation-hapi": "0.34.0", - "@opentelemetry/instrumentation-http": "0.48.0", - "@opentelemetry/instrumentation-koa": "0.37.0", - "@opentelemetry/instrumentation-mongodb": "0.39.0", - "@opentelemetry/instrumentation-mongoose": "0.35.0", - "@opentelemetry/instrumentation-mysql": "0.35.0", - "@opentelemetry/instrumentation-mysql2": "0.35.0", - "@opentelemetry/instrumentation-nestjs-core": "0.34.0", - "@opentelemetry/instrumentation-pg": "0.38.0", - "@opentelemetry/resources": "1.21.0", - "@opentelemetry/sdk-trace-base": "1.21.0", - "@opentelemetry/semantic-conventions": "1.21.0", - "@prisma/instrumentation": "5.9.0", + "@sentry-internal/tracing": "8.0.0-alpha.5", "@sentry/core": "8.0.0-alpha.5", - "@sentry/opentelemetry": "8.0.0-alpha.5", "@sentry/types": "8.0.0-alpha.5", "@sentry/utils": "8.0.0-alpha.5" }, "devDependencies": { - "@types/node": "^14.18.0" - }, - "optionalDependencies": { - "opentelemetry-instrumentation-fetch-node": "1.1.2" + "@types/cookie": "0.5.2", + "@types/express": "^4.17.14", + "@types/lru-cache": "^5.1.0", + "@types/node": "14.18.63", + "express": "^4.17.1", + "nock": "^13.0.5", + "undici": "^5.21.0" }, "scripts": { - "build": "run-p build:transpile build:types", + "build": "run-s build:transpile build:types", "build:dev": "yarn build", "build:transpile": "rollup -c rollup.npm.config.mjs", "build:types": "run-s build:types:core build:types:downlevel", @@ -89,13 +72,23 @@ "clean": "rimraf build coverage sentry-node-experimental-*.tgz", "fix": "eslint . --format stylish --fix", "lint": "eslint . --format stylish", - "test": "yarn test:jest", + "test": "run-s test:jest test:express test:webpack test:release-health", + "test:express": "node test/manual/express-scope-separation/start.js", "test:jest": "jest", + "test:release-health": "node test/manual/release-health/runner.js", + "test:webpack": "cd test/manual/webpack-async-context/ && yarn --silent --ignore-engines && node npm-build.js", "test:watch": "jest --watch", "yalc:publish": "ts-node ../../scripts/prepack.ts && yalc publish build --push --sig" }, "volta": { "extends": "../../package.json" }, + "madge": { + "detectiveOptions": { + "ts": { + "skipTypeImports": true + } + } + }, "sideEffects": false } diff --git a/packages/node-experimental/rollup.anr-worker.config.mjs b/packages/node-experimental/rollup.anr-worker.config.mjs index bd3c1d4b825c..48463d5763ee 100644 --- a/packages/node-experimental/rollup.anr-worker.config.mjs +++ b/packages/node-experimental/rollup.anr-worker.config.mjs @@ -1,31 +1,34 @@ import { makeBaseBundleConfig } from '@sentry-internal/rollup-utils'; -export function createAnrWorkerCode() { - let base64Code; - - return { - workerRollupConfig: makeBaseBundleConfig({ - bundleType: 'node-worker', - entrypoints: ['src/integrations/anr/worker.ts'], - licenseTitle: '@sentry/node', - outputFileBase: () => 'worker-script.js', - packageSpecificConfig: { - output: { - dir: 'build/esm/integrations/anr', - sourcemap: false, - }, - plugins: [ - { - name: 'output-base64-worker-script', - renderChunk(code) { - base64Code = Buffer.from(code).toString('base64'); - }, - }, - ], +function createAnrWorkerConfig(destDir, esm) { + return makeBaseBundleConfig({ + bundleType: 'node-worker', + entrypoints: ['src/integrations/anr/worker.ts'], + licenseTitle: '@sentry/node', + outputFileBase: () => 'worker-script.js', + packageSpecificConfig: { + output: { + dir: destDir, + sourcemap: false, }, - }), - getBase64Code() { - return base64Code; + plugins: [ + { + name: 'output-base64-worker-script', + renderChunk(code) { + const base64Code = Buffer.from(code).toString('base64'); + if (esm) { + return `export const base64WorkerScript = '${base64Code}';`; + } else { + return `exports.base64WorkerScript = '${base64Code}';`; + } + }, + }, + ], }, - }; + }); } + +export const anrWorkerConfigs = [ + createAnrWorkerConfig('build/esm/integrations/anr', true), + createAnrWorkerConfig('build/cjs/integrations/anr', false), +]; diff --git a/packages/node-experimental/rollup.npm.config.mjs b/packages/node-experimental/rollup.npm.config.mjs index 17c0727d7eff..88c90de4825f 100644 --- a/packages/node-experimental/rollup.npm.config.mjs +++ b/packages/node-experimental/rollup.npm.config.mjs @@ -1,32 +1,8 @@ -import replace from '@rollup/plugin-replace'; import { makeBaseNPMConfig, makeNPMConfigVariants } from '@sentry-internal/rollup-utils'; -import { createAnrWorkerCode } from './rollup.anr-worker.config.mjs'; - -const { workerRollupConfig, getBase64Code } = createAnrWorkerCode(); +import { anrWorkerConfigs } from './rollup.anr-worker.config.mjs'; export default [ - // The worker needs to be built first since it's output is used in the main bundle. - workerRollupConfig, - ...makeNPMConfigVariants( - makeBaseNPMConfig({ - packageSpecificConfig: { - output: { - // set exports to 'named' or 'auto' so that rollup doesn't warn - exports: 'named', - // set preserveModules to false because we want to bundle everything into one file. - preserveModules: false, - }, - plugins: [ - replace({ - delimiters: ['###', '###'], - // removes some webpack warnings - preventAssignment: true, - values: { - base64WorkerScript: () => getBase64Code(), - }, - }), - ], - }, - }), - ), + ...makeNPMConfigVariants(makeBaseNPMConfig()), + // The ANR worker builds must come after the main build because they overwrite the worker-script.js file + ...anrWorkerConfigs, ]; diff --git a/packages/node/src/_setSpanForScope.ts b/packages/node-experimental/src/_setSpanForScope.ts similarity index 100% rename from packages/node/src/_setSpanForScope.ts rename to packages/node-experimental/src/_setSpanForScope.ts diff --git a/packages/node/src/async/domain.ts b/packages/node-experimental/src/async/domain.ts similarity index 100% rename from packages/node/src/async/domain.ts rename to packages/node-experimental/src/async/domain.ts diff --git a/packages/node/src/async/hooks.ts b/packages/node-experimental/src/async/hooks.ts similarity index 100% rename from packages/node/src/async/hooks.ts rename to packages/node-experimental/src/async/hooks.ts diff --git a/packages/node/src/async/index.ts b/packages/node-experimental/src/async/index.ts similarity index 100% rename from packages/node/src/async/index.ts rename to packages/node-experimental/src/async/index.ts diff --git a/packages/node/src/client.ts b/packages/node-experimental/src/client.ts similarity index 100% rename from packages/node/src/client.ts rename to packages/node-experimental/src/client.ts diff --git a/packages/node/src/handlers.ts b/packages/node-experimental/src/handlers.ts similarity index 100% rename from packages/node/src/handlers.ts rename to packages/node-experimental/src/handlers.ts diff --git a/packages/node-experimental/src/index.ts b/packages/node-experimental/src/index.ts index 33bbcca54213..36b27086ef08 100644 --- a/packages/node-experimental/src/index.ts +++ b/packages/node-experimental/src/index.ts @@ -1,127 +1,150 @@ -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 { anrIntegration } from './integrations/anr'; - -export { expressIntegration, expressErrorHandler, setupExpressErrorHandler } from './integrations/tracing/express'; -export { fastifyIntegration, setupFastifyErrorHandler } from './integrations/tracing/fastify'; -export { graphqlIntegration } from './integrations/tracing/graphql'; -export { mongoIntegration } from './integrations/tracing/mongo'; -export { mongooseIntegration } from './integrations/tracing/mongoose'; -export { mysqlIntegration } from './integrations/tracing/mysql'; -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 { spotlightIntegration } from './integrations/spotlight'; - -export { init, getDefaultIntegrations } from './sdk/init'; -export { initOpenTelemetry } from './sdk/initOtel'; -export { getAutoPerformanceIntegrations } from './integrations/tracing'; -export { getSentryRelease, defaultStackParser } from './sdk/api'; -export { createGetModuleFromFilename } from './utils/module'; -export { makeNodeTransport } from './transports'; -export { NodeClient } from './sdk/client'; -export { cron } from './cron'; +export type { + Breadcrumb, + BreadcrumbHint, + PolymorphicRequest, + Request, + SdkInfo, + Event, + EventHint, + Exception, + Session, + SeverityLevel, + Span, + StackFrame, + Stacktrace, + Thread, + Transaction, + User, +} from '@sentry/types'; +export type { AddRequestDataToEventOptions, TransactionNamingScheme } from '@sentry/utils'; export type { NodeOptions } from './types'; export { - addRequestDataToEvent, - DEFAULT_USER_INCLUDES, - extractRequestData, -} from '@sentry/utils'; - -// These are custom variants that need to be used instead of the core one -// As they have slightly different implementations -export { continueTrace } from '@sentry/opentelemetry'; - -export { + addEventProcessor, addBreadcrumb, - isInitialized, - getGlobalScope, + addIntegration, + captureException, + captureEvent, + captureMessage, close, createTransport, flush, + // eslint-disable-next-line deprecation/deprecation + getCurrentHub, + getClient, + isInitialized, + getCurrentScope, + getGlobalScope, + getIsolationScope, Hub, + setCurrentClient, + Scope, SDK_VERSION, + setContext, + setExtra, + setExtras, + setTag, + setTags, + setUser, getSpanStatusFromHttpCode, setHttpStatus, + withScope, + withIsolationScope, captureCheckIn, withMonitor, - requestDataIntegration, + setMeasurement, + getActiveSpan, + getRootSpan, + startSpan, + startInactiveSpan, + startSpanManual, + withActiveSpan, + getSpanDescendants, + continueTrace, + parameterize, functionToStringIntegration, inboundFiltersIntegration, linkedErrorsIntegration, - addEventProcessor, - setContext, - setExtra, - setExtras, - setTag, - setTags, - setUser, + requestDataIntegration, + metricsDefault as metrics, + startSession, + captureSession, + endSession, +} from '@sentry/core'; + +export { SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE, - setCurrentClient, - Scope, - setMeasurement, - getSpanDescendants, - parameterize, - getClient, - // eslint-disable-next-line deprecation/deprecation - getCurrentHub, - getCurrentScope, - getIsolationScope, - withScope, - withIsolationScope, - captureException, - captureEvent, - captureMessage, +} from '@sentry/core'; + +export { autoDiscoverNodePerformanceMonitoringIntegrations } from './tracing'; + +export { NodeClient } from './client'; +export { makeNodeTransport } from './transports'; +export { + getDefaultIntegrations, + init, + defaultStackParser, + getSentryRelease, +} from './sdk'; +export { addRequestDataToEvent, DEFAULT_USER_INCLUDES, extractRequestData } from '@sentry/utils'; + +export { createGetModuleFromFilename } from './module'; + +import * as Handlers from './handlers'; +import * as NodeIntegrations from './integrations'; +import * as TracingIntegrations from './tracing/integrations'; + +// TODO: Deprecate this once we migrated tracing integrations +export const Integrations = { + ...NodeIntegrations, + ...TracingIntegrations, +}; + +export { captureConsoleIntegration, debugIntegration, dedupeIntegration, extraErrorDataIntegration, rewriteFramesIntegration, sessionTimingIntegration, - metricsDefault as metrics, - startSession, - captureSession, - endSession, - addIntegration, - startSpan, - startSpanManual, - startInactiveSpan, - getActiveSpan, - withActiveSpan, - getRootSpan, - spanToJSON, } from '@sentry/core'; -export type { - Breadcrumb, - BreadcrumbHint, - PolymorphicRequest, - Request, - SdkInfo, - Event, - EventHint, - Exception, - Session, - SeverityLevel, - StackFrame, - Stacktrace, - Thread, - Transaction, - User, - Span, -} from '@sentry/types'; +export { consoleIntegration } from './integrations/console'; +export { onUncaughtExceptionIntegration } from './integrations/onuncaughtexception'; +export { onUnhandledRejectionIntegration } from './integrations/onunhandledrejection'; +export { modulesIntegration } from './integrations/modules'; +export { contextLinesIntegration } from './integrations/contextlines'; +export { nodeContextIntegration } from './integrations/context'; +export { localVariablesIntegration } from './integrations/local-variables'; +export { spotlightIntegration } from './integrations/spotlight'; +export { anrIntegration } from './integrations/anr'; +export { hapiIntegration } from './integrations/hapi'; +// eslint-disable-next-line deprecation/deprecation +export { Undici, nativeNodeFetchintegration } from './integrations/undici'; +// eslint-disable-next-line deprecation/deprecation +export { Http, httpIntegration } from './integrations/http'; + +// TODO(v8): Remove all of these exports. They were part of a hotfix #10339 where we produced wrong .d.ts files because we were packing packages inside the /build folder. +export type { LocalVariablesIntegrationOptions } from './integrations/local-variables/common'; +export type { DebugSession } from './integrations/local-variables/local-variables-sync'; +export type { AnrIntegrationOptions } from './integrations/anr/common'; +// --- + +export { Handlers }; + +export { hapiErrorPlugin } from './integrations/hapi'; + +import { instrumentCron } from './cron/cron'; +import { instrumentNodeCron } from './cron/node-cron'; +import { instrumentNodeSchedule } from './cron/node-schedule'; + +/** Methods to instrument cron libraries for Sentry check-ins */ +export const cron = { + instrumentCron, + instrumentNodeCron, + instrumentNodeSchedule, +}; diff --git a/packages/node-experimental/src/integrations/anr/common.ts b/packages/node-experimental/src/integrations/anr/common.ts index e2e50fae4179..5617871ccb24 100644 --- a/packages/node-experimental/src/integrations/anr/common.ts +++ b/packages/node-experimental/src/integrations/anr/common.ts @@ -37,7 +37,6 @@ export interface WorkerStartData extends AnrIntegrationOptions { debug: boolean; sdkMetadata: SdkMetadata; dsn: DsnComponents; - tunnel: string | undefined; release: string | undefined; environment: string; dist: string | undefined; diff --git a/packages/node-experimental/src/integrations/anr/index.ts b/packages/node-experimental/src/integrations/anr/index.ts index 7dbe9e905cb4..7e0de1d0badc 100644 --- a/packages/node-experimental/src/integrations/anr/index.ts +++ b/packages/node-experimental/src/integrations/anr/index.ts @@ -1,41 +1,37 @@ -import { defineIntegration, mergeScopeData } from '@sentry/core'; -import type { Contexts, Event, EventHint, Integration, IntegrationFn, ScopeData } from '@sentry/types'; -import { GLOBAL_OBJ, logger } from '@sentry/utils'; -import * as inspector from 'inspector'; -import { Worker } from 'worker_threads'; -import { getCurrentScope, getGlobalScope, getIsolationScope } from '../..'; +// TODO (v8): This import can be removed once we only support Node with global URL +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 type { NodeClient } from '../../client'; import { 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); } -function globalWithScopeFetchFn(): typeof GLOBAL_OBJ & { __SENTRY_GET_SCOPES__?: () => ScopeData } { - return GLOBAL_OBJ; -} - -/** Fetches merged scope data */ -function getScopeData(): ScopeData { - const scope = getGlobalScope().getScopeData(); - mergeScopeData(scope, getIsolationScope().getScopeData()); - mergeScopeData(scope, getCurrentScope().getScopeData()); - - // We remove attachments because they likely won't serialize well as json - scope.attachments = []; - // We can't serialize event processor functions - scope.eventProcessors = []; - - return scope; +/** + * 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 shouldn't be called until all integrations are setup + * Gets contexts by calling all event processors. This relies on being called after all integrations are setup */ async function getContexts(client: NodeClient): Promise { let event: Event | null = { message: 'ANR' }; @@ -49,76 +45,40 @@ async function getContexts(client: NodeClient): Promise { return event?.contexts || {}; } -const INTEGRATION_NAME = 'Anr'; +interface InspectorApi { + open: (port: number) => void; + url: () => string | undefined; +} -type AnrInternal = { startWorker: () => void; stopWorker: () => void }; +const INTEGRATION_NAME = 'Anr'; const _anrIntegration = ((options: Partial = {}) => { - if (NODE_VERSION.major < 16 || (NODE_VERSION.major === 16 && NODE_VERSION.minor < 17)) { - throw new Error('ANR detection requires Node 16.17.0 or later'); - } - - let worker: Promise<() => void> | undefined; - let client: NodeClient | undefined; - - // Hookup the scope fetch function to the global object so that it can be called from the worker thread via the - // debugger when it pauses - const gbl = globalWithScopeFetchFn(); - gbl.__SENTRY_GET_SCOPES__ = getScopeData; - return { name: INTEGRATION_NAME, - startWorker: () => { - if (worker) { - return; - } - - if (client) { - worker = _startWorker(client, options); - } - }, - stopWorker: () => { - if (worker) { - // eslint-disable-next-line @typescript-eslint/no-floating-promises - worker.then(stop => { - stop(); - worker = undefined; - }); + setup(client: NodeClient) { + if (NODE_VERSION.major < 16 || (NODE_VERSION.major === 16 && NODE_VERSION.minor < 17)) { + throw new Error('ANR detection requires Node 16.17.0 or later'); } - }, - setup(initClient: NodeClient) { - client = initClient; - // setImmediate is used to ensure that all other integrations have had their setup called first. - // This allows us to call into all integrations to fetch the full context - setImmediate(() => this.startWorker()); + // setImmediate is used to ensure that all other integrations have been setup + setImmediate(() => _startWorker(client, options)); }, - } as Integration & AnrInternal; + }; }) satisfies IntegrationFn; -type AnrReturn = (options?: Partial) => Integration & AnrInternal; - -export const anrIntegration = defineIntegration(_anrIntegration) as AnrReturn; +export const anrIntegration = defineIntegration(_anrIntegration); /** * Starts the ANR worker thread - * - * @returns A function to stop the worker */ -async function _startWorker( - client: NodeClient, - integrationOptions: Partial, -): Promise<() => void> { +async function _startWorker(client: NodeClient, _options: Partial): Promise { + const contexts = await getContexts(client); const dsn = client.getDsn(); if (!dsn) { - return () => { - // - }; + return; } - const contexts = await getContexts(client); - // These will not be accurate if sent later from the worker thread delete contexts.app?.app_memory; delete contexts.device?.free_memory; @@ -133,25 +93,28 @@ async function _startWorker( const options: WorkerStartData = { debug: logger.isEnabled(), dsn, - tunnel: initOptions.tunnel, environment: initOptions.environment || 'production', release: initOptions.release, dist: initOptions.dist, sdkMetadata, - appRootPath: integrationOptions.appRootPath, - pollInterval: integrationOptions.pollInterval || DEFAULT_INTERVAL, - anrThreshold: integrationOptions.anrThreshold || DEFAULT_HANG_THRESHOLD, - captureStackTrace: !!integrationOptions.captureStackTrace, - staticTags: integrationOptions.staticTags || {}, + 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, }); @@ -195,10 +158,4 @@ async function _startWorker( // Ensure this thread can't block app exit worker.unref(); - - return () => { - // eslint-disable-next-line @typescript-eslint/no-floating-promises - worker.terminate(); - clearInterval(timer); - }; } diff --git a/packages/node-experimental/src/integrations/anr/worker-script.ts b/packages/node-experimental/src/integrations/anr/worker-script.ts index c70323e0fc50..16394eaacfe1 100644 --- a/packages/node-experimental/src/integrations/anr/worker-script.ts +++ b/packages/node-experimental/src/integrations/anr/worker-script.ts @@ -1,2 +1,2 @@ -// This string is a placeholder that gets overwritten with the worker code. -export const base64WorkerScript = '###base64WorkerScript###'; +// This file is a placeholder that gets overwritten in the build directory. +export const base64WorkerScript = ''; diff --git a/packages/node-experimental/src/integrations/anr/worker.ts b/packages/node-experimental/src/integrations/anr/worker.ts index 21bdcbbb0631..a8b984b48379 100644 --- a/packages/node-experimental/src/integrations/anr/worker.ts +++ b/packages/node-experimental/src/integrations/anr/worker.ts @@ -1,12 +1,11 @@ import { - applyScopeDataToEvent, createEventEnvelope, createSessionEnvelope, getEnvelopeEndpointWithUrlEncodedAuth, makeSession, updateSession, } from '@sentry/core'; -import type { Event, ScopeData, Session, StackFrame } from '@sentry/types'; +import type { Event, Session, StackFrame, TraceContext } from '@sentry/types'; import { callFrameToStackFrame, normalizeUrlToBase, @@ -17,11 +16,12 @@ import { import { Session as InspectorSession } from 'inspector'; import { parentPort, workerData } from 'worker_threads'; +import { createGetModuleFromFilename } from '../../module'; import { makeNodeTransport } from '../../transports'; -import { createGetModuleFromFilename } from '../../utils/module'; import type { WorkerStartData } from './common'; type VoidFunction = () => void; +type InspectorSessionNodeV12 = InspectorSession & { connectToMainThread: VoidFunction }; const options: WorkerStartData = workerData; let session: Session | undefined; @@ -34,7 +34,7 @@ function log(msg: string): void { } } -const url = getEnvelopeEndpointWithUrlEncodedAuth(options.dsn, options.tunnel, options.sdkMetadata.sdk); +const url = getEnvelopeEndpointWithUrlEncodedAuth(options.dsn); const transport = makeNodeTransport({ url, recordDroppedEvent: () => { @@ -48,7 +48,7 @@ async function sendAbnormalSession(): Promise { log('Sending abnormal session'); updateSession(session, { status: 'abnormal', abnormal_mechanism: 'anr_foreground' }); - const envelope = createSessionEnvelope(session, options.dsn, options.sdkMetadata, options.tunnel); + const envelope = createSessionEnvelope(session, options.dsn, options.sdkMetadata); // Log the envelope so to aid in testing log(JSON.stringify(envelope)); @@ -87,23 +87,7 @@ function prepareStackFrames(stackFrames: StackFrame[] | undefined): StackFrame[] return strippedFrames; } -function applyScopeToEvent(event: Event, scope: ScopeData): void { - applyScopeDataToEvent(event, scope); - - if (!event.contexts?.trace) { - const { traceId, spanId, parentSpanId } = scope.propagationContext; - event.contexts = { - trace: { - trace_id: traceId, - span_id: spanId, - parent_span_id: parentSpanId, - }, - ...event.contexts, - }; - } -} - -async function sendAnrEvent(frames?: StackFrame[], scope?: ScopeData): Promise { +async function sendAnrEvent(frames?: StackFrame[], traceContext?: TraceContext): Promise { if (hasSentAnrEvent) { return; } @@ -116,7 +100,7 @@ async function sendAnrEvent(frames?: StackFrame[], scope?: ScopeData): Promise { process.exit(0); }, 5_000); @@ -159,7 +139,7 @@ let debuggerPause: VoidFunction | undefined; if (options.captureStackTrace) { log('Connecting to debugger'); - const session = new InspectorSession(); + const session = new InspectorSession() as InspectorSessionNodeV12; session.connectToMainThread(); log('Connected to debugger'); @@ -192,23 +172,20 @@ if (options.captureStackTrace) { 'Runtime.evaluate', { // Grab the trace context from the current scope - expression: 'global.__SENTRY_GET_SCOPES__();', + expression: + 'const ctx = __SENTRY__.acs?.getCurrentScope().getPropagationContext() || {}; ctx.traceId + "-" + ctx.spanId + "-" + ctx.parentSpanId', // Don't re-trigger the debugger if this causes an error silent: true, - // Serialize the result to json otherwise only primitives are supported - returnByValue: true, }, - (err, param) => { - if (err) { - log(`Error executing script: '${err.message}'`); - } - - const scopes = param && param.result ? (param.result.value as ScopeData) : undefined; + (_, param) => { + const traceId = param && param.result ? (param.result.value as string) : '--'; + const [trace_id, span_id, parent_span_id] = traceId.split('-') as (string | undefined)[]; session.post('Debugger.resume'); session.post('Debugger.disable'); - sendAnrEvent(stackFrames, scopes).then(null, () => { + const context = trace_id?.length && span_id?.length ? { trace_id, span_id, parent_span_id } : undefined; + sendAnrEvent(stackFrames, context).then(null, () => { log('Sending ANR event failed.'); }); }, diff --git a/packages/node-experimental/src/integrations/console.ts b/packages/node-experimental/src/integrations/console.ts index 0b3d27fe8510..5a185b8fcee1 100644 --- a/packages/node-experimental/src/integrations/console.ts +++ b/packages/node-experimental/src/integrations/console.ts @@ -1,6 +1,6 @@ import * as util from 'util'; -import { addBreadcrumb, defineIntegration, getClient } from '@sentry/core'; -import type { IntegrationFn } from '@sentry/types'; +import { addBreadcrumb, convertIntegrationFnToClass, defineIntegration, getClient } from '@sentry/core'; +import type { Client, Integration, IntegrationClass, IntegrationFn } from '@sentry/types'; import { addConsoleInstrumentationHandler, severityLevelFromString } from '@sentry/utils'; const INTEGRATION_NAME = 'Console'; @@ -30,7 +30,16 @@ const _consoleIntegration = (() => { }; }) satisfies IntegrationFn; +export const consoleIntegration = defineIntegration(_consoleIntegration); + /** - * Capture console logs as breadcrumbs. + * Console module integration. + * @deprecated Use `consoleIntegration()` instead. */ -export const consoleIntegration = defineIntegration(_consoleIntegration); +// eslint-disable-next-line deprecation/deprecation +export const Console = convertIntegrationFnToClass(INTEGRATION_NAME, consoleIntegration) as IntegrationClass< + Integration & { setup: (client: Client) => void } +>; + +// eslint-disable-next-line deprecation/deprecation +export type Console = typeof Console; diff --git a/packages/node-experimental/src/integrations/context.ts b/packages/node-experimental/src/integrations/context.ts index c33d97e79044..fa5184204bf2 100644 --- a/packages/node-experimental/src/integrations/context.ts +++ b/packages/node-experimental/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'; -import { defineIntegration } from '@sentry/core'; +import { convertIntegrationFnToClass, defineIntegration } from '@sentry/core'; import type { AppContext, CloudResourceContext, @@ -11,10 +12,13 @@ import type { CultureContext, DeviceContext, Event, + Integration, + IntegrationClass, IntegrationFn, OsContext, } from '@sentry/types'; +// TODO: Required until we drop support for Node v8 export const readFileAsync = promisify(readFile); export const readDirAsync = promisify(readdir); @@ -104,10 +108,27 @@ const _nodeContextIntegration = ((options: ContextOptions = {}) => { }; }) satisfies IntegrationFn; +export const nodeContextIntegration = defineIntegration(_nodeContextIntegration); + /** - * Capture context about the environment and the device that the client is running on, to events. + * Add node modules / packages to the event. + * @deprecated Use `nodeContextIntegration()` instead. */ -export const nodeContextIntegration = defineIntegration(_nodeContextIntegration); +// eslint-disable-next-line deprecation/deprecation +export const Context = convertIntegrationFnToClass(INTEGRATION_NAME, nodeContextIntegration) as IntegrationClass< + Integration & { processEvent: (event: Event) => Promise } +> & { + new (options?: { + app?: boolean; + os?: boolean; + device?: { cpu?: boolean; memory?: boolean } | boolean; + culture?: boolean; + cloudResource?: boolean; + }): Integration; +}; + +// eslint-disable-next-line deprecation/deprecation +export type Context = typeof Context; /** * Updates the context with dynamic values that can change diff --git a/packages/node-experimental/src/integrations/contextlines.ts b/packages/node-experimental/src/integrations/contextlines.ts index 3755e164e5ea..7c367f51a970 100644 --- a/packages/node-experimental/src/integrations/contextlines.ts +++ b/packages/node-experimental/src/integrations/contextlines.ts @@ -1,4 +1,4 @@ -import { promises } from 'fs'; +import { readFile } from 'fs'; import { defineIntegration } from '@sentry/core'; import type { Event, IntegrationFn, StackFrame } from '@sentry/types'; import { LRUMap, addContextToFrame } from '@sentry/utils'; @@ -7,7 +7,15 @@ const FILE_CONTENT_CACHE = new LRUMap(100); const DEFAULT_LINES_OF_CONTEXT = 7; const INTEGRATION_NAME = 'ContextLines'; -const readFileAsync = promises.readFile; +// TODO: Replace with promisify when minimum supported node >= v8 +function readTextFileAsync(path: string): Promise { + return new Promise((resolve, reject) => { + readFile(path, 'utf8', (err, data) => { + if (err) reject(err); + else resolve(data); + }); + }); +} /** * Resets the file cache. Exists for testing purposes. @@ -27,8 +35,7 @@ interface ContextLinesOptions { frameContextLines?: number; } -/** Exported only for tests, as a type-safe variant. */ -export const _contextLinesIntegration = ((options: ContextLinesOptions = {}) => { +const _contextLinesIntegration = ((options: ContextLinesOptions = {}) => { const contextLines = options.frameContextLines !== undefined ? options.frameContextLines : DEFAULT_LINES_OF_CONTEXT; return { @@ -39,9 +46,6 @@ export const _contextLinesIntegration = ((options: ContextLinesOptions = {}) => }; }) satisfies IntegrationFn; -/** - * Capture the lines before and after the frame's context. - */ export const contextLinesIntegration = defineIntegration(_contextLinesIntegration); async function addSourceContext(event: Event, contextLines: number): Promise { @@ -135,7 +139,7 @@ async function _readSourceFile(filename: string): Promise { // If we made it to here, it means that our file is not cache nor marked as failed, so attempt to read it let content: string[] | null = null; try { - const rawFileContents = await readFileAsync(filename, 'utf-8'); + const rawFileContents = await readTextFileAsync(filename); content = rawFileContents.split('\n'); } catch (_) { // if we fail, we will mark the file as null in the cache and short circuit next time we try to read it diff --git a/packages/node/src/integrations/hapi/index.ts b/packages/node-experimental/src/integrations/hapi/index.ts similarity index 100% rename from packages/node/src/integrations/hapi/index.ts rename to packages/node-experimental/src/integrations/hapi/index.ts diff --git a/packages/node/src/integrations/hapi/types.ts b/packages/node-experimental/src/integrations/hapi/types.ts similarity index 100% rename from packages/node/src/integrations/hapi/types.ts rename to packages/node-experimental/src/integrations/hapi/types.ts diff --git a/packages/node-experimental/src/integrations/http.ts b/packages/node-experimental/src/integrations/http.ts index ed93ebacdaa5..9eb7deab5318 100644 --- a/packages/node-experimental/src/integrations/http.ts +++ b/packages/node-experimental/src/integrations/http.ts @@ -1,155 +1,468 @@ -import type { ServerResponse } from 'http'; -import type { Span } from '@opentelemetry/api'; -import { SpanKind } from '@opentelemetry/api'; -import { registerInstrumentations } from '@opentelemetry/instrumentation'; -import { HttpInstrumentation } from '@opentelemetry/instrumentation-http'; - -import { addBreadcrumb, defineIntegration, getIsolationScope, isSentryRequestUrl } from '@sentry/core'; -import { _INTERNAL, getClient, getSpanKind } from '@sentry/opentelemetry'; -import type { IntegrationFn } from '@sentry/types'; - -import type { NodeClient } from '../sdk/client'; -import { setIsolationScope } from '../sdk/scope'; -import type { HTTPModuleRequestIncomingMessage } from '../transports/http-module'; -import { addOriginToSpan } from '../utils/addOriginToSpan'; -import { getRequestUrl } from '../utils/getRequestUrl'; +/* eslint-disable max-lines */ +import type * as http from 'http'; +import type * as https from 'https'; +import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, startInactiveSpan } from '@sentry/core'; +import { defineIntegration, getIsolationScope, hasTracingEnabled } from '@sentry/core'; +import { + addBreadcrumb, + getClient, + getCurrentScope, + getDynamicSamplingContextFromClient, + getDynamicSamplingContextFromSpan, + isSentryRequestUrl, + setHttpStatus, + spanToJSON, + spanToTraceHeader, +} from '@sentry/core'; +import type { + ClientOptions, + Integration, + IntegrationFn, + SanitizedRequestData, + TracePropagationTargets, +} from '@sentry/types'; +import { + LRUMap, + dropUndefinedKeys, + dynamicSamplingContextToSentryBaggageHeader, + fill, + generateSentryTraceHeader, + logger, + stringMatchesSomePattern, +} from '@sentry/utils'; + +import type { NodeClient } from '../client'; +import { DEBUG_BUILD } from '../debug-build'; +import { NODE_VERSION } from '../nodeVersion'; +import type { NodeClientOptions } from '../types'; +import type { RequestMethod, RequestMethodArgs, RequestOptions } from './utils/http'; +import { cleanSpanName, extractRawUrl, extractUrl, normalizeRequestArgs } from './utils/http'; + +interface TracingOptions { + /** + * List of strings/regex controlling to which outgoing requests + * the SDK will attach tracing headers. + * + * By default the SDK will attach those headers to all outgoing + * requests. If this option is provided, the SDK will match the + * request URL of outgoing requests against the items in this + * array, and only attach tracing headers if a match was found. + * + * @deprecated Use top level `tracePropagationTargets` option instead. + * This option will be removed in v8. + * + * ``` + * Sentry.init({ + * tracePropagationTargets: ['api.site.com'], + * }) + */ + tracePropagationTargets?: TracePropagationTargets; + + /** + * Function determining whether or not to create spans to track outgoing requests to the given URL. + * By default, spans will be created for all outgoing requests. + */ + shouldCreateSpanForRequest?: (url: string) => boolean; + + /** + * This option is just for compatibility with v7. + * In v8, this will be the default behavior. + */ + enableIfHasTracingEnabled?: boolean; +} interface HttpOptions { /** - * Whether breadcrumbs should be recorded for requests. + * Whether breadcrumbs should be recorded for requests * Defaults to true */ breadcrumbs?: boolean; /** - * Do not capture spans or breadcrumbs for outgoing HTTP requests to URLs where the given callback returns `true`. - * This controls both span & breadcrumb creation - spans will be non recording if tracing is disabled. + * Whether tracing spans should be created for requests + * Defaults to false */ - ignoreOutgoingRequests?: (url: string) => boolean; + tracing?: TracingOptions | boolean; +} +/* These are the newer options for `httpIntegration`. */ +interface HttpIntegrationOptions { /** - * Do not capture spans or breadcrumbs for incoming HTTP requests to URLs where the given callback returns `true`. - * This controls both span & breadcrumb creation - spans will be non recording if tracing is disabled. + * Whether breadcrumbs should be recorded for requests + * Defaults to true. */ - ignoreIncomingRequests?: (url: string) => boolean; + breadcrumbs?: boolean; + + /** + * Whether tracing spans should be created for requests + * If not set, this will be enabled/disabled based on if tracing is enabled. + */ + tracing?: boolean; + + /** + * Function determining whether or not to create spans to track outgoing requests to the given URL. + * By default, spans will be created for all outgoing requests. + */ + shouldCreateSpanForRequest?: (url: string) => boolean; } -const _httpIntegration = ((options: HttpOptions = {}) => { - const _breadcrumbs = typeof options.breadcrumbs === 'undefined' ? true : options.breadcrumbs; - const _ignoreOutgoingRequests = options.ignoreOutgoingRequests; - const _ignoreIncomingRequests = options.ignoreIncomingRequests; - - return { - name: 'Http', - setupOnce() { - const instrumentations = [ - new HttpInstrumentation({ - ignoreOutgoingRequestHook: request => { - const url = getRequestUrl(request); - - if (!url) { - return false; - } +const _httpIntegration = ((options: HttpIntegrationOptions = {}) => { + const { breadcrumbs, tracing, shouldCreateSpanForRequest } = options; - if (isSentryRequestUrl(url, getClient())) { - return true; - } + const convertedOptions: HttpOptions = { + breadcrumbs, + tracing: + tracing === false + ? false + : dropUndefinedKeys({ + // If tracing is forced to `true`, we don't want to set `enableIfHasTracingEnabled` + enableIfHasTracingEnabled: tracing === true ? undefined : true, + shouldCreateSpanForRequest, + }), + }; + // eslint-disable-next-line deprecation/deprecation + return new Http(convertedOptions) as unknown as Integration; +}) satisfies IntegrationFn; - if (_ignoreOutgoingRequests && _ignoreOutgoingRequests(url)) { - return true; - } +/** + * The http module integration instruments Node's internal http module. It creates breadcrumbs, spans for outgoing + * http requests, and attaches trace data when tracing is enabled via its `tracing` option. + * + * By default, this will always create breadcrumbs, and will create spans if tracing is enabled. + */ +export const httpIntegration = defineIntegration(_httpIntegration); - return false; - }, +/** + * The http integration instruments Node's internal http and https modules. + * It creates breadcrumbs and spans for outgoing HTTP requests which will be attached to the currently active span. + * + * @deprecated Use `httpIntegration()` instead. + */ +export class Http implements Integration { + /** + * @inheritDoc + */ + public static id: string = 'Http'; - ignoreIncomingRequestHook: request => { - const url = getRequestUrl(request); + /** + * @inheritDoc + */ + // eslint-disable-next-line deprecation/deprecation + public name: string = Http.id; - const method = request.method?.toUpperCase(); - // We do not capture OPTIONS/HEAD requests as transactions - if (method === 'OPTIONS' || method === 'HEAD') { - return true; - } + private readonly _breadcrumbs: boolean; + private readonly _tracing: TracingOptions | undefined; - if (_ignoreIncomingRequests && _ignoreIncomingRequests(url)) { - return true; - } + /** + * @inheritDoc + */ + public constructor(options: HttpOptions = {}) { + this._breadcrumbs = typeof options.breadcrumbs === 'undefined' ? true : options.breadcrumbs; + this._tracing = !options.tracing ? undefined : options.tracing === true ? {} : options.tracing; + } - return false; - }, + /** + * @inheritDoc + */ + public setupOnce(): void { + const clientOptions = getClient()?.getOptions(); - requireParentforOutgoingSpans: true, - requireParentforIncomingSpans: false, - requestHook: (span, req) => { - _updateSpan(span); + // If `tracing` is not explicitly set, we default this based on whether or not tracing is enabled. + // But for compatibility, we only do that if `enableIfHasTracingEnabled` is set. + const shouldCreateSpans = _shouldCreateSpans(this._tracing, clientOptions); - // Update the isolation scope, isolate this request - if (getSpanKind(span) === SpanKind.SERVER) { - const isolationScope = getIsolationScope().clone(); - isolationScope.setSDKProcessingMetadata({ request: req }); + // No need to instrument if we don't want to track anything + if (!this._breadcrumbs && !shouldCreateSpans) { + return; + } - const client = getClient(); - if (client && client.getOptions().autoSessionTracking) { - isolationScope.setRequestSession({ status: 'ok' }); - } - setIsolationScope(isolationScope); - } - }, - responseHook: (span, res) => { - if (_breadcrumbs) { - _addRequestBreadcrumb(span, res); - } + const shouldCreateSpanForRequest = _getShouldCreateSpanForRequest(shouldCreateSpans, this._tracing, clientOptions); - const client = getClient(); - if (client && client.getOptions().autoSessionTracking) { - setImmediate(() => { - client['_captureRequestSession'](); - }); - } - }, - }), - ]; - - registerInstrumentations({ - instrumentations, - }); - }, - }; -}) satisfies IntegrationFn; + // eslint-disable-next-line deprecation/deprecation + const tracePropagationTargets = clientOptions?.tracePropagationTargets || this._tracing?.tracePropagationTargets; + + // eslint-disable-next-line @typescript-eslint/no-var-requires + const httpModule = require('http'); + const wrappedHttpHandlerMaker = _createWrappedRequestMethodFactory( + httpModule, + this._breadcrumbs, + shouldCreateSpanForRequest, + tracePropagationTargets, + ); + fill(httpModule, 'get', wrappedHttpHandlerMaker); + fill(httpModule, 'request', wrappedHttpHandlerMaker); + + // NOTE: Prior to Node 9, `https` used internals of `http` module, thus we don't patch it. + // If we do, we'd get double breadcrumbs and double spans for `https` calls. + // It has been changed in Node 9, so for all versions equal and above, we patch `https` separately. + if (NODE_VERSION.major > 8) { + // eslint-disable-next-line @typescript-eslint/no-var-requires + const httpsModule = require('node:https'); + const wrappedHttpsHandlerMaker = _createWrappedRequestMethodFactory( + httpsModule, + this._breadcrumbs, + shouldCreateSpanForRequest, + tracePropagationTargets, + ); + fill(httpsModule, 'get', wrappedHttpsHandlerMaker); + fill(httpsModule, 'request', wrappedHttpsHandlerMaker); + } + } +} + +// for ease of reading below +type OriginalRequestMethod = RequestMethod; +type WrappedRequestMethod = RequestMethod; +type WrappedRequestMethodFactory = (original: OriginalRequestMethod) => WrappedRequestMethod; /** - * The http integration instruments Node's internal http and https modules. - * It creates breadcrumbs and spans for outgoing HTTP requests which will be attached to the currently active span. + * Function which creates a function which creates wrapped versions of internal `request` and `get` calls within `http` + * and `https` modules. (NB: Not a typo - this is a creator^2!) + * + * @param breadcrumbsEnabled Whether or not to record outgoing requests as breadcrumbs + * @param tracingEnabled Whether or not to record outgoing requests as tracing spans + * + * @returns A function which accepts the exiting handler and returns a wrapped handler */ -export const httpIntegration = defineIntegration(_httpIntegration); +function _createWrappedRequestMethodFactory( + httpModule: typeof http | typeof https, + breadcrumbsEnabled: boolean, + shouldCreateSpanForRequest: ((url: string) => boolean) | undefined, + tracePropagationTargets: TracePropagationTargets | undefined, +): WrappedRequestMethodFactory { + // We're caching results so we don't have to recompute regexp every time we create a request. + const createSpanUrlMap = new LRUMap(100); + const headersUrlMap = new LRUMap(100); + + const shouldCreateSpan = (url: string): boolean => { + if (shouldCreateSpanForRequest === undefined) { + return true; + } + + const cachedDecision = createSpanUrlMap.get(url); + if (cachedDecision !== undefined) { + return cachedDecision; + } + + const decision = shouldCreateSpanForRequest(url); + createSpanUrlMap.set(url, decision); + return decision; + }; + + const shouldAttachTraceData = (url: string): boolean => { + if (tracePropagationTargets === undefined) { + return true; + } + + const cachedDecision = headersUrlMap.get(url); + if (cachedDecision !== undefined) { + return cachedDecision; + } + + const decision = stringMatchesSomePattern(url, tracePropagationTargets); + headersUrlMap.set(url, decision); + return decision; + }; + + /** + * Captures Breadcrumb based on provided request/response pair + */ + function addRequestBreadcrumb( + event: string, + requestSpanData: SanitizedRequestData, + req: http.ClientRequest, + res?: http.IncomingMessage, + ): void { + if (!getClient()?.getIntegrationByName('Http')) { + return; + } + + addBreadcrumb( + { + category: 'http', + data: { + status_code: res && res.statusCode, + ...requestSpanData, + }, + type: 'http', + }, + { + event, + request: req, + response: res, + }, + ); + } + + return function wrappedRequestMethodFactory(originalRequestMethod: OriginalRequestMethod): WrappedRequestMethod { + return function wrappedMethod(this: unknown, ...args: RequestMethodArgs): http.ClientRequest { + const requestArgs = normalizeRequestArgs(httpModule, args); + const requestOptions = requestArgs[0]; + const rawRequestUrl = extractRawUrl(requestOptions); + const requestUrl = extractUrl(requestOptions); + const client = getClient(); + + // we don't want to record requests to Sentry as either breadcrumbs or spans, so just use the original method + if (isSentryRequestUrl(requestUrl, client)) { + return originalRequestMethod.apply(httpModule, requestArgs); + } + + const scope = getCurrentScope(); + const isolationScope = getIsolationScope(); + + const attributes = getRequestSpanData(requestUrl, requestOptions); -/** Update the span with data we need. */ -function _updateSpan(span: Span): void { - addOriginToSpan(span, 'auto.http.otel.http'); + const requestSpan = shouldCreateSpan(rawRequestUrl) + ? startInactiveSpan({ + onlyIfParent: true, + op: 'http.client', + name: `${attributes['http.method']} ${attributes.url}`, + attributes: { + ...attributes, + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.node.http', + }, + }) + : undefined; + + if (client && shouldAttachTraceData(rawRequestUrl)) { + const { traceId, spanId, sampled, dsc } = { + ...isolationScope.getPropagationContext(), + ...scope.getPropagationContext(), + }; + + const sentryTraceHeader = requestSpan + ? spanToTraceHeader(requestSpan) + : generateSentryTraceHeader(traceId, spanId, sampled); + + const sentryBaggageHeader = dynamicSamplingContextToSentryBaggageHeader( + dsc || + (requestSpan + ? getDynamicSamplingContextFromSpan(requestSpan) + : getDynamicSamplingContextFromClient(traceId, client)), + ); + + addHeadersToRequestOptions(requestOptions, requestUrl, sentryTraceHeader, sentryBaggageHeader); + } else { + DEBUG_BUILD && + logger.log( + `[Tracing] Not adding sentry-trace header to outgoing request (${requestUrl}) due to mismatching tracePropagationTargets option.`, + ); + } + + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + return originalRequestMethod + .apply(httpModule, requestArgs) + .once('response', function (this: http.ClientRequest, res: http.IncomingMessage): void { + // eslint-disable-next-line @typescript-eslint/no-this-alias + const req = this; + if (breadcrumbsEnabled) { + addRequestBreadcrumb('response', attributes, req, res); + } + if (requestSpan) { + if (res.statusCode) { + setHttpStatus(requestSpan, res.statusCode); + } + requestSpan.updateName(cleanSpanName(spanToJSON(requestSpan).description || '', requestOptions, req) || ''); + requestSpan.end(); + } + }) + .once('error', function (this: http.ClientRequest): void { + // eslint-disable-next-line @typescript-eslint/no-this-alias + const req = this; + + if (breadcrumbsEnabled) { + addRequestBreadcrumb('error', attributes, req); + } + if (requestSpan) { + setHttpStatus(requestSpan, 500); + requestSpan.updateName(cleanSpanName(spanToJSON(requestSpan).description || '', requestOptions, req) || ''); + requestSpan.end(); + } + }); + }; + }; } -/** Add a breadcrumb for outgoing requests. */ -function _addRequestBreadcrumb(span: Span, response: HTTPModuleRequestIncomingMessage | ServerResponse): void { - if (getSpanKind(span) !== SpanKind.CLIENT) { +function addHeadersToRequestOptions( + requestOptions: RequestOptions, + requestUrl: string, + sentryTraceHeader: string, + sentryBaggageHeader: string | undefined, +): void { + // Don't overwrite sentry-trace and baggage header if it's already set. + const headers = requestOptions.headers || {}; + if (headers['sentry-trace']) { return; } - const data = _INTERNAL.getRequestSpanData(span); - addBreadcrumb( - { - category: 'http', - data: { - status_code: response.statusCode, - ...data, - }, - type: 'http', - }, - { - event: 'response', - // TODO FN: Do we need access to `request` here? - // If we do, we'll have to use the `applyCustomAttributesOnSpan` hook instead, - // but this has worse context semantics than request/responseHook. - response, - }, - ); + DEBUG_BUILD && + logger.log(`[Tracing] Adding sentry-trace header ${sentryTraceHeader} to outgoing request to "${requestUrl}": `); + + requestOptions.headers = { + ...requestOptions.headers, + 'sentry-trace': sentryTraceHeader, + // Setting a header to `undefined` will crash in node so we only set the baggage header when it's defined + ...(sentryBaggageHeader && + sentryBaggageHeader.length > 0 && { baggage: normalizeBaggageHeader(requestOptions, sentryBaggageHeader) }), + }; +} + +function getRequestSpanData(requestUrl: string, requestOptions: RequestOptions): SanitizedRequestData { + const method = requestOptions.method || 'GET'; + const data: SanitizedRequestData = { + url: requestUrl, + 'http.method': method, + }; + if (requestOptions.hash) { + // strip leading "#" + data['http.fragment'] = requestOptions.hash.substring(1); + } + if (requestOptions.search) { + // strip leading "?" + data['http.query'] = requestOptions.search.substring(1); + } + return data; +} + +function normalizeBaggageHeader( + requestOptions: RequestOptions, + sentryBaggageHeader: string | undefined, +): string | string[] | undefined { + if (!requestOptions.headers || !requestOptions.headers.baggage) { + return sentryBaggageHeader; + } else if (!sentryBaggageHeader) { + return requestOptions.headers.baggage as string | string[]; + } else if (Array.isArray(requestOptions.headers.baggage)) { + return [...requestOptions.headers.baggage, sentryBaggageHeader]; + } + // Type-cast explanation: + // Technically this the following could be of type `(number | string)[]` but for the sake of simplicity + // we say this is undefined behaviour, since it would not be baggage spec conform if the user did this. + return [requestOptions.headers.baggage, sentryBaggageHeader] as string[]; +} + +/** Exported for tests only. */ +export function _shouldCreateSpans( + tracingOptions: TracingOptions | undefined, + clientOptions: Partial | undefined, +): boolean { + return tracingOptions === undefined + ? false + : tracingOptions.enableIfHasTracingEnabled + ? hasTracingEnabled(clientOptions) + : true; +} + +/** Exported for tests only. */ +export function _getShouldCreateSpanForRequest( + shouldCreateSpans: boolean, + tracingOptions: TracingOptions | undefined, + clientOptions: Partial | undefined, +): undefined | ((url: string) => boolean) { + const handler = shouldCreateSpans + ? // eslint-disable-next-line deprecation/deprecation + tracingOptions?.shouldCreateSpanForRequest || clientOptions?.shouldCreateSpanForRequest + : () => false; + + return handler; } diff --git a/packages/node/src/integrations/index.ts b/packages/node-experimental/src/integrations/index.ts similarity index 100% rename from packages/node/src/integrations/index.ts rename to packages/node-experimental/src/integrations/index.ts diff --git a/packages/node/src/integrations/local-variables/inspector.d.ts b/packages/node-experimental/src/integrations/local-variables/inspector.d.ts similarity index 100% rename from packages/node/src/integrations/local-variables/inspector.d.ts rename to packages/node-experimental/src/integrations/local-variables/inspector.d.ts diff --git a/packages/node/src/integrations/local-variables/local-variables-async.ts b/packages/node-experimental/src/integrations/local-variables/local-variables-async.ts similarity index 100% rename from packages/node/src/integrations/local-variables/local-variables-async.ts rename to packages/node-experimental/src/integrations/local-variables/local-variables-async.ts diff --git a/packages/node-experimental/src/integrations/local-variables/local-variables-sync.ts b/packages/node-experimental/src/integrations/local-variables/local-variables-sync.ts index 91fb9005b4c3..e1ec4b57023c 100644 --- a/packages/node-experimental/src/integrations/local-variables/local-variables-sync.ts +++ b/packages/node-experimental/src/integrations/local-variables/local-variables-sync.ts @@ -1,11 +1,11 @@ -import { defineIntegration, getClient } from '@sentry/core'; -import type { Event, Exception, IntegrationFn, StackParser } from '@sentry/types'; +/* eslint-disable max-lines */ +import { convertIntegrationFnToClass, defineIntegration, getClient } from '@sentry/core'; +import type { Event, Exception, Integration, IntegrationClass, IntegrationFn, StackParser } from '@sentry/types'; import { LRUMap, logger } from '@sentry/utils'; -import type { Debugger, InspectorNotification, Runtime } from 'inspector'; -import { Session } from 'inspector'; +import type { Debugger, InspectorNotification, Runtime, Session } from 'inspector'; +import type { NodeClient } from '../../client'; -import { NODE_MAJOR } from '../../nodeVersion'; -import type { NodeClient } from '../../sdk/client'; +import { NODE_VERSION } from '../../nodeVersion'; import type { FrameVariables, LocalVariablesIntegrationOptions, @@ -79,6 +79,22 @@ class AsyncSession implements DebugSession { /** Throws if inspector API is not available */ public constructor() { + /* + TODO: We really should get rid of this require statement below for a couple of reasons: + 1. It makes the integration unusable in the SvelteKit SDK, as it's not possible to use `require` + in SvelteKit server code (at least not by default). + 2. Throwing in a constructor is bad practice + + More context for a future attempt to fix this: + We already tried replacing it with import but didn't get it to work because of async problems. + We still called import in the constructor but assigned to a promise which we "awaited" in + `configureAndConnect`. However, this broke the Node integration tests as no local variables + were reported any more. We probably missed a place where we need to await the promise, too. + */ + + // Node can be built without inspector support so this can throw + // eslint-disable-next-line @typescript-eslint/no-var-requires + const { Session } = require('inspector'); this._session = new Session(); } @@ -288,16 +304,14 @@ const _localVariablesSyncIntegration = (( return; } - // Filter out frames where the function name is `new Promise` since these are in the error.stack frames - // but do not appear in the debugger call frames - const frames = (exception.stacktrace?.frames || []).filter(frame => frame.function !== 'new Promise'); + const frameCount = exception.stacktrace?.frames?.length || 0; - for (let i = 0; i < frames.length; i++) { + for (let i = 0; i < frameCount; i++) { // Sentry frames are in reverse order - const frameIndex = frames.length - i - 1; + const frameIndex = frameCount - i - 1; // Drop out if we run out of frames to match up - if (!frames[frameIndex] || !cachedFrame[i]) { + if (!exception?.stacktrace?.frames?.[frameIndex] || !cachedFrame[i]) { break; } @@ -305,14 +319,14 @@ const _localVariablesSyncIntegration = (( // We need to have vars to add cachedFrame[i].vars === undefined || // We're not interested in frames that are not in_app because the vars are not relevant - frames[frameIndex].in_app === false || + exception.stacktrace.frames[frameIndex].in_app === false || // The function names need to match - !functionNamesMatch(frames[frameIndex].function, cachedFrame[i].function) + !functionNamesMatch(exception.stacktrace.frames[frameIndex].function, cachedFrame[i].function) ) { continue; } - frames[frameIndex].vars = cachedFrame[i].vars; + exception.stacktrace.frames[frameIndex].vars = cachedFrame[i].vars; } } @@ -333,7 +347,7 @@ const _localVariablesSyncIntegration = (( if (session && clientOptions?.includeLocalVariables) { // Only setup this integration if the Node version is >= v18 // https://github.com/getsentry/sentry-javascript/issues/7697 - const unsupportedNodeVersion = NODE_MAJOR < 18; + const unsupportedNodeVersion = NODE_VERSION.major < 18; if (unsupportedNodeVersion) { logger.log('The `LocalVariables` integration is only supported on Node >= v18.'); @@ -386,7 +400,19 @@ const _localVariablesSyncIntegration = (( }; }) satisfies IntegrationFn; +export const localVariablesSyncIntegration = defineIntegration(_localVariablesSyncIntegration); + /** * Adds local variables to exception frames. + * @deprecated Use `localVariablesSyncIntegration()` instead. */ -export const localVariablesSyncIntegration = defineIntegration(_localVariablesSyncIntegration); +// eslint-disable-next-line deprecation/deprecation +export const LocalVariablesSync = convertIntegrationFnToClass( + INTEGRATION_NAME, + localVariablesSyncIntegration, +) as IntegrationClass Event; setup: (client: NodeClient) => void }> & { + new (options?: LocalVariablesIntegrationOptions, session?: DebugSession): Integration; +}; + +// eslint-disable-next-line deprecation/deprecation +export type LocalVariablesSync = typeof LocalVariablesSync; diff --git a/packages/node-experimental/src/integrations/modules.ts b/packages/node-experimental/src/integrations/modules.ts index ad30bb4d7a3b..1f9aff7303e3 100644 --- a/packages/node-experimental/src/integrations/modules.ts +++ b/packages/node-experimental/src/integrations/modules.ts @@ -1,31 +1,12 @@ import { existsSync, readFileSync } from 'fs'; import { dirname, join } from 'path'; -import { defineIntegration } from '@sentry/core'; -import type { IntegrationFn } from '@sentry/types'; +import { convertIntegrationFnToClass, defineIntegration } from '@sentry/core'; +import type { Event, Integration, IntegrationClass, IntegrationFn } from '@sentry/types'; let moduleCache: { [key: string]: string }; const INTEGRATION_NAME = '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. - */ -export const modulesIntegration = defineIntegration(_modulesIntegration); - /** Extract information about paths */ function getPaths(): string[] { try { @@ -94,3 +75,31 @@ function _getModules(): { [key: string]: string } { } return moduleCache; } + +const _modulesIntegration = (() => { + return { + name: INTEGRATION_NAME, + processEvent(event) { + event.modules = { + ...event.modules, + ..._getModules(), + }; + + return event; + }, + }; +}) satisfies IntegrationFn; + +export const modulesIntegration = defineIntegration(_modulesIntegration); + +/** + * Add node modules / packages to the event. + * @deprecated Use `modulesIntegration()` instead. + */ +// eslint-disable-next-line deprecation/deprecation +export const Modules = convertIntegrationFnToClass(INTEGRATION_NAME, modulesIntegration) as IntegrationClass< + Integration & { processEvent: (event: Event) => Event } +>; + +// eslint-disable-next-line deprecation/deprecation +export type Modules = typeof Modules; diff --git a/packages/node-experimental/src/integrations/onuncaughtexception.ts b/packages/node-experimental/src/integrations/onuncaughtexception.ts index e56c3c0801d7..68be68a6d6cc 100644 --- a/packages/node-experimental/src/integrations/onuncaughtexception.ts +++ b/packages/node-experimental/src/integrations/onuncaughtexception.ts @@ -1,11 +1,11 @@ -import { captureException, defineIntegration } from '@sentry/core'; +import { captureException, convertIntegrationFnToClass, defineIntegration } from '@sentry/core'; import { getClient } from '@sentry/core'; -import type { IntegrationFn } from '@sentry/types'; +import type { Integration, IntegrationClass, IntegrationFn } from '@sentry/types'; import { logger } from '@sentry/utils'; +import type { NodeClient } from '../client'; import { DEBUG_BUILD } from '../debug-build'; -import type { NodeClient } from '../sdk/client'; -import { logAndExitProcess } from '../utils/errorhandling'; +import { logAndExitProcess } from './utils/errorhandling'; type OnFatalErrorHandler = (firstError: Error, secondError?: Error) => void; @@ -54,10 +54,27 @@ const _onUncaughtExceptionIntegration = ((options: Partial void }> & { + new ( + options?: Partial<{ + exitEvenIfOtherHandlersAreRegistered: boolean; + onFatalError?(this: void, firstError: Error, secondError?: Error): void; + }>, + ): Integration; +}; + +// eslint-disable-next-line deprecation/deprecation +export type OnUncaughtException = typeof OnUncaughtException; type ErrorHandler = { _errorHandler: boolean } & ((error: Error) => void); @@ -86,19 +103,20 @@ export function makeErrorHandler(client: NodeClient, options: OnUncaughtExceptio // exit behaviour of the SDK accordingly: // - If other listeners are attached, do not exit. // - If the only listener attached is ours, exit. - const userProvidedListenersCount = (global.process.listeners('uncaughtException') as TaggedListener[]).filter( - listener => { + const userProvidedListenersCount = ( + global.process.listeners('uncaughtException') as TaggedListener[] + ).reduce((acc, listener) => { + if ( // There are 3 listeners we ignore: - return ( - // as soon as we're using domains this listener is attached by node itself - listener.name !== 'domainUncaughtExceptionClear' && - // the handler we register for tracing - listener.tag !== 'sentry_tracingErrorCallback' && - // the handler we register in this integration - (listener as ErrorHandler)._errorHandler !== true - ); - }, - ).length; + listener.name === 'domainUncaughtExceptionClear' || // as soon as we're using domains this listener is attached by node itself + (listener.tag && listener.tag === 'sentry_tracingErrorCallback') || // the handler we register for tracing + (listener as ErrorHandler)._errorHandler // the handler we register in this integration + ) { + return acc; + } else { + return acc + 1; + } + }, 0); const processWouldExit = userProvidedListenersCount === 0; const shouldApplyFatalHandlingLogic = options.exitEvenIfOtherHandlersAreRegistered || processWouldExit; diff --git a/packages/node-experimental/src/integrations/onunhandledrejection.ts b/packages/node-experimental/src/integrations/onunhandledrejection.ts index e1bc0b4145cf..9f3801b7a0cf 100644 --- a/packages/node-experimental/src/integrations/onunhandledrejection.ts +++ b/packages/node-experimental/src/integrations/onunhandledrejection.ts @@ -1,7 +1,8 @@ -import { captureException, defineIntegration, getClient } from '@sentry/core'; -import type { Client, IntegrationFn } from '@sentry/types'; +import { captureException, convertIntegrationFnToClass, defineIntegration, getClient } from '@sentry/core'; +import type { Client, Integration, IntegrationClass, IntegrationFn } from '@sentry/types'; import { consoleSandbox } from '@sentry/utils'; -import { logAndExitProcess } from '../utils/errorhandling'; + +import { logAndExitProcess } from './utils/errorhandling'; type UnhandledRejectionMode = 'none' | 'warn' | 'strict'; @@ -26,10 +27,22 @@ const _onUnhandledRejectionIntegration = ((options: Partial void }> & { + new (options?: Partial<{ mode: UnhandledRejectionMode }>): Integration; +}; + +// eslint-disable-next-line deprecation/deprecation +export type OnUnhandledRejection = typeof OnUnhandledRejection; /** * Send an exception with reason diff --git a/packages/node-experimental/src/integrations/spotlight.ts b/packages/node-experimental/src/integrations/spotlight.ts index 21629ad340ac..eb9c34260b61 100644 --- a/packages/node-experimental/src/integrations/spotlight.ts +++ b/packages/node-experimental/src/integrations/spotlight.ts @@ -1,6 +1,7 @@ import * as http from 'http'; -import { defineIntegration } from '@sentry/core'; -import type { Client, Envelope, IntegrationFn } from '@sentry/types'; +import { URL } from 'url'; +import { convertIntegrationFnToClass, defineIntegration } from '@sentry/core'; +import type { Client, Envelope, Integration, IntegrationClass, IntegrationFn } from '@sentry/types'; import { logger, serializeEnvelope } from '@sentry/utils'; type SpotlightConnectionOptions = { @@ -29,14 +30,30 @@ const _spotlightIntegration = ((options: Partial = { }; }) satisfies IntegrationFn; +export const spotlightIntegration = defineIntegration(_spotlightIntegration); + /** * Use this integration to send errors and transactions to Spotlight. * * Learn more about spotlight at https://spotlightjs.com * * Important: This integration only works with Node 18 or newer. + * + * @deprecated Use `spotlightIntegration()` instead. */ -export const spotlightIntegration = defineIntegration(_spotlightIntegration); +// eslint-disable-next-line deprecation/deprecation +export const Spotlight = convertIntegrationFnToClass(INTEGRATION_NAME, spotlightIntegration) as IntegrationClass< + Integration & { setup: (client: Client) => void } +> & { + new ( + options?: Partial<{ + sidecarUrl?: string; + }>, + ): Integration; +}; + +// eslint-disable-next-line deprecation/deprecation +export type Spotlight = typeof Spotlight; function connectToSpotlight(client: Client, options: Required): void { const spotlightUrl = parseSidecarUrl(options.sidecarUrl); diff --git a/packages/node/src/integrations/undici/index.ts b/packages/node-experimental/src/integrations/undici/index.ts similarity index 100% rename from packages/node/src/integrations/undici/index.ts rename to packages/node-experimental/src/integrations/undici/index.ts diff --git a/packages/node/src/integrations/undici/types.ts b/packages/node-experimental/src/integrations/undici/types.ts similarity index 100% rename from packages/node/src/integrations/undici/types.ts rename to packages/node-experimental/src/integrations/undici/types.ts diff --git a/packages/node/src/integrations/utils/errorhandling.ts b/packages/node-experimental/src/integrations/utils/errorhandling.ts similarity index 100% rename from packages/node/src/integrations/utils/errorhandling.ts rename to packages/node-experimental/src/integrations/utils/errorhandling.ts diff --git a/packages/node/src/integrations/utils/http.ts b/packages/node-experimental/src/integrations/utils/http.ts similarity index 100% rename from packages/node/src/integrations/utils/http.ts rename to packages/node-experimental/src/integrations/utils/http.ts diff --git a/packages/node-experimental/src/utils/module.ts b/packages/node-experimental/src/module.ts similarity index 100% rename from packages/node-experimental/src/utils/module.ts rename to packages/node-experimental/src/module.ts diff --git a/packages/node-experimental/src/nodeVersion.ts b/packages/node-experimental/src/nodeVersion.ts index 1f07883b771b..1574237f3fb4 100644 --- a/packages/node-experimental/src/nodeVersion.ts +++ b/packages/node-experimental/src/nodeVersion.ts @@ -1,4 +1,3 @@ import { parseSemver } from '@sentry/utils'; export const NODE_VERSION = parseSemver(process.versions.node) as { major: number; minor: number; patch: number }; -export const NODE_MAJOR = NODE_VERSION.major; diff --git a/packages/node-experimental/src/proxy/helpers.ts b/packages/node-experimental/src/proxy/helpers.ts index 031878511f6c..a5064408855d 100644 --- a/packages/node-experimental/src/proxy/helpers.ts +++ b/packages/node-experimental/src/proxy/helpers.ts @@ -30,6 +30,8 @@ import * as http from 'node:http'; import * as https from 'node:https'; import type { Readable } from 'stream'; +// TODO (v8): Remove this when Node < 12 is no longer supported +import type { URL } from 'url'; export type ThenableRequest = http.ClientRequest & { then: Promise['then']; diff --git a/packages/node-experimental/src/proxy/index.ts b/packages/node-experimental/src/proxy/index.ts index 83f72d56fb4e..15c700ed3e62 100644 --- a/packages/node-experimental/src/proxy/index.ts +++ b/packages/node-experimental/src/proxy/index.ts @@ -32,6 +32,8 @@ import type * as http from 'http'; import type { OutgoingHttpHeaders } from 'http'; import * as net from 'net'; import * as tls from 'tls'; +// TODO (v8): Remove this when Node < 12 is no longer supported +import { URL } from 'url'; import { logger } from '@sentry/utils'; import { Agent } from './base'; import type { AgentConnectOpts } from './base'; diff --git a/packages/node/src/sdk.ts b/packages/node-experimental/src/sdk.ts similarity index 100% rename from packages/node/src/sdk.ts rename to packages/node-experimental/src/sdk.ts diff --git a/packages/node/src/tracing/index.ts b/packages/node-experimental/src/tracing/index.ts similarity index 100% rename from packages/node/src/tracing/index.ts rename to packages/node-experimental/src/tracing/index.ts diff --git a/packages/node/src/tracing/integrations.ts b/packages/node-experimental/src/tracing/integrations.ts similarity index 100% rename from packages/node/src/tracing/integrations.ts rename to packages/node-experimental/src/tracing/integrations.ts diff --git a/packages/node-experimental/src/transports/http-module.ts b/packages/node-experimental/src/transports/http-module.ts index f5cbe6fd35f9..64b255cc869c 100644 --- a/packages/node-experimental/src/transports/http-module.ts +++ b/packages/node-experimental/src/transports/http-module.ts @@ -1,5 +1,7 @@ -import type { ClientRequest, IncomingHttpHeaders, RequestOptions as HTTPRequestOptions } from 'node:http'; -import type { RequestOptions as HTTPSRequestOptions } from 'node:https'; +import type { IncomingHttpHeaders, RequestOptions as HTTPRequestOptions } from 'http'; +import type { RequestOptions as HTTPSRequestOptions } from 'https'; +import type { Writable } from 'stream'; +import type { URL } from 'url'; export type HTTPModuleRequestOptions = HTTPRequestOptions | HTTPSRequestOptions | string | URL; @@ -24,5 +26,15 @@ export interface HTTPModule { * @param options These are {@see TransportOptions} * @param callback Callback when request is finished */ - request(options: HTTPModuleRequestOptions, callback?: (res: HTTPModuleRequestIncomingMessage) => void): ClientRequest; + request(options: HTTPModuleRequestOptions, callback?: (res: HTTPModuleRequestIncomingMessage) => void): Writable; + + // This is the type for nodejs versions that handle the URL argument + // (v10.9.0+), but we do not use it just yet because we support older node + // versions: + + // request( + // url: string | URL, + // options: http.RequestOptions | https.RequestOptions, + // callback?: (res: http.IncomingMessage) => void, + // ): http.ClientRequest; } diff --git a/packages/node-experimental/src/transports/http.ts b/packages/node-experimental/src/transports/http.ts index 4cbe7ece1f60..a6a05fc07c95 100644 --- a/packages/node-experimental/src/transports/http.ts +++ b/packages/node-experimental/src/transports/http.ts @@ -1,9 +1,8 @@ import * as http from 'node:http'; import * as https from 'node:https'; import { Readable } from 'stream'; +import { URL } from 'url'; import { createGzip } from 'zlib'; -import { context } from '@opentelemetry/api'; -import { suppressTracing } from '@opentelemetry/core'; import { createTransport } from '@sentry/core'; import type { BaseTransportOptions, @@ -14,6 +13,7 @@ import type { } from '@sentry/types'; import { consoleSandbox } from '@sentry/utils'; import { HttpsProxyAgent } from '../proxy'; + import type { HTTPModule } from './http-module'; export interface NodeTransportOptions extends BaseTransportOptions { @@ -81,11 +81,8 @@ export function makeNodeTransport(options: NodeTransportOptions): Transport { ? (new HttpsProxyAgent(proxy) as http.Agent) : new nativeHttpModule.Agent({ keepAlive, maxSockets: 30, timeout: 2000 }); - // This ensures we do not generate any spans in OpenTelemetry for the transport - return context.with(suppressTracing(context.active()), () => { - const requestExecutor = createRequestExecutor(options, options.httpModule ?? nativeHttpModule, agent); - return createTransport(options, requestExecutor); - }); + const requestExecutor = createRequestExecutor(options, options.httpModule ?? nativeHttpModule, agent); + return createTransport(options, requestExecutor); } /** diff --git a/packages/node-experimental/src/types.ts b/packages/node-experimental/src/types.ts index d78e1761fd79..01f91fb46cbe 100644 --- a/packages/node-experimental/src/types.ts +++ b/packages/node-experimental/src/types.ts @@ -1,7 +1,6 @@ -import type { Span as WriteableSpan } from '@opentelemetry/api'; -import type { ReadableSpan } from '@opentelemetry/sdk-trace-base'; -import type { ClientOptions, Options, SamplingContext, Scope, Span, TracePropagationTargets } from '@sentry/types'; +import type { ClientOptions, Options, SamplingContext, TracePropagationTargets } from '@sentry/types'; +import type { NodeClient } from './client'; import type { NodeTransportOptions } from './transports'; export interface BaseNodeOptions { @@ -52,6 +51,14 @@ export interface BaseNodeOptions { */ includeLocalVariables?: boolean; + /** + * Specify a custom NodeClient to be used. Must extend NodeClient! + * This is not a public, supported API, but used internally only. + * + * @hidden + * */ + clientClass?: typeof NodeClient; + /** * If you use Spotlight by Sentry during development, use * this option to forward captured Sentry events to Spotlight. @@ -64,15 +71,23 @@ export interface BaseNodeOptions { */ spotlight?: boolean | string; + // TODO (v8): Remove this in v8 /** - * If this is set to true, the SDK will not set up OpenTelemetry automatically. - * In this case, you _have_ to ensure to set it up correctly yourself, including: - * * The `SentrySpanProcessor` - * * The `SentryPropagator` - * * The `SentryContextManager` - * * The `SentrySampler` + * @deprecated Moved to constructor options of the `Http` and `Undici` integration. + * @example + * ```js + * Sentry.init({ + * integrations: [ + * new Sentry.Integrations.Http({ + * tracing: { + * shouldCreateSpanForRequest: (url: string) => false, + * } + * }); + * ], + * }); + * ``` */ - skipOpenTelemetrySetup?: boolean; + shouldCreateSpanForRequest?(this: void, url: string): boolean; /** Callback that is executed when a fatal global error occurs. */ onFatalError?(this: void, error: Error): void; @@ -89,19 +104,3 @@ export interface NodeOptions extends Options, BaseNodeOpti * @see NodeClient for more information. */ export interface NodeClientOptions extends ClientOptions, BaseNodeOptions {} - -export interface CurrentScopes { - scope: Scope; - isolationScope: Scope; -} - -/** - * The base `Span` type is basically a `WriteableSpan`. - * There are places where we basically want to allow passing _any_ span, - * so in these cases we type this as `AbstractSpan` which could be either a regular `Span` or a `ReadableSpan`. - * You'll have to make sur to check revelant fields before accessing them. - * - * Note that technically, the `Span` exported from `@opentelemwetry/sdk-trace-base` matches this, - * but we cannot be 100% sure that we are actually getting such a span, so this type is more defensive. - */ -export type AbstractSpan = WriteableSpan | ReadableSpan | Span; diff --git a/packages/node/test/async/domain.test.ts b/packages/node-experimental/test/async/domain.test.ts similarity index 100% rename from packages/node/test/async/domain.test.ts rename to packages/node-experimental/test/async/domain.test.ts diff --git a/packages/node/test/async/hooks.test.ts b/packages/node-experimental/test/async/hooks.test.ts similarity index 100% rename from packages/node/test/async/hooks.test.ts rename to packages/node-experimental/test/async/hooks.test.ts diff --git a/packages/node/test/client.test.ts b/packages/node-experimental/test/client.test.ts similarity index 100% rename from packages/node/test/client.test.ts rename to packages/node-experimental/test/client.test.ts diff --git a/packages/node/test/eventbuilders.test.ts b/packages/node-experimental/test/eventbuilders.test.ts similarity index 100% rename from packages/node/test/eventbuilders.test.ts rename to packages/node-experimental/test/eventbuilders.test.ts diff --git a/packages/node/test/fixtures/testDeepReadDirSync/cats/eddy.txt b/packages/node-experimental/test/fixtures/testDeepReadDirSync/cats/eddy.txt similarity index 100% rename from packages/node/test/fixtures/testDeepReadDirSync/cats/eddy.txt rename to packages/node-experimental/test/fixtures/testDeepReadDirSync/cats/eddy.txt diff --git a/packages/node/test/fixtures/testDeepReadDirSync/cats/persephone.txt b/packages/node-experimental/test/fixtures/testDeepReadDirSync/cats/persephone.txt similarity index 100% rename from packages/node/test/fixtures/testDeepReadDirSync/cats/persephone.txt rename to packages/node-experimental/test/fixtures/testDeepReadDirSync/cats/persephone.txt diff --git a/packages/node/test/fixtures/testDeepReadDirSync/cats/piper.txt b/packages/node-experimental/test/fixtures/testDeepReadDirSync/cats/piper.txt similarity index 100% rename from packages/node/test/fixtures/testDeepReadDirSync/cats/piper.txt rename to packages/node-experimental/test/fixtures/testDeepReadDirSync/cats/piper.txt diff --git a/packages/node/test/fixtures/testDeepReadDirSync/cats/sassafras.txt b/packages/node-experimental/test/fixtures/testDeepReadDirSync/cats/sassafras.txt similarity index 100% rename from packages/node/test/fixtures/testDeepReadDirSync/cats/sassafras.txt rename to packages/node-experimental/test/fixtures/testDeepReadDirSync/cats/sassafras.txt diff --git a/packages/node/test/fixtures/testDeepReadDirSync/cats/teaberry.txt b/packages/node-experimental/test/fixtures/testDeepReadDirSync/cats/teaberry.txt similarity index 100% rename from packages/node/test/fixtures/testDeepReadDirSync/cats/teaberry.txt rename to packages/node-experimental/test/fixtures/testDeepReadDirSync/cats/teaberry.txt diff --git a/packages/node/test/fixtures/testDeepReadDirSync/debra.txt b/packages/node-experimental/test/fixtures/testDeepReadDirSync/debra.txt similarity index 100% rename from packages/node/test/fixtures/testDeepReadDirSync/debra.txt rename to packages/node-experimental/test/fixtures/testDeepReadDirSync/debra.txt diff --git a/packages/node/test/fixtures/testDeepReadDirSync/dogs/theBigs/charlie.txt b/packages/node-experimental/test/fixtures/testDeepReadDirSync/dogs/theBigs/charlie.txt similarity index 100% rename from packages/node/test/fixtures/testDeepReadDirSync/dogs/theBigs/charlie.txt rename to packages/node-experimental/test/fixtures/testDeepReadDirSync/dogs/theBigs/charlie.txt diff --git a/packages/node/test/fixtures/testDeepReadDirSync/dogs/theBigs/maisey.txt b/packages/node-experimental/test/fixtures/testDeepReadDirSync/dogs/theBigs/maisey.txt similarity index 100% rename from packages/node/test/fixtures/testDeepReadDirSync/dogs/theBigs/maisey.txt rename to packages/node-experimental/test/fixtures/testDeepReadDirSync/dogs/theBigs/maisey.txt diff --git a/packages/node/test/fixtures/testDeepReadDirSync/dogs/theSmalls/bodhi.txt b/packages/node-experimental/test/fixtures/testDeepReadDirSync/dogs/theSmalls/bodhi.txt similarity index 100% rename from packages/node/test/fixtures/testDeepReadDirSync/dogs/theSmalls/bodhi.txt rename to packages/node-experimental/test/fixtures/testDeepReadDirSync/dogs/theSmalls/bodhi.txt diff --git a/packages/node/test/fixtures/testDeepReadDirSync/dogs/theSmalls/cory.txt b/packages/node-experimental/test/fixtures/testDeepReadDirSync/dogs/theSmalls/cory.txt similarity index 100% rename from packages/node/test/fixtures/testDeepReadDirSync/dogs/theSmalls/cory.txt rename to packages/node-experimental/test/fixtures/testDeepReadDirSync/dogs/theSmalls/cory.txt diff --git a/packages/node/test/handlers.test.ts b/packages/node-experimental/test/handlers.test.ts similarity index 100% rename from packages/node/test/handlers.test.ts rename to packages/node-experimental/test/handlers.test.ts diff --git a/packages/node/test/helper/error.ts b/packages/node-experimental/test/helper/error.ts similarity index 100% rename from packages/node/test/helper/error.ts rename to packages/node-experimental/test/helper/error.ts diff --git a/packages/node/test/helper/node-client-options.ts b/packages/node-experimental/test/helper/node-client-options.ts similarity index 100% rename from packages/node/test/helper/node-client-options.ts rename to packages/node-experimental/test/helper/node-client-options.ts diff --git a/packages/node/test/index.test.ts b/packages/node-experimental/test/index.test.ts similarity index 100% rename from packages/node/test/index.test.ts rename to packages/node-experimental/test/index.test.ts diff --git a/packages/node-experimental/test/integrations/contextlines.test.ts b/packages/node-experimental/test/integrations/contextlines.test.ts index c4ef1efaa292..67f3ad793fbc 100644 --- a/packages/node-experimental/test/integrations/contextlines.test.ts +++ b/packages/node-experimental/test/integrations/contextlines.test.ts @@ -1,37 +1,30 @@ -import { promises } from 'fs'; -import type { StackFrame } from '@sentry/types'; +import * as fs from 'fs'; +import type { Event, Integration, StackFrame } from '@sentry/types'; import { parseStackFrames } from '@sentry/utils'; -import { _contextLinesIntegration, resetFileContentCache } from '../../src/integrations/contextlines'; -import { defaultStackParser } from '../../src/sdk/api'; -import { getError } from '../helpers/error'; - -jest.mock('fs', () => { - const actual = jest.requireActual('fs'); - return { - ...actual, - promises: { - ...actual.promises, - readFile: jest.fn(actual.promises), - }, - }; -}); +import { contextLinesIntegration } from '../../src'; +import { resetFileContentCache } from '../../src/integrations/contextlines'; +import { defaultStackParser } from '../../src/sdk'; +import { getError } from '../helper/error'; describe('ContextLines', () => { - const readFileSpy = promises.readFile as unknown as jest.SpyInstance; - let contextLines: ReturnType; + let readFileSpy: jest.SpyInstance; + let contextLines: Integration; async function addContext(frames: StackFrame[]): Promise { - await contextLines.processEvent({ exception: { values: [{ stacktrace: { frames } }] } }); + await (contextLines as Integration & { processEvent: (event: Event) => Promise }).processEvent({ + exception: { values: [{ stacktrace: { frames } }] }, + }); } beforeEach(() => { - contextLines = _contextLinesIntegration(); + readFileSpy = jest.spyOn(fs, 'readFile'); + contextLines = contextLinesIntegration(); resetFileContentCache(); }); afterEach(() => { - jest.clearAllMocks(); + jest.restoreAllMocks(); }); describe('lru file cache', () => { @@ -108,7 +101,7 @@ describe('ContextLines', () => { }); test('parseStack with no context', async () => { - contextLines = _contextLinesIntegration({ frameContextLines: 0 }); + contextLines = contextLinesIntegration({ frameContextLines: 0 }); expect.assertions(1); const frames = parseStackFrames(defaultStackParser, new Error('test')); @@ -120,6 +113,7 @@ describe('ContextLines', () => { test('does not attempt to readfile multiple times if it fails', async () => { expect.assertions(1); + contextLines = contextLinesIntegration(); readFileSpy.mockImplementation(() => { throw new Error("ENOENT: no such file or directory, open '/does/not/exist.js'"); diff --git a/packages/node/test/integrations/http.test.ts b/packages/node-experimental/test/integrations/http.test.ts similarity index 100% rename from packages/node/test/integrations/http.test.ts rename to packages/node-experimental/test/integrations/http.test.ts diff --git a/packages/node-experimental/test/integrations/localvariables.test.ts b/packages/node-experimental/test/integrations/localvariables.test.ts index db9385214d42..abc1d241f842 100644 --- a/packages/node-experimental/test/integrations/localvariables.test.ts +++ b/packages/node-experimental/test/integrations/localvariables.test.ts @@ -1,12 +1,12 @@ import { createRateLimiter } from '../../src/integrations/local-variables/common'; import { createCallbackList } from '../../src/integrations/local-variables/local-variables-sync'; -import { NODE_MAJOR } from '../../src/nodeVersion'; +import { NODE_VERSION } from '../../src/nodeVersion'; jest.setTimeout(20_000); const describeIf = (condition: boolean) => (condition ? describe : describe.skip); -describeIf(NODE_MAJOR >= 18)('LocalVariables', () => { +describeIf(NODE_VERSION.major >= 18)('LocalVariables', () => { describe('createCallbackList', () => { it('Should call callbacks in reverse order', done => { const log: number[] = []; diff --git a/packages/node-experimental/test/integrations/spotlight.test.ts b/packages/node-experimental/test/integrations/spotlight.test.ts index 6b888c22edcd..a892677d0dd0 100644 --- a/packages/node-experimental/test/integrations/spotlight.test.ts +++ b/packages/node-experimental/test/integrations/spotlight.test.ts @@ -2,9 +2,9 @@ import * as http from 'http'; import type { Envelope, EventEnvelope } from '@sentry/types'; import { createEnvelope, logger } from '@sentry/utils'; -import { spotlightIntegration } from '../../src/integrations/spotlight'; -import { NodeClient } from '../../src/sdk/client'; -import { getDefaultNodeClientOptions } from '../helpers/getDefaultNodeClientOptions'; +import { NodeClient, spotlightIntegration } from '../../src'; +import { Spotlight } from '../../src/integrations'; +import { getDefaultNodeClientOptions } from '../helper/node-client-options'; describe('Spotlight', () => { const loggerSpy = jest.spyOn(logger, 'warn'); @@ -17,9 +17,12 @@ describe('Spotlight', () => { const options = getDefaultNodeClientOptions(); const client = new NodeClient(options); - it('has a name', () => { - const integration = spotlightIntegration(); + it('has a name and id', () => { + // eslint-disable-next-line deprecation/deprecation + const integration = new Spotlight(); expect(integration.name).toEqual('Spotlight'); + // eslint-disable-next-line deprecation/deprecation + expect(Spotlight.id).toEqual('Spotlight'); }); it('registers a callback on the `beforeEnvelope` hook', () => { diff --git a/packages/node/test/integrations/undici.test.ts b/packages/node-experimental/test/integrations/undici.test.ts similarity index 100% rename from packages/node/test/integrations/undici.test.ts rename to packages/node-experimental/test/integrations/undici.test.ts diff --git a/packages/node/test/manual/colorize.js b/packages/node-experimental/test/manual/colorize.js similarity index 100% rename from packages/node/test/manual/colorize.js rename to packages/node-experimental/test/manual/colorize.js diff --git a/packages/node/test/manual/express-scope-separation/start.js b/packages/node-experimental/test/manual/express-scope-separation/start.js similarity index 100% rename from packages/node/test/manual/express-scope-separation/start.js rename to packages/node-experimental/test/manual/express-scope-separation/start.js diff --git a/packages/node/test/manual/memory-leak/.gitignore b/packages/node-experimental/test/manual/memory-leak/.gitignore similarity index 100% rename from packages/node/test/manual/memory-leak/.gitignore rename to packages/node-experimental/test/manual/memory-leak/.gitignore diff --git a/packages/node/test/manual/memory-leak/README.md b/packages/node-experimental/test/manual/memory-leak/README.md similarity index 100% rename from packages/node/test/manual/memory-leak/README.md rename to packages/node-experimental/test/manual/memory-leak/README.md diff --git a/packages/node/test/manual/memory-leak/context-memory.js b/packages/node-experimental/test/manual/memory-leak/context-memory.js similarity index 100% rename from packages/node/test/manual/memory-leak/context-memory.js rename to packages/node-experimental/test/manual/memory-leak/context-memory.js diff --git a/packages/node/test/manual/memory-leak/express-patient.js b/packages/node-experimental/test/manual/memory-leak/express-patient.js similarity index 100% rename from packages/node/test/manual/memory-leak/express-patient.js rename to packages/node-experimental/test/manual/memory-leak/express-patient.js diff --git a/packages/node/test/manual/memory-leak/large-module-src.js b/packages/node-experimental/test/manual/memory-leak/large-module-src.js similarity index 100% rename from packages/node/test/manual/memory-leak/large-module-src.js rename to packages/node-experimental/test/manual/memory-leak/large-module-src.js diff --git a/packages/node/test/manual/memory-leak/manager.js b/packages/node-experimental/test/manual/memory-leak/manager.js similarity index 100% rename from packages/node/test/manual/memory-leak/manager.js rename to packages/node-experimental/test/manual/memory-leak/manager.js diff --git a/packages/node/test/manual/memory-leak/poke-patient.sh b/packages/node-experimental/test/manual/memory-leak/poke-patient.sh similarity index 100% rename from packages/node/test/manual/memory-leak/poke-patient.sh rename to packages/node-experimental/test/manual/memory-leak/poke-patient.sh diff --git a/packages/node/test/manual/release-health/runner.js b/packages/node-experimental/test/manual/release-health/runner.js similarity index 100% rename from packages/node/test/manual/release-health/runner.js rename to packages/node-experimental/test/manual/release-health/runner.js diff --git a/packages/node/test/manual/release-health/session-aggregates/aggregates-disable-single-session.js b/packages/node-experimental/test/manual/release-health/session-aggregates/aggregates-disable-single-session.js similarity index 100% rename from packages/node/test/manual/release-health/session-aggregates/aggregates-disable-single-session.js rename to packages/node-experimental/test/manual/release-health/session-aggregates/aggregates-disable-single-session.js diff --git a/packages/node/test/manual/release-health/single-session/caught-exception-errored-session.js b/packages/node-experimental/test/manual/release-health/single-session/caught-exception-errored-session.js similarity index 100% rename from packages/node/test/manual/release-health/single-session/caught-exception-errored-session.js rename to packages/node-experimental/test/manual/release-health/single-session/caught-exception-errored-session.js diff --git a/packages/node/test/manual/release-health/single-session/errors-in-session-capped-to-one.js b/packages/node-experimental/test/manual/release-health/single-session/errors-in-session-capped-to-one.js similarity index 100% rename from packages/node/test/manual/release-health/single-session/errors-in-session-capped-to-one.js rename to packages/node-experimental/test/manual/release-health/single-session/errors-in-session-capped-to-one.js diff --git a/packages/node/test/manual/release-health/single-session/healthy-session.js b/packages/node-experimental/test/manual/release-health/single-session/healthy-session.js similarity index 100% rename from packages/node/test/manual/release-health/single-session/healthy-session.js rename to packages/node-experimental/test/manual/release-health/single-session/healthy-session.js diff --git a/packages/node/test/manual/release-health/single-session/terminal-state-sessions-sent-once.js b/packages/node-experimental/test/manual/release-health/single-session/terminal-state-sessions-sent-once.js similarity index 100% rename from packages/node/test/manual/release-health/single-session/terminal-state-sessions-sent-once.js rename to packages/node-experimental/test/manual/release-health/single-session/terminal-state-sessions-sent-once.js diff --git a/packages/node/test/manual/release-health/single-session/uncaught-exception-crashed-session.js b/packages/node-experimental/test/manual/release-health/single-session/uncaught-exception-crashed-session.js similarity index 100% rename from packages/node/test/manual/release-health/single-session/uncaught-exception-crashed-session.js rename to packages/node-experimental/test/manual/release-health/single-session/uncaught-exception-crashed-session.js diff --git a/packages/node/test/manual/release-health/single-session/unhandled-rejection-crashed-session.js b/packages/node-experimental/test/manual/release-health/single-session/unhandled-rejection-crashed-session.js similarity index 100% rename from packages/node/test/manual/release-health/single-session/unhandled-rejection-crashed-session.js rename to packages/node-experimental/test/manual/release-health/single-session/unhandled-rejection-crashed-session.js diff --git a/packages/node/test/manual/release-health/test-utils.js b/packages/node-experimental/test/manual/release-health/test-utils.js similarity index 100% rename from packages/node/test/manual/release-health/test-utils.js rename to packages/node-experimental/test/manual/release-health/test-utils.js diff --git a/packages/node/test/manual/webpack-async-context/index.js b/packages/node-experimental/test/manual/webpack-async-context/index.js similarity index 100% rename from packages/node/test/manual/webpack-async-context/index.js rename to packages/node-experimental/test/manual/webpack-async-context/index.js diff --git a/packages/node/test/manual/webpack-async-context/npm-build.js b/packages/node-experimental/test/manual/webpack-async-context/npm-build.js similarity index 100% rename from packages/node/test/manual/webpack-async-context/npm-build.js rename to packages/node-experimental/test/manual/webpack-async-context/npm-build.js diff --git a/packages/node/test/manual/webpack-async-context/package.json b/packages/node-experimental/test/manual/webpack-async-context/package.json similarity index 100% rename from packages/node/test/manual/webpack-async-context/package.json rename to packages/node-experimental/test/manual/webpack-async-context/package.json diff --git a/packages/node/test/manual/webpack-async-context/yarn.lock b/packages/node-experimental/test/manual/webpack-async-context/yarn.lock similarity index 100% rename from packages/node/test/manual/webpack-async-context/yarn.lock rename to packages/node-experimental/test/manual/webpack-async-context/yarn.lock diff --git a/packages/node/test/module.test.ts b/packages/node-experimental/test/module.test.ts similarity index 100% rename from packages/node/test/module.test.ts rename to packages/node-experimental/test/module.test.ts diff --git a/packages/node/test/onuncaughtexception.test.ts b/packages/node-experimental/test/onuncaughtexception.test.ts similarity index 100% rename from packages/node/test/onuncaughtexception.test.ts rename to packages/node-experimental/test/onuncaughtexception.test.ts diff --git a/packages/node/test/onunhandledrejection.test.ts b/packages/node-experimental/test/onunhandledrejection.test.ts similarity index 100% rename from packages/node/test/onunhandledrejection.test.ts rename to packages/node-experimental/test/onunhandledrejection.test.ts diff --git a/packages/node/test/performance.test.ts b/packages/node-experimental/test/performance.test.ts similarity index 100% rename from packages/node/test/performance.test.ts rename to packages/node-experimental/test/performance.test.ts diff --git a/packages/node/test/sdk.test.ts b/packages/node-experimental/test/sdk.test.ts similarity index 100% rename from packages/node/test/sdk.test.ts rename to packages/node-experimental/test/sdk.test.ts diff --git a/packages/node/test/stacktrace.test.ts b/packages/node-experimental/test/stacktrace.test.ts similarity index 100% rename from packages/node/test/stacktrace.test.ts rename to packages/node-experimental/test/stacktrace.test.ts diff --git a/packages/node-experimental/test/transports/http.test.ts b/packages/node-experimental/test/transports/http.test.ts index e945c086959a..ddf73039a009 100644 --- a/packages/node-experimental/test/transports/http.test.ts +++ b/packages/node-experimental/test/transports/http.test.ts @@ -1,4 +1,6 @@ +/* eslint-disable deprecation/deprecation */ import * as http from 'http'; + import { createGunzip } from 'zlib'; import { createTransport } from '@sentry/core'; import type { EventEnvelope, EventItem } from '@sentry/types'; @@ -49,18 +51,19 @@ function setupTestServer( res.end(); // also terminate socket because keepalive hangs connection a bit - // eslint-disable-next-line deprecation/deprecation - res.connection?.end(); + if (res.connection) { + res.connection.end(); + } }); - testServer.listen(18101); + testServer.listen(18099); return new Promise(resolve => { testServer?.on('listening', resolve); }); } -const TEST_SERVER_URL = 'http://localhost:18101'; +const TEST_SERVER_URL = 'http://localhost:18099'; const EVENT_ENVELOPE = createEnvelope({ event_id: 'aa3ff046696b4bc6b609ce6d28fde9e2', sent_at: '123' }, [ [{ type: 'event' }, { event_id: 'aa3ff046696b4bc6b609ce6d28fde9e2' }] as EventItem, diff --git a/packages/node-experimental/test/transports/https.test.ts b/packages/node-experimental/test/transports/https.test.ts index 8b0d3312ba54..a45319c40e42 100644 --- a/packages/node-experimental/test/transports/https.test.ts +++ b/packages/node-experimental/test/transports/https.test.ts @@ -1,3 +1,4 @@ +/* eslint-disable deprecation/deprecation */ import * as http from 'http'; import * as https from 'https'; import { createTransport } from '@sentry/core'; @@ -49,18 +50,19 @@ function setupTestServer( res.end(); // also terminate socket because keepalive hangs connection a bit - // eslint-disable-next-line deprecation/deprecation - res.connection?.end(); + if (res.connection) { + res.connection.end(); + } }); - testServer.listen(8100); + testServer.listen(8099); return new Promise(resolve => { testServer?.on('listening', resolve); }); } -const TEST_SERVER_URL = 'https://localhost:8100'; +const TEST_SERVER_URL = 'https://localhost:8099'; const EVENT_ENVELOPE = createEnvelope({ event_id: 'aa3ff046696b4bc6b609ce6d28fde9e2', sent_at: '123' }, [ [{ type: 'event' }, { event_id: 'aa3ff046696b4bc6b609ce6d28fde9e2' }] as EventItem, diff --git a/packages/node/test/utils.ts b/packages/node-experimental/test/utils.ts similarity index 100% rename from packages/node/test/utils.ts rename to packages/node-experimental/test/utils.ts diff --git a/packages/node-experimental/tsconfig.json b/packages/node-experimental/tsconfig.json index 8f38d240197e..89a9b9e0e2fe 100644 --- a/packages/node-experimental/tsconfig.json +++ b/packages/node-experimental/tsconfig.json @@ -4,7 +4,6 @@ "include": ["src/**/*"], "compilerOptions": { - "lib": ["es2018"], - "module": "Node16" + "lib": ["es2018"] } } diff --git a/packages/node-experimental/tsconfig.test.json b/packages/node-experimental/tsconfig.test.json index 87f6afa06b86..52333183eb70 100644 --- a/packages/node-experimental/tsconfig.test.json +++ b/packages/node-experimental/tsconfig.test.json @@ -1,7 +1,7 @@ { "extends": "./tsconfig.json", - "include": ["test/**/*"], + "include": ["test/**/*", "src/**/*.d.ts"], "compilerOptions": { // should include all types from `./tsconfig.json` plus types for all test frameworks used diff --git a/packages/node/LICENSE b/packages/node/LICENSE index 535ef0561e1b..d11896ba1181 100644 --- a/packages/node/LICENSE +++ b/packages/node/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2019 Sentry (https://sentry.io) and individual contributors. All rights reserved. +Copyright (c) 2023 Sentry (https://sentry.io) and individual contributors. All rights reserved. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the diff --git a/packages/node/README.md b/packages/node/README.md index af6819c9a4ce..324700199b53 100644 --- a/packages/node/README.md +++ b/packages/node/README.md @@ -4,36 +4,28 @@

-# Legacy Sentry SDK for NodeJS +# Official Sentry SDK for Node -[![npm version](https://img.shields.io/npm/v/@sentry/node-experimental.svg)](https://www.npmjs.com/package/@sentry/node-experimental) -[![npm dm](https://img.shields.io/npm/dm/@sentry/node-experimental.svg)](https://www.npmjs.com/package/@sentry/node-experimental) -[![npm dt](https://img.shields.io/npm/dt/@sentry/node-experimental.svg)](https://www.npmjs.com/package/@sentry/node-experimental) +[![npm version](https://img.shields.io/npm/v/@sentry/node.svg)](https://www.npmjs.com/package/@sentry/node) +[![npm dm](https://img.shields.io/npm/dm/@sentry/node.svg)](https://www.npmjs.com/package/@sentry/node) +[![npm dt](https://img.shields.io/npm/dt/@sentry/node.svg)](https://www.npmjs.com/package/@sentry/node) -## Links - -- [Official SDK Docs](https://docs.sentry.io/quickstart/) -- [TypeDoc](http://getsentry.github.io/sentry-javascript/) +## Installation -## Status +```bash +npm install @sentry/node -Since v8, this is the _legacy_ SDK, and it will most likely be completely removed before v8 is fully stable. It only -exists so that Meta-SDKs like `@sentry/nextjs` or `@sentry/sveltekit` can be migrated to the new `@sentry/node` -step-by-step. - -You should instead use [@sentry/node](./../node-experimental/). +# Or yarn +yarn add @sentry/node +``` ## Usage -To use this SDK, call `init(options)` as early as possible in the main entry module. This will initialize the SDK and -hook into the environment. Note that you can turn off almost all side effects using the respective options. Minimum -supported Node version is Node 14. - -```javascript -// CJS syntax -const Sentry = require('@sentry/node-experimental'); -// ESM syntax -import * as Sentry from '@sentry/node-experimental'; +```js +// CJS Syntax +const Sentry = require('@sentry/node'); +// ESM Syntax +import * as Sentry from '@sentry/node'; Sentry.init({ dsn: '__DSN__', @@ -41,28 +33,27 @@ Sentry.init({ }); ``` -To set context information or send manual events, use the exported functions of `@sentry/node-experimental`. Note that -these functions will not perform any action before you have called `init()`: +Note that it is necessary to initialize Sentry **before you import any package that may be instrumented by us**. -```javascript -// Set user information, as well as tags and further extras -Sentry.setExtra('battery', 0.7); -Sentry.setTag('user_mode', 'admin'); -Sentry.setUser({ id: '4711' }); +[More information on how to set up Sentry for Node in v8.](./../../docs/v8-node.md) -// Add a breadcrumb for future events -Sentry.addBreadcrumb({ - message: 'My Breadcrumb', - // ... -}); +### ESM Support -// Capture exceptions, messages or manual events -Sentry.captureMessage('Hello, world!'); -Sentry.captureException(new Error('Good bye')); -Sentry.captureEvent({ - message: 'Manual', - stacktrace: [ - // ... - ], -}); +Due to the way OpenTelemetry handles instrumentation, this only works out of the box for CommonJS (`require`) +applications. + +There is experimental support for running OpenTelemetry with ESM (`"type": "module"`): + +```bash +node --experimental-loader=@opentelemetry/instrumentation/hook.mjs ./app.js ``` + +You'll need to install `@opentelemetry/instrumentation` in your app to ensure this works. + +See +[OpenTelemetry Instrumentation Docs](https://github.com/open-telemetry/opentelemetry-js/tree/main/experimental/packages/opentelemetry-instrumentation#instrumentation-for-es-modules-in-nodejs-experimental) +for details on this - but note that this is a) experimental, and b) does not work with all integrations. + +## Links + +- [Official SDK Docs](https://docs.sentry.io/quickstart/) diff --git a/packages/node/package.json b/packages/node/package.json index cc7c0cd5843c..c28e026b92ab 100644 --- a/packages/node/package.json +++ b/packages/node/package.json @@ -1,7 +1,7 @@ { - "name": "@sentry/node-experimental", + "name": "@sentry/node", "version": "8.0.0-alpha.5", - "description": "The old version of Sentry SDK for Node.js, without OpenTelemetry support.", + "description": "Sentry Node SDK using OpenTelemetry for performance instrumentation", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/node", "author": "Sentry", @@ -42,22 +42,39 @@ "access": "public" }, "dependencies": { - "@sentry-internal/tracing": "8.0.0-alpha.5", + "@opentelemetry/api": "1.7.0", + "@opentelemetry/context-async-hooks": "1.21.0", + "@opentelemetry/core": "1.21.0", + "@opentelemetry/instrumentation": "0.48.0", + "@opentelemetry/instrumentation-express": "0.35.0", + "@opentelemetry/instrumentation-fastify": "0.33.0", + "@opentelemetry/instrumentation-graphql": "0.37.0", + "@opentelemetry/instrumentation-hapi": "0.34.0", + "@opentelemetry/instrumentation-http": "0.48.0", + "@opentelemetry/instrumentation-koa": "0.37.0", + "@opentelemetry/instrumentation-mongodb": "0.39.0", + "@opentelemetry/instrumentation-mongoose": "0.35.0", + "@opentelemetry/instrumentation-mysql": "0.35.0", + "@opentelemetry/instrumentation-mysql2": "0.35.0", + "@opentelemetry/instrumentation-nestjs-core": "0.34.0", + "@opentelemetry/instrumentation-pg": "0.38.0", + "@opentelemetry/resources": "1.21.0", + "@opentelemetry/sdk-trace-base": "1.21.0", + "@opentelemetry/semantic-conventions": "1.21.0", + "@prisma/instrumentation": "5.9.0", "@sentry/core": "8.0.0-alpha.5", + "@sentry/opentelemetry": "8.0.0-alpha.5", "@sentry/types": "8.0.0-alpha.5", "@sentry/utils": "8.0.0-alpha.5" }, "devDependencies": { - "@types/cookie": "0.5.2", - "@types/express": "^4.17.14", - "@types/lru-cache": "^5.1.0", - "@types/node": "14.18.63", - "express": "^4.17.1", - "nock": "^13.0.5", - "undici": "^5.21.0" + "@types/node": "^14.18.0" + }, + "optionalDependencies": { + "opentelemetry-instrumentation-fetch-node": "1.1.2" }, "scripts": { - "build": "run-s build:transpile build:types", + "build": "run-p build:transpile build:types", "build:dev": "yarn build", "build:transpile": "rollup -c rollup.npm.config.mjs", "build:types": "run-s build:types:core build:types:downlevel", @@ -72,23 +89,13 @@ "clean": "rimraf build coverage sentry-node-*.tgz", "fix": "eslint . --format stylish --fix", "lint": "eslint . --format stylish", - "test": "run-s test:jest test:express test:webpack test:release-health", - "test:express": "node test/manual/express-scope-separation/start.js", + "test": "yarn test:jest", "test:jest": "jest", - "test:release-health": "node test/manual/release-health/runner.js", - "test:webpack": "cd test/manual/webpack-async-context/ && yarn --silent --ignore-engines && node npm-build.js", "test:watch": "jest --watch", "yalc:publish": "ts-node ../../scripts/prepack.ts && yalc publish build --push --sig" }, "volta": { "extends": "../../package.json" }, - "madge": { - "detectiveOptions": { - "ts": { - "skipTypeImports": true - } - } - }, "sideEffects": false } diff --git a/packages/node/rollup.anr-worker.config.mjs b/packages/node/rollup.anr-worker.config.mjs index 48463d5763ee..bd3c1d4b825c 100644 --- a/packages/node/rollup.anr-worker.config.mjs +++ b/packages/node/rollup.anr-worker.config.mjs @@ -1,34 +1,31 @@ import { makeBaseBundleConfig } from '@sentry-internal/rollup-utils'; -function createAnrWorkerConfig(destDir, esm) { - return makeBaseBundleConfig({ - bundleType: 'node-worker', - entrypoints: ['src/integrations/anr/worker.ts'], - licenseTitle: '@sentry/node', - outputFileBase: () => 'worker-script.js', - packageSpecificConfig: { - output: { - dir: destDir, - sourcemap: false, - }, - plugins: [ - { - name: 'output-base64-worker-script', - renderChunk(code) { - const base64Code = Buffer.from(code).toString('base64'); - if (esm) { - return `export const base64WorkerScript = '${base64Code}';`; - } else { - return `exports.base64WorkerScript = '${base64Code}';`; - } - }, +export function createAnrWorkerCode() { + let base64Code; + + return { + workerRollupConfig: makeBaseBundleConfig({ + bundleType: 'node-worker', + entrypoints: ['src/integrations/anr/worker.ts'], + licenseTitle: '@sentry/node', + outputFileBase: () => 'worker-script.js', + packageSpecificConfig: { + output: { + dir: 'build/esm/integrations/anr', + sourcemap: false, }, - ], + plugins: [ + { + name: 'output-base64-worker-script', + renderChunk(code) { + base64Code = Buffer.from(code).toString('base64'); + }, + }, + ], + }, + }), + getBase64Code() { + return base64Code; }, - }); + }; } - -export const anrWorkerConfigs = [ - createAnrWorkerConfig('build/esm/integrations/anr', true), - createAnrWorkerConfig('build/cjs/integrations/anr', false), -]; diff --git a/packages/node/rollup.npm.config.mjs b/packages/node/rollup.npm.config.mjs index 88c90de4825f..17c0727d7eff 100644 --- a/packages/node/rollup.npm.config.mjs +++ b/packages/node/rollup.npm.config.mjs @@ -1,8 +1,32 @@ +import replace from '@rollup/plugin-replace'; import { makeBaseNPMConfig, makeNPMConfigVariants } from '@sentry-internal/rollup-utils'; -import { anrWorkerConfigs } from './rollup.anr-worker.config.mjs'; +import { createAnrWorkerCode } from './rollup.anr-worker.config.mjs'; + +const { workerRollupConfig, getBase64Code } = createAnrWorkerCode(); export default [ - ...makeNPMConfigVariants(makeBaseNPMConfig()), - // The ANR worker builds must come after the main build because they overwrite the worker-script.js file - ...anrWorkerConfigs, + // The worker needs to be built first since it's output is used in the main bundle. + workerRollupConfig, + ...makeNPMConfigVariants( + makeBaseNPMConfig({ + packageSpecificConfig: { + output: { + // set exports to 'named' or 'auto' so that rollup doesn't warn + exports: 'named', + // set preserveModules to false because we want to bundle everything into one file. + preserveModules: false, + }, + plugins: [ + replace({ + delimiters: ['###', '###'], + // removes some webpack warnings + preventAssignment: true, + values: { + base64WorkerScript: () => getBase64Code(), + }, + }), + ], + }, + }), + ), ]; diff --git a/packages/node-experimental/src/cron/index.ts b/packages/node/src/cron/index.ts similarity index 100% rename from packages/node-experimental/src/cron/index.ts rename to packages/node/src/cron/index.ts diff --git a/packages/node/src/index.ts b/packages/node/src/index.ts index 36b27086ef08..33bbcca54213 100644 --- a/packages/node/src/index.ts +++ b/packages/node/src/index.ts @@ -1,150 +1,127 @@ -export type { - Breadcrumb, - BreadcrumbHint, - PolymorphicRequest, - Request, - SdkInfo, - Event, - EventHint, - Exception, - Session, - SeverityLevel, - Span, - StackFrame, - Stacktrace, - Thread, - Transaction, - User, -} from '@sentry/types'; -export type { AddRequestDataToEventOptions, TransactionNamingScheme } from '@sentry/utils'; +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 { anrIntegration } from './integrations/anr'; + +export { expressIntegration, expressErrorHandler, setupExpressErrorHandler } from './integrations/tracing/express'; +export { fastifyIntegration, setupFastifyErrorHandler } from './integrations/tracing/fastify'; +export { graphqlIntegration } from './integrations/tracing/graphql'; +export { mongoIntegration } from './integrations/tracing/mongo'; +export { mongooseIntegration } from './integrations/tracing/mongoose'; +export { mysqlIntegration } from './integrations/tracing/mysql'; +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 { spotlightIntegration } from './integrations/spotlight'; + +export { init, getDefaultIntegrations } from './sdk/init'; +export { initOpenTelemetry } from './sdk/initOtel'; +export { getAutoPerformanceIntegrations } from './integrations/tracing'; +export { getSentryRelease, defaultStackParser } from './sdk/api'; +export { createGetModuleFromFilename } from './utils/module'; +export { makeNodeTransport } from './transports'; +export { NodeClient } from './sdk/client'; +export { cron } from './cron'; export type { NodeOptions } from './types'; export { - addEventProcessor, + addRequestDataToEvent, + DEFAULT_USER_INCLUDES, + extractRequestData, +} from '@sentry/utils'; + +// These are custom variants that need to be used instead of the core one +// As they have slightly different implementations +export { continueTrace } from '@sentry/opentelemetry'; + +export { addBreadcrumb, - addIntegration, - captureException, - captureEvent, - captureMessage, + isInitialized, + getGlobalScope, close, createTransport, flush, - // eslint-disable-next-line deprecation/deprecation - getCurrentHub, - getClient, - isInitialized, - getCurrentScope, - getGlobalScope, - getIsolationScope, Hub, - setCurrentClient, - Scope, SDK_VERSION, - setContext, - setExtra, - setExtras, - setTag, - setTags, - setUser, getSpanStatusFromHttpCode, setHttpStatus, - withScope, - withIsolationScope, captureCheckIn, withMonitor, - setMeasurement, - getActiveSpan, - getRootSpan, - startSpan, - startInactiveSpan, - startSpanManual, - withActiveSpan, - getSpanDescendants, - continueTrace, - parameterize, + requestDataIntegration, functionToStringIntegration, inboundFiltersIntegration, linkedErrorsIntegration, - requestDataIntegration, - metricsDefault as metrics, - startSession, - captureSession, - endSession, -} from '@sentry/core'; - -export { + addEventProcessor, + setContext, + setExtra, + setExtras, + setTag, + setTags, + setUser, SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE, -} from '@sentry/core'; - -export { autoDiscoverNodePerformanceMonitoringIntegrations } from './tracing'; - -export { NodeClient } from './client'; -export { makeNodeTransport } from './transports'; -export { - getDefaultIntegrations, - init, - defaultStackParser, - getSentryRelease, -} from './sdk'; -export { addRequestDataToEvent, DEFAULT_USER_INCLUDES, extractRequestData } from '@sentry/utils'; - -export { createGetModuleFromFilename } from './module'; - -import * as Handlers from './handlers'; -import * as NodeIntegrations from './integrations'; -import * as TracingIntegrations from './tracing/integrations'; - -// TODO: Deprecate this once we migrated tracing integrations -export const Integrations = { - ...NodeIntegrations, - ...TracingIntegrations, -}; - -export { + setCurrentClient, + Scope, + setMeasurement, + getSpanDescendants, + parameterize, + getClient, + // eslint-disable-next-line deprecation/deprecation + getCurrentHub, + getCurrentScope, + getIsolationScope, + withScope, + withIsolationScope, + captureException, + captureEvent, + captureMessage, captureConsoleIntegration, debugIntegration, dedupeIntegration, extraErrorDataIntegration, rewriteFramesIntegration, sessionTimingIntegration, + metricsDefault as metrics, + startSession, + captureSession, + endSession, + addIntegration, + startSpan, + startSpanManual, + startInactiveSpan, + getActiveSpan, + withActiveSpan, + getRootSpan, + spanToJSON, } from '@sentry/core'; -export { consoleIntegration } from './integrations/console'; -export { onUncaughtExceptionIntegration } from './integrations/onuncaughtexception'; -export { onUnhandledRejectionIntegration } from './integrations/onunhandledrejection'; -export { modulesIntegration } from './integrations/modules'; -export { contextLinesIntegration } from './integrations/contextlines'; -export { nodeContextIntegration } from './integrations/context'; -export { localVariablesIntegration } from './integrations/local-variables'; -export { spotlightIntegration } from './integrations/spotlight'; -export { anrIntegration } from './integrations/anr'; -export { hapiIntegration } from './integrations/hapi'; -// eslint-disable-next-line deprecation/deprecation -export { Undici, nativeNodeFetchintegration } from './integrations/undici'; -// eslint-disable-next-line deprecation/deprecation -export { Http, httpIntegration } from './integrations/http'; - -// TODO(v8): Remove all of these exports. They were part of a hotfix #10339 where we produced wrong .d.ts files because we were packing packages inside the /build folder. -export type { LocalVariablesIntegrationOptions } from './integrations/local-variables/common'; -export type { DebugSession } from './integrations/local-variables/local-variables-sync'; -export type { AnrIntegrationOptions } from './integrations/anr/common'; -// --- - -export { Handlers }; - -export { hapiErrorPlugin } from './integrations/hapi'; - -import { instrumentCron } from './cron/cron'; -import { instrumentNodeCron } from './cron/node-cron'; -import { instrumentNodeSchedule } from './cron/node-schedule'; - -/** Methods to instrument cron libraries for Sentry check-ins */ -export const cron = { - instrumentCron, - instrumentNodeCron, - instrumentNodeSchedule, -}; +export type { + Breadcrumb, + BreadcrumbHint, + PolymorphicRequest, + Request, + SdkInfo, + Event, + EventHint, + Exception, + Session, + SeverityLevel, + StackFrame, + Stacktrace, + Thread, + Transaction, + User, + Span, +} from '@sentry/types'; diff --git a/packages/node/src/integrations/anr/common.ts b/packages/node/src/integrations/anr/common.ts index 5617871ccb24..e2e50fae4179 100644 --- a/packages/node/src/integrations/anr/common.ts +++ b/packages/node/src/integrations/anr/common.ts @@ -37,6 +37,7 @@ export interface WorkerStartData extends AnrIntegrationOptions { debug: boolean; sdkMetadata: SdkMetadata; dsn: DsnComponents; + tunnel: string | undefined; release: string | undefined; environment: string; dist: string | undefined; diff --git a/packages/node/src/integrations/anr/index.ts b/packages/node/src/integrations/anr/index.ts index 7e0de1d0badc..7dbe9e905cb4 100644 --- a/packages/node/src/integrations/anr/index.ts +++ b/packages/node/src/integrations/anr/index.ts @@ -1,37 +1,41 @@ -// TODO (v8): This import can be removed once we only support Node with global URL -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 type { NodeClient } from '../../client'; +import { defineIntegration, mergeScopeData } from '@sentry/core'; +import type { Contexts, Event, EventHint, Integration, IntegrationFn, ScopeData } from '@sentry/types'; +import { GLOBAL_OBJ, logger } from '@sentry/utils'; +import * as inspector from 'inspector'; +import { Worker } from 'worker_threads'; +import { getCurrentScope, getGlobalScope, getIsolationScope } from '../..'; import { 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'); +function globalWithScopeFetchFn(): typeof GLOBAL_OBJ & { __SENTRY_GET_SCOPES__?: () => ScopeData } { + return GLOBAL_OBJ; +} + +/** Fetches merged scope data */ +function getScopeData(): ScopeData { + const scope = getGlobalScope().getScopeData(); + mergeScopeData(scope, getIsolationScope().getScopeData()); + mergeScopeData(scope, getCurrentScope().getScopeData()); + + // We remove attachments because they likely won't serialize well as json + scope.attachments = []; + // We can't serialize event processor functions + scope.eventProcessors = []; + + return scope; } /** - * Gets contexts by calling all event processors. This relies on being called after all integrations are setup + * Gets contexts by calling all event processors. This shouldn't be called until all integrations are setup */ async function getContexts(client: NodeClient): Promise { let event: Event | null = { message: 'ANR' }; @@ -45,40 +49,76 @@ async function getContexts(client: NodeClient): Promise { return event?.contexts || {}; } -interface InspectorApi { - open: (port: number) => void; - url: () => string | undefined; -} - const INTEGRATION_NAME = 'Anr'; +type AnrInternal = { startWorker: () => void; stopWorker: () => void }; + const _anrIntegration = ((options: Partial = {}) => { + if (NODE_VERSION.major < 16 || (NODE_VERSION.major === 16 && NODE_VERSION.minor < 17)) { + throw new Error('ANR detection requires Node 16.17.0 or later'); + } + + let worker: Promise<() => void> | undefined; + let client: NodeClient | undefined; + + // Hookup the scope fetch function to the global object so that it can be called from the worker thread via the + // debugger when it pauses + const gbl = globalWithScopeFetchFn(); + gbl.__SENTRY_GET_SCOPES__ = getScopeData; + return { name: INTEGRATION_NAME, - setup(client: NodeClient) { - if (NODE_VERSION.major < 16 || (NODE_VERSION.major === 16 && NODE_VERSION.minor < 17)) { - throw new Error('ANR detection requires Node 16.17.0 or later'); + startWorker: () => { + if (worker) { + return; + } + + if (client) { + worker = _startWorker(client, options); + } + }, + stopWorker: () => { + if (worker) { + // eslint-disable-next-line @typescript-eslint/no-floating-promises + worker.then(stop => { + stop(); + worker = undefined; + }); } + }, + setup(initClient: NodeClient) { + client = initClient; - // setImmediate is used to ensure that all other integrations have been setup - setImmediate(() => _startWorker(client, options)); + // setImmediate is used to ensure that all other integrations have had their setup called first. + // This allows us to call into all integrations to fetch the full context + setImmediate(() => this.startWorker()); }, - }; + } as Integration & AnrInternal; }) satisfies IntegrationFn; -export const anrIntegration = defineIntegration(_anrIntegration); +type AnrReturn = (options?: Partial) => Integration & AnrInternal; + +export const anrIntegration = defineIntegration(_anrIntegration) as AnrReturn; /** * Starts the ANR worker thread + * + * @returns A function to stop the worker */ -async function _startWorker(client: NodeClient, _options: Partial): Promise { - const contexts = await getContexts(client); +async function _startWorker( + client: NodeClient, + integrationOptions: Partial, +): Promise<() => void> { const dsn = client.getDsn(); if (!dsn) { - return; + return () => { + // + }; } + const contexts = await getContexts(client); + // These will not be accurate if sent later from the worker thread delete contexts.app?.app_memory; delete contexts.device?.free_memory; @@ -93,28 +133,25 @@ async function _startWorker(client: NodeClient, _options: Partial { + // eslint-disable-next-line @typescript-eslint/no-floating-promises + worker.terminate(); + clearInterval(timer); + }; } diff --git a/packages/node/src/integrations/anr/worker-script.ts b/packages/node/src/integrations/anr/worker-script.ts index 16394eaacfe1..c70323e0fc50 100644 --- a/packages/node/src/integrations/anr/worker-script.ts +++ b/packages/node/src/integrations/anr/worker-script.ts @@ -1,2 +1,2 @@ -// This file is a placeholder that gets overwritten in the build directory. -export const base64WorkerScript = ''; +// This string is a placeholder that gets overwritten with the worker code. +export const base64WorkerScript = '###base64WorkerScript###'; diff --git a/packages/node/src/integrations/anr/worker.ts b/packages/node/src/integrations/anr/worker.ts index a8b984b48379..21bdcbbb0631 100644 --- a/packages/node/src/integrations/anr/worker.ts +++ b/packages/node/src/integrations/anr/worker.ts @@ -1,11 +1,12 @@ import { + applyScopeDataToEvent, createEventEnvelope, createSessionEnvelope, getEnvelopeEndpointWithUrlEncodedAuth, makeSession, updateSession, } from '@sentry/core'; -import type { Event, Session, StackFrame, TraceContext } from '@sentry/types'; +import type { Event, ScopeData, Session, StackFrame } from '@sentry/types'; import { callFrameToStackFrame, normalizeUrlToBase, @@ -16,12 +17,11 @@ import { import { Session as InspectorSession } from 'inspector'; import { parentPort, workerData } from 'worker_threads'; -import { createGetModuleFromFilename } from '../../module'; import { makeNodeTransport } from '../../transports'; +import { createGetModuleFromFilename } from '../../utils/module'; import type { WorkerStartData } from './common'; type VoidFunction = () => void; -type InspectorSessionNodeV12 = InspectorSession & { connectToMainThread: VoidFunction }; const options: WorkerStartData = workerData; let session: Session | undefined; @@ -34,7 +34,7 @@ function log(msg: string): void { } } -const url = getEnvelopeEndpointWithUrlEncodedAuth(options.dsn); +const url = getEnvelopeEndpointWithUrlEncodedAuth(options.dsn, options.tunnel, options.sdkMetadata.sdk); const transport = makeNodeTransport({ url, recordDroppedEvent: () => { @@ -48,7 +48,7 @@ async function sendAbnormalSession(): Promise { log('Sending abnormal session'); updateSession(session, { status: 'abnormal', abnormal_mechanism: 'anr_foreground' }); - const envelope = createSessionEnvelope(session, options.dsn, options.sdkMetadata); + const envelope = createSessionEnvelope(session, options.dsn, options.sdkMetadata, options.tunnel); // Log the envelope so to aid in testing log(JSON.stringify(envelope)); @@ -87,7 +87,23 @@ function prepareStackFrames(stackFrames: StackFrame[] | undefined): StackFrame[] return strippedFrames; } -async function sendAnrEvent(frames?: StackFrame[], traceContext?: TraceContext): Promise { +function applyScopeToEvent(event: Event, scope: ScopeData): void { + applyScopeDataToEvent(event, scope); + + if (!event.contexts?.trace) { + const { traceId, spanId, parentSpanId } = scope.propagationContext; + event.contexts = { + trace: { + trace_id: traceId, + span_id: spanId, + parent_span_id: parentSpanId, + }, + ...event.contexts, + }; + } +} + +async function sendAnrEvent(frames?: StackFrame[], scope?: ScopeData): Promise { if (hasSentAnrEvent) { return; } @@ -100,7 +116,7 @@ async function sendAnrEvent(frames?: StackFrame[], traceContext?: TraceContext): const event: Event = { event_id: uuid4(), - contexts: { ...options.contexts, trace: traceContext }, + contexts: options.contexts, release: options.release, environment: options.environment, dist: options.dist, @@ -120,15 +136,19 @@ async function sendAnrEvent(frames?: StackFrame[], traceContext?: TraceContext): tags: options.staticTags, }; - const envelope = createEventEnvelope(event, options.dsn, options.sdkMetadata); - // Log the envelope so to aid in testing + if (scope) { + applyScopeToEvent(event, scope); + } + + const envelope = createEventEnvelope(event, options.dsn, options.sdkMetadata, options.tunnel); + // Log the envelope to aid in testing log(JSON.stringify(envelope)); await transport.send(envelope); await transport.flush(2000); - // Delay for 5 seconds so that stdio can flush in the main event loop ever restarts. - // This is mainly for the benefit of logging/debugging issues. + // Delay for 5 seconds so that stdio can flush if the main event loop ever restarts. + // This is mainly for the benefit of logging or debugging. setTimeout(() => { process.exit(0); }, 5_000); @@ -139,7 +159,7 @@ let debuggerPause: VoidFunction | undefined; if (options.captureStackTrace) { log('Connecting to debugger'); - const session = new InspectorSession() as InspectorSessionNodeV12; + const session = new InspectorSession(); session.connectToMainThread(); log('Connected to debugger'); @@ -172,20 +192,23 @@ if (options.captureStackTrace) { 'Runtime.evaluate', { // Grab the trace context from the current scope - expression: - 'const ctx = __SENTRY__.acs?.getCurrentScope().getPropagationContext() || {}; ctx.traceId + "-" + ctx.spanId + "-" + ctx.parentSpanId', + expression: 'global.__SENTRY_GET_SCOPES__();', // Don't re-trigger the debugger if this causes an error silent: true, + // Serialize the result to json otherwise only primitives are supported + returnByValue: true, }, - (_, param) => { - const traceId = param && param.result ? (param.result.value as string) : '--'; - const [trace_id, span_id, parent_span_id] = traceId.split('-') as (string | undefined)[]; + (err, param) => { + if (err) { + log(`Error executing script: '${err.message}'`); + } + + const scopes = param && param.result ? (param.result.value as ScopeData) : undefined; session.post('Debugger.resume'); session.post('Debugger.disable'); - const context = trace_id?.length && span_id?.length ? { trace_id, span_id, parent_span_id } : undefined; - sendAnrEvent(stackFrames, context).then(null, () => { + sendAnrEvent(stackFrames, scopes).then(null, () => { log('Sending ANR event failed.'); }); }, diff --git a/packages/node/src/integrations/console.ts b/packages/node/src/integrations/console.ts index 5a185b8fcee1..0b3d27fe8510 100644 --- a/packages/node/src/integrations/console.ts +++ b/packages/node/src/integrations/console.ts @@ -1,6 +1,6 @@ import * as util from 'util'; -import { addBreadcrumb, convertIntegrationFnToClass, defineIntegration, getClient } from '@sentry/core'; -import type { Client, Integration, IntegrationClass, IntegrationFn } from '@sentry/types'; +import { addBreadcrumb, defineIntegration, getClient } from '@sentry/core'; +import type { IntegrationFn } from '@sentry/types'; import { addConsoleInstrumentationHandler, severityLevelFromString } from '@sentry/utils'; const INTEGRATION_NAME = 'Console'; @@ -30,16 +30,7 @@ const _consoleIntegration = (() => { }; }) satisfies IntegrationFn; -export const consoleIntegration = defineIntegration(_consoleIntegration); - /** - * Console module integration. - * @deprecated Use `consoleIntegration()` instead. + * Capture console logs as breadcrumbs. */ -// eslint-disable-next-line deprecation/deprecation -export const Console = convertIntegrationFnToClass(INTEGRATION_NAME, consoleIntegration) as IntegrationClass< - Integration & { setup: (client: Client) => void } ->; - -// eslint-disable-next-line deprecation/deprecation -export type Console = typeof Console; +export const consoleIntegration = defineIntegration(_consoleIntegration); diff --git a/packages/node/src/integrations/context.ts b/packages/node/src/integrations/context.ts index fa5184204bf2..c33d97e79044 100644 --- a/packages/node/src/integrations/context.ts +++ b/packages/node/src/integrations/context.ts @@ -1,10 +1,9 @@ -/* 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'; -import { convertIntegrationFnToClass, defineIntegration } from '@sentry/core'; +import { defineIntegration } from '@sentry/core'; import type { AppContext, CloudResourceContext, @@ -12,13 +11,10 @@ import type { CultureContext, DeviceContext, Event, - Integration, - IntegrationClass, IntegrationFn, OsContext, } from '@sentry/types'; -// TODO: Required until we drop support for Node v8 export const readFileAsync = promisify(readFile); export const readDirAsync = promisify(readdir); @@ -108,27 +104,10 @@ const _nodeContextIntegration = ((options: ContextOptions = {}) => { }; }) satisfies IntegrationFn; -export const nodeContextIntegration = defineIntegration(_nodeContextIntegration); - /** - * Add node modules / packages to the event. - * @deprecated Use `nodeContextIntegration()` instead. + * Capture context about the environment and the device that the client is running on, to events. */ -// eslint-disable-next-line deprecation/deprecation -export const Context = convertIntegrationFnToClass(INTEGRATION_NAME, nodeContextIntegration) as IntegrationClass< - Integration & { processEvent: (event: Event) => Promise } -> & { - new (options?: { - app?: boolean; - os?: boolean; - device?: { cpu?: boolean; memory?: boolean } | boolean; - culture?: boolean; - cloudResource?: boolean; - }): Integration; -}; - -// eslint-disable-next-line deprecation/deprecation -export type Context = typeof Context; +export const nodeContextIntegration = defineIntegration(_nodeContextIntegration); /** * 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 7c367f51a970..3755e164e5ea 100644 --- a/packages/node/src/integrations/contextlines.ts +++ b/packages/node/src/integrations/contextlines.ts @@ -1,4 +1,4 @@ -import { readFile } from 'fs'; +import { promises } from 'fs'; import { defineIntegration } from '@sentry/core'; import type { Event, IntegrationFn, StackFrame } from '@sentry/types'; import { LRUMap, addContextToFrame } from '@sentry/utils'; @@ -7,15 +7,7 @@ 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 { - return new Promise((resolve, reject) => { - readFile(path, 'utf8', (err, data) => { - if (err) reject(err); - else resolve(data); - }); - }); -} +const readFileAsync = promises.readFile; /** * Resets the file cache. Exists for testing purposes. @@ -35,7 +27,8 @@ interface ContextLinesOptions { frameContextLines?: number; } -const _contextLinesIntegration = ((options: ContextLinesOptions = {}) => { +/** Exported only for tests, as a type-safe variant. */ +export const _contextLinesIntegration = ((options: ContextLinesOptions = {}) => { const contextLines = options.frameContextLines !== undefined ? options.frameContextLines : DEFAULT_LINES_OF_CONTEXT; return { @@ -46,6 +39,9 @@ const _contextLinesIntegration = ((options: ContextLinesOptions = {}) => { }; }) satisfies IntegrationFn; +/** + * Capture the lines before and after the frame's context. + */ export const contextLinesIntegration = defineIntegration(_contextLinesIntegration); async function addSourceContext(event: Event, contextLines: number): Promise { @@ -139,7 +135,7 @@ async function _readSourceFile(filename: string): Promise { // If we made it to here, it means that our file is not cache nor marked as failed, so attempt to read it let content: string[] | null = null; try { - const rawFileContents = await readTextFileAsync(filename); + const rawFileContents = await readFileAsync(filename, 'utf-8'); content = rawFileContents.split('\n'); } catch (_) { // if we fail, we will mark the file as null in the cache and short circuit next time we try to read it diff --git a/packages/node/src/integrations/http.ts b/packages/node/src/integrations/http.ts index 9eb7deab5318..ed93ebacdaa5 100644 --- a/packages/node/src/integrations/http.ts +++ b/packages/node/src/integrations/http.ts @@ -1,468 +1,155 @@ -/* eslint-disable max-lines */ -import type * as http from 'http'; -import type * as https from 'https'; -import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, startInactiveSpan } from '@sentry/core'; -import { defineIntegration, getIsolationScope, hasTracingEnabled } from '@sentry/core'; -import { - addBreadcrumb, - getClient, - getCurrentScope, - getDynamicSamplingContextFromClient, - getDynamicSamplingContextFromSpan, - isSentryRequestUrl, - setHttpStatus, - spanToJSON, - spanToTraceHeader, -} from '@sentry/core'; -import type { - ClientOptions, - Integration, - IntegrationFn, - SanitizedRequestData, - TracePropagationTargets, -} from '@sentry/types'; -import { - LRUMap, - dropUndefinedKeys, - dynamicSamplingContextToSentryBaggageHeader, - fill, - generateSentryTraceHeader, - logger, - stringMatchesSomePattern, -} from '@sentry/utils'; - -import type { NodeClient } from '../client'; -import { DEBUG_BUILD } from '../debug-build'; -import { NODE_VERSION } from '../nodeVersion'; -import type { NodeClientOptions } from '../types'; -import type { RequestMethod, RequestMethodArgs, RequestOptions } from './utils/http'; -import { cleanSpanName, extractRawUrl, extractUrl, normalizeRequestArgs } from './utils/http'; - -interface TracingOptions { - /** - * List of strings/regex controlling to which outgoing requests - * the SDK will attach tracing headers. - * - * By default the SDK will attach those headers to all outgoing - * requests. If this option is provided, the SDK will match the - * request URL of outgoing requests against the items in this - * array, and only attach tracing headers if a match was found. - * - * @deprecated Use top level `tracePropagationTargets` option instead. - * This option will be removed in v8. - * - * ``` - * Sentry.init({ - * tracePropagationTargets: ['api.site.com'], - * }) - */ - tracePropagationTargets?: TracePropagationTargets; - - /** - * Function determining whether or not to create spans to track outgoing requests to the given URL. - * By default, spans will be created for all outgoing requests. - */ - shouldCreateSpanForRequest?: (url: string) => boolean; - - /** - * This option is just for compatibility with v7. - * In v8, this will be the default behavior. - */ - enableIfHasTracingEnabled?: boolean; -} +import type { ServerResponse } from 'http'; +import type { Span } from '@opentelemetry/api'; +import { SpanKind } from '@opentelemetry/api'; +import { registerInstrumentations } from '@opentelemetry/instrumentation'; +import { HttpInstrumentation } from '@opentelemetry/instrumentation-http'; + +import { addBreadcrumb, defineIntegration, getIsolationScope, isSentryRequestUrl } from '@sentry/core'; +import { _INTERNAL, getClient, getSpanKind } from '@sentry/opentelemetry'; +import type { IntegrationFn } from '@sentry/types'; + +import type { NodeClient } from '../sdk/client'; +import { setIsolationScope } from '../sdk/scope'; +import type { HTTPModuleRequestIncomingMessage } from '../transports/http-module'; +import { addOriginToSpan } from '../utils/addOriginToSpan'; +import { getRequestUrl } from '../utils/getRequestUrl'; interface HttpOptions { /** - * Whether breadcrumbs should be recorded for requests + * Whether breadcrumbs should be recorded for requests. * Defaults to true */ breadcrumbs?: boolean; /** - * Whether tracing spans should be created for requests - * Defaults to false + * Do not capture spans or breadcrumbs for outgoing HTTP requests to URLs where the given callback returns `true`. + * This controls both span & breadcrumb creation - spans will be non recording if tracing is disabled. */ - tracing?: TracingOptions | boolean; -} + ignoreOutgoingRequests?: (url: string) => boolean; -/* These are the newer options for `httpIntegration`. */ -interface HttpIntegrationOptions { /** - * Whether breadcrumbs should be recorded for requests - * Defaults to true. + * Do not capture spans or breadcrumbs for incoming HTTP requests to URLs where the given callback returns `true`. + * This controls both span & breadcrumb creation - spans will be non recording if tracing is disabled. */ - breadcrumbs?: boolean; - - /** - * Whether tracing spans should be created for requests - * If not set, this will be enabled/disabled based on if tracing is enabled. - */ - tracing?: boolean; - - /** - * Function determining whether or not to create spans to track outgoing requests to the given URL. - * By default, spans will be created for all outgoing requests. - */ - shouldCreateSpanForRequest?: (url: string) => boolean; + ignoreIncomingRequests?: (url: string) => boolean; } -const _httpIntegration = ((options: HttpIntegrationOptions = {}) => { - const { breadcrumbs, tracing, shouldCreateSpanForRequest } = options; - - const convertedOptions: HttpOptions = { - breadcrumbs, - tracing: - tracing === false - ? false - : dropUndefinedKeys({ - // If tracing is forced to `true`, we don't want to set `enableIfHasTracingEnabled` - enableIfHasTracingEnabled: tracing === true ? undefined : true, - shouldCreateSpanForRequest, - }), - }; - // eslint-disable-next-line deprecation/deprecation - return new Http(convertedOptions) as unknown as Integration; -}) satisfies IntegrationFn; - -/** - * The http module integration instruments Node's internal http module. It creates breadcrumbs, spans for outgoing - * http requests, and attaches trace data when tracing is enabled via its `tracing` option. - * - * By default, this will always create breadcrumbs, and will create spans if tracing is enabled. - */ -export const httpIntegration = defineIntegration(_httpIntegration); - -/** - * The http integration instruments Node's internal http and https modules. - * It creates breadcrumbs and spans for outgoing HTTP requests which will be attached to the currently active span. - * - * @deprecated Use `httpIntegration()` instead. - */ -export class Http implements Integration { - /** - * @inheritDoc - */ - public static id: string = 'Http'; +const _httpIntegration = ((options: HttpOptions = {}) => { + const _breadcrumbs = typeof options.breadcrumbs === 'undefined' ? true : options.breadcrumbs; + const _ignoreOutgoingRequests = options.ignoreOutgoingRequests; + const _ignoreIncomingRequests = options.ignoreIncomingRequests; + + return { + name: 'Http', + setupOnce() { + const instrumentations = [ + new HttpInstrumentation({ + ignoreOutgoingRequestHook: request => { + const url = getRequestUrl(request); + + if (!url) { + return false; + } - /** - * @inheritDoc - */ - // eslint-disable-next-line deprecation/deprecation - public name: string = Http.id; + if (isSentryRequestUrl(url, getClient())) { + return true; + } - private readonly _breadcrumbs: boolean; - private readonly _tracing: TracingOptions | undefined; + if (_ignoreOutgoingRequests && _ignoreOutgoingRequests(url)) { + return true; + } - /** - * @inheritDoc - */ - public constructor(options: HttpOptions = {}) { - this._breadcrumbs = typeof options.breadcrumbs === 'undefined' ? true : options.breadcrumbs; - this._tracing = !options.tracing ? undefined : options.tracing === true ? {} : options.tracing; - } + return false; + }, - /** - * @inheritDoc - */ - public setupOnce(): void { - const clientOptions = getClient()?.getOptions(); + ignoreIncomingRequestHook: request => { + const url = getRequestUrl(request); - // If `tracing` is not explicitly set, we default this based on whether or not tracing is enabled. - // But for compatibility, we only do that if `enableIfHasTracingEnabled` is set. - const shouldCreateSpans = _shouldCreateSpans(this._tracing, clientOptions); + const method = request.method?.toUpperCase(); + // We do not capture OPTIONS/HEAD requests as transactions + if (method === 'OPTIONS' || method === 'HEAD') { + return true; + } - // No need to instrument if we don't want to track anything - if (!this._breadcrumbs && !shouldCreateSpans) { - return; - } + if (_ignoreIncomingRequests && _ignoreIncomingRequests(url)) { + return true; + } - const shouldCreateSpanForRequest = _getShouldCreateSpanForRequest(shouldCreateSpans, this._tracing, clientOptions); + return false; + }, - // eslint-disable-next-line deprecation/deprecation - const tracePropagationTargets = clientOptions?.tracePropagationTargets || this._tracing?.tracePropagationTargets; + requireParentforOutgoingSpans: true, + requireParentforIncomingSpans: false, + requestHook: (span, req) => { + _updateSpan(span); - // eslint-disable-next-line @typescript-eslint/no-var-requires - const httpModule = require('http'); - const wrappedHttpHandlerMaker = _createWrappedRequestMethodFactory( - httpModule, - this._breadcrumbs, - shouldCreateSpanForRequest, - tracePropagationTargets, - ); - fill(httpModule, 'get', wrappedHttpHandlerMaker); - fill(httpModule, 'request', wrappedHttpHandlerMaker); + // Update the isolation scope, isolate this request + if (getSpanKind(span) === SpanKind.SERVER) { + const isolationScope = getIsolationScope().clone(); + isolationScope.setSDKProcessingMetadata({ request: req }); - // NOTE: Prior to Node 9, `https` used internals of `http` module, thus we don't patch it. - // If we do, we'd get double breadcrumbs and double spans for `https` calls. - // It has been changed in Node 9, so for all versions equal and above, we patch `https` separately. - if (NODE_VERSION.major > 8) { - // eslint-disable-next-line @typescript-eslint/no-var-requires - const httpsModule = require('node:https'); - const wrappedHttpsHandlerMaker = _createWrappedRequestMethodFactory( - httpsModule, - this._breadcrumbs, - shouldCreateSpanForRequest, - tracePropagationTargets, - ); - fill(httpsModule, 'get', wrappedHttpsHandlerMaker); - fill(httpsModule, 'request', wrappedHttpsHandlerMaker); - } - } -} + const client = getClient(); + if (client && client.getOptions().autoSessionTracking) { + isolationScope.setRequestSession({ status: 'ok' }); + } + setIsolationScope(isolationScope); + } + }, + responseHook: (span, res) => { + if (_breadcrumbs) { + _addRequestBreadcrumb(span, res); + } -// for ease of reading below -type OriginalRequestMethod = RequestMethod; -type WrappedRequestMethod = RequestMethod; -type WrappedRequestMethodFactory = (original: OriginalRequestMethod) => WrappedRequestMethod; + const client = getClient(); + if (client && client.getOptions().autoSessionTracking) { + setImmediate(() => { + client['_captureRequestSession'](); + }); + } + }, + }), + ]; + + registerInstrumentations({ + instrumentations, + }); + }, + }; +}) satisfies IntegrationFn; /** - * Function which creates a function which creates wrapped versions of internal `request` and `get` calls within `http` - * and `https` modules. (NB: Not a typo - this is a creator^2!) - * - * @param breadcrumbsEnabled Whether or not to record outgoing requests as breadcrumbs - * @param tracingEnabled Whether or not to record outgoing requests as tracing spans - * - * @returns A function which accepts the exiting handler and returns a wrapped handler + * The http integration instruments Node's internal http and https modules. + * It creates breadcrumbs and spans for outgoing HTTP requests which will be attached to the currently active span. */ -function _createWrappedRequestMethodFactory( - httpModule: typeof http | typeof https, - breadcrumbsEnabled: boolean, - shouldCreateSpanForRequest: ((url: string) => boolean) | undefined, - tracePropagationTargets: TracePropagationTargets | undefined, -): WrappedRequestMethodFactory { - // We're caching results so we don't have to recompute regexp every time we create a request. - const createSpanUrlMap = new LRUMap(100); - const headersUrlMap = new LRUMap(100); - - const shouldCreateSpan = (url: string): boolean => { - if (shouldCreateSpanForRequest === undefined) { - return true; - } - - const cachedDecision = createSpanUrlMap.get(url); - if (cachedDecision !== undefined) { - return cachedDecision; - } - - const decision = shouldCreateSpanForRequest(url); - createSpanUrlMap.set(url, decision); - return decision; - }; - - const shouldAttachTraceData = (url: string): boolean => { - if (tracePropagationTargets === undefined) { - return true; - } - - const cachedDecision = headersUrlMap.get(url); - if (cachedDecision !== undefined) { - return cachedDecision; - } - - const decision = stringMatchesSomePattern(url, tracePropagationTargets); - headersUrlMap.set(url, decision); - return decision; - }; - - /** - * Captures Breadcrumb based on provided request/response pair - */ - function addRequestBreadcrumb( - event: string, - requestSpanData: SanitizedRequestData, - req: http.ClientRequest, - res?: http.IncomingMessage, - ): void { - if (!getClient()?.getIntegrationByName('Http')) { - return; - } - - addBreadcrumb( - { - category: 'http', - data: { - status_code: res && res.statusCode, - ...requestSpanData, - }, - type: 'http', - }, - { - event, - request: req, - response: res, - }, - ); - } - - return function wrappedRequestMethodFactory(originalRequestMethod: OriginalRequestMethod): WrappedRequestMethod { - return function wrappedMethod(this: unknown, ...args: RequestMethodArgs): http.ClientRequest { - const requestArgs = normalizeRequestArgs(httpModule, args); - const requestOptions = requestArgs[0]; - const rawRequestUrl = extractRawUrl(requestOptions); - const requestUrl = extractUrl(requestOptions); - const client = getClient(); - - // we don't want to record requests to Sentry as either breadcrumbs or spans, so just use the original method - if (isSentryRequestUrl(requestUrl, client)) { - return originalRequestMethod.apply(httpModule, requestArgs); - } - - const scope = getCurrentScope(); - const isolationScope = getIsolationScope(); - - const attributes = getRequestSpanData(requestUrl, requestOptions); - - const requestSpan = shouldCreateSpan(rawRequestUrl) - ? startInactiveSpan({ - onlyIfParent: true, - op: 'http.client', - name: `${attributes['http.method']} ${attributes.url}`, - attributes: { - ...attributes, - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.node.http', - }, - }) - : undefined; - - if (client && shouldAttachTraceData(rawRequestUrl)) { - const { traceId, spanId, sampled, dsc } = { - ...isolationScope.getPropagationContext(), - ...scope.getPropagationContext(), - }; - - const sentryTraceHeader = requestSpan - ? spanToTraceHeader(requestSpan) - : generateSentryTraceHeader(traceId, spanId, sampled); - - const sentryBaggageHeader = dynamicSamplingContextToSentryBaggageHeader( - dsc || - (requestSpan - ? getDynamicSamplingContextFromSpan(requestSpan) - : getDynamicSamplingContextFromClient(traceId, client)), - ); - - addHeadersToRequestOptions(requestOptions, requestUrl, sentryTraceHeader, sentryBaggageHeader); - } else { - DEBUG_BUILD && - logger.log( - `[Tracing] Not adding sentry-trace header to outgoing request (${requestUrl}) due to mismatching tracePropagationTargets option.`, - ); - } - - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - return originalRequestMethod - .apply(httpModule, requestArgs) - .once('response', function (this: http.ClientRequest, res: http.IncomingMessage): void { - // eslint-disable-next-line @typescript-eslint/no-this-alias - const req = this; - if (breadcrumbsEnabled) { - addRequestBreadcrumb('response', attributes, req, res); - } - if (requestSpan) { - if (res.statusCode) { - setHttpStatus(requestSpan, res.statusCode); - } - requestSpan.updateName(cleanSpanName(spanToJSON(requestSpan).description || '', requestOptions, req) || ''); - requestSpan.end(); - } - }) - .once('error', function (this: http.ClientRequest): void { - // eslint-disable-next-line @typescript-eslint/no-this-alias - const req = this; +export const httpIntegration = defineIntegration(_httpIntegration); - if (breadcrumbsEnabled) { - addRequestBreadcrumb('error', attributes, req); - } - if (requestSpan) { - setHttpStatus(requestSpan, 500); - requestSpan.updateName(cleanSpanName(spanToJSON(requestSpan).description || '', requestOptions, req) || ''); - requestSpan.end(); - } - }); - }; - }; +/** Update the span with data we need. */ +function _updateSpan(span: Span): void { + addOriginToSpan(span, 'auto.http.otel.http'); } -function addHeadersToRequestOptions( - requestOptions: RequestOptions, - requestUrl: string, - sentryTraceHeader: string, - sentryBaggageHeader: string | undefined, -): void { - // Don't overwrite sentry-trace and baggage header if it's already set. - const headers = requestOptions.headers || {}; - if (headers['sentry-trace']) { +/** Add a breadcrumb for outgoing requests. */ +function _addRequestBreadcrumb(span: Span, response: HTTPModuleRequestIncomingMessage | ServerResponse): void { + if (getSpanKind(span) !== SpanKind.CLIENT) { return; } - DEBUG_BUILD && - logger.log(`[Tracing] Adding sentry-trace header ${sentryTraceHeader} to outgoing request to "${requestUrl}": `); - - requestOptions.headers = { - ...requestOptions.headers, - 'sentry-trace': sentryTraceHeader, - // Setting a header to `undefined` will crash in node so we only set the baggage header when it's defined - ...(sentryBaggageHeader && - sentryBaggageHeader.length > 0 && { baggage: normalizeBaggageHeader(requestOptions, sentryBaggageHeader) }), - }; -} - -function getRequestSpanData(requestUrl: string, requestOptions: RequestOptions): SanitizedRequestData { - const method = requestOptions.method || 'GET'; - const data: SanitizedRequestData = { - url: requestUrl, - 'http.method': method, - }; - if (requestOptions.hash) { - // strip leading "#" - data['http.fragment'] = requestOptions.hash.substring(1); - } - if (requestOptions.search) { - // strip leading "?" - data['http.query'] = requestOptions.search.substring(1); - } - return data; -} - -function normalizeBaggageHeader( - requestOptions: RequestOptions, - sentryBaggageHeader: string | undefined, -): string | string[] | undefined { - if (!requestOptions.headers || !requestOptions.headers.baggage) { - return sentryBaggageHeader; - } else if (!sentryBaggageHeader) { - return requestOptions.headers.baggage as string | string[]; - } else if (Array.isArray(requestOptions.headers.baggage)) { - return [...requestOptions.headers.baggage, sentryBaggageHeader]; - } - // Type-cast explanation: - // Technically this the following could be of type `(number | string)[]` but for the sake of simplicity - // we say this is undefined behaviour, since it would not be baggage spec conform if the user did this. - return [requestOptions.headers.baggage, sentryBaggageHeader] as string[]; -} - -/** Exported for tests only. */ -export function _shouldCreateSpans( - tracingOptions: TracingOptions | undefined, - clientOptions: Partial | undefined, -): boolean { - return tracingOptions === undefined - ? false - : tracingOptions.enableIfHasTracingEnabled - ? hasTracingEnabled(clientOptions) - : true; -} - -/** Exported for tests only. */ -export function _getShouldCreateSpanForRequest( - shouldCreateSpans: boolean, - tracingOptions: TracingOptions | undefined, - clientOptions: Partial | undefined, -): undefined | ((url: string) => boolean) { - const handler = shouldCreateSpans - ? // eslint-disable-next-line deprecation/deprecation - tracingOptions?.shouldCreateSpanForRequest || clientOptions?.shouldCreateSpanForRequest - : () => false; - - return handler; + const data = _INTERNAL.getRequestSpanData(span); + addBreadcrumb( + { + category: 'http', + data: { + status_code: response.statusCode, + ...data, + }, + type: 'http', + }, + { + event: 'response', + // TODO FN: Do we need access to `request` here? + // If we do, we'll have to use the `applyCustomAttributesOnSpan` hook instead, + // but this has worse context semantics than request/responseHook. + response, + }, + ); } diff --git a/packages/node/src/integrations/local-variables/local-variables-sync.ts b/packages/node/src/integrations/local-variables/local-variables-sync.ts index e1ec4b57023c..91fb9005b4c3 100644 --- a/packages/node/src/integrations/local-variables/local-variables-sync.ts +++ b/packages/node/src/integrations/local-variables/local-variables-sync.ts @@ -1,11 +1,11 @@ -/* eslint-disable max-lines */ -import { convertIntegrationFnToClass, defineIntegration, getClient } from '@sentry/core'; -import type { Event, Exception, Integration, IntegrationClass, IntegrationFn, StackParser } from '@sentry/types'; +import { defineIntegration, getClient } from '@sentry/core'; +import type { Event, Exception, IntegrationFn, StackParser } from '@sentry/types'; import { LRUMap, logger } from '@sentry/utils'; -import type { Debugger, InspectorNotification, Runtime, Session } from 'inspector'; -import type { NodeClient } from '../../client'; +import type { Debugger, InspectorNotification, Runtime } from 'inspector'; +import { Session } from 'inspector'; -import { NODE_VERSION } from '../../nodeVersion'; +import { NODE_MAJOR } from '../../nodeVersion'; +import type { NodeClient } from '../../sdk/client'; import type { FrameVariables, LocalVariablesIntegrationOptions, @@ -79,22 +79,6 @@ class AsyncSession implements DebugSession { /** Throws if inspector API is not available */ public constructor() { - /* - TODO: We really should get rid of this require statement below for a couple of reasons: - 1. It makes the integration unusable in the SvelteKit SDK, as it's not possible to use `require` - in SvelteKit server code (at least not by default). - 2. Throwing in a constructor is bad practice - - More context for a future attempt to fix this: - We already tried replacing it with import but didn't get it to work because of async problems. - We still called import in the constructor but assigned to a promise which we "awaited" in - `configureAndConnect`. However, this broke the Node integration tests as no local variables - were reported any more. We probably missed a place where we need to await the promise, too. - */ - - // Node can be built without inspector support so this can throw - // eslint-disable-next-line @typescript-eslint/no-var-requires - const { Session } = require('inspector'); this._session = new Session(); } @@ -304,14 +288,16 @@ const _localVariablesSyncIntegration = (( return; } - const frameCount = exception.stacktrace?.frames?.length || 0; + // Filter out frames where the function name is `new Promise` since these are in the error.stack frames + // but do not appear in the debugger call frames + const frames = (exception.stacktrace?.frames || []).filter(frame => frame.function !== 'new Promise'); - for (let i = 0; i < frameCount; i++) { + for (let i = 0; i < frames.length; i++) { // Sentry frames are in reverse order - const frameIndex = frameCount - i - 1; + const frameIndex = frames.length - i - 1; // Drop out if we run out of frames to match up - if (!exception?.stacktrace?.frames?.[frameIndex] || !cachedFrame[i]) { + if (!frames[frameIndex] || !cachedFrame[i]) { break; } @@ -319,14 +305,14 @@ const _localVariablesSyncIntegration = (( // We need to have vars to add cachedFrame[i].vars === undefined || // We're not interested in frames that are not in_app because the vars are not relevant - exception.stacktrace.frames[frameIndex].in_app === false || + frames[frameIndex].in_app === false || // The function names need to match - !functionNamesMatch(exception.stacktrace.frames[frameIndex].function, cachedFrame[i].function) + !functionNamesMatch(frames[frameIndex].function, cachedFrame[i].function) ) { continue; } - exception.stacktrace.frames[frameIndex].vars = cachedFrame[i].vars; + frames[frameIndex].vars = cachedFrame[i].vars; } } @@ -347,7 +333,7 @@ const _localVariablesSyncIntegration = (( if (session && clientOptions?.includeLocalVariables) { // Only setup this integration if the Node version is >= v18 // https://github.com/getsentry/sentry-javascript/issues/7697 - const unsupportedNodeVersion = NODE_VERSION.major < 18; + const unsupportedNodeVersion = NODE_MAJOR < 18; if (unsupportedNodeVersion) { logger.log('The `LocalVariables` integration is only supported on Node >= v18.'); @@ -400,19 +386,7 @@ const _localVariablesSyncIntegration = (( }; }) satisfies IntegrationFn; -export const localVariablesSyncIntegration = defineIntegration(_localVariablesSyncIntegration); - /** * Adds local variables to exception frames. - * @deprecated Use `localVariablesSyncIntegration()` instead. */ -// eslint-disable-next-line deprecation/deprecation -export const LocalVariablesSync = convertIntegrationFnToClass( - INTEGRATION_NAME, - localVariablesSyncIntegration, -) as IntegrationClass Event; setup: (client: NodeClient) => void }> & { - new (options?: LocalVariablesIntegrationOptions, session?: DebugSession): Integration; -}; - -// eslint-disable-next-line deprecation/deprecation -export type LocalVariablesSync = typeof LocalVariablesSync; +export const localVariablesSyncIntegration = defineIntegration(_localVariablesSyncIntegration); diff --git a/packages/node/src/integrations/modules.ts b/packages/node/src/integrations/modules.ts index 1f9aff7303e3..ad30bb4d7a3b 100644 --- a/packages/node/src/integrations/modules.ts +++ b/packages/node/src/integrations/modules.ts @@ -1,12 +1,31 @@ import { existsSync, readFileSync } from 'fs'; import { dirname, join } from 'path'; -import { convertIntegrationFnToClass, defineIntegration } from '@sentry/core'; -import type { Event, Integration, IntegrationClass, IntegrationFn } from '@sentry/types'; +import { defineIntegration } from '@sentry/core'; +import type { IntegrationFn } from '@sentry/types'; let moduleCache: { [key: string]: string }; const INTEGRATION_NAME = '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. + */ +export const modulesIntegration = defineIntegration(_modulesIntegration); + /** Extract information about paths */ function getPaths(): string[] { try { @@ -75,31 +94,3 @@ function _getModules(): { [key: string]: string } { } return moduleCache; } - -const _modulesIntegration = (() => { - return { - name: INTEGRATION_NAME, - processEvent(event) { - event.modules = { - ...event.modules, - ..._getModules(), - }; - - return event; - }, - }; -}) satisfies IntegrationFn; - -export const modulesIntegration = defineIntegration(_modulesIntegration); - -/** - * Add node modules / packages to the event. - * @deprecated Use `modulesIntegration()` instead. - */ -// eslint-disable-next-line deprecation/deprecation -export const Modules = convertIntegrationFnToClass(INTEGRATION_NAME, modulesIntegration) as IntegrationClass< - Integration & { processEvent: (event: Event) => Event } ->; - -// eslint-disable-next-line deprecation/deprecation -export type Modules = typeof Modules; diff --git a/packages/node-experimental/src/integrations/node-fetch.ts b/packages/node/src/integrations/node-fetch.ts similarity index 100% rename from packages/node-experimental/src/integrations/node-fetch.ts rename to packages/node/src/integrations/node-fetch.ts diff --git a/packages/node/src/integrations/onuncaughtexception.ts b/packages/node/src/integrations/onuncaughtexception.ts index 68be68a6d6cc..e56c3c0801d7 100644 --- a/packages/node/src/integrations/onuncaughtexception.ts +++ b/packages/node/src/integrations/onuncaughtexception.ts @@ -1,11 +1,11 @@ -import { captureException, convertIntegrationFnToClass, defineIntegration } from '@sentry/core'; +import { captureException, defineIntegration } from '@sentry/core'; import { getClient } from '@sentry/core'; -import type { Integration, IntegrationClass, IntegrationFn } from '@sentry/types'; +import type { IntegrationFn } from '@sentry/types'; import { logger } from '@sentry/utils'; -import type { NodeClient } from '../client'; import { DEBUG_BUILD } from '../debug-build'; -import { logAndExitProcess } from './utils/errorhandling'; +import type { NodeClient } from '../sdk/client'; +import { logAndExitProcess } from '../utils/errorhandling'; type OnFatalErrorHandler = (firstError: Error, secondError?: Error) => void; @@ -54,27 +54,10 @@ const _onUncaughtExceptionIntegration = ((options: Partial void }> & { - new ( - options?: Partial<{ - exitEvenIfOtherHandlersAreRegistered: boolean; - onFatalError?(this: void, firstError: Error, secondError?: Error): void; - }>, - ): Integration; -}; - -// eslint-disable-next-line deprecation/deprecation -export type OnUncaughtException = typeof OnUncaughtException; +export const onUncaughtExceptionIntegration = defineIntegration(_onUncaughtExceptionIntegration); type ErrorHandler = { _errorHandler: boolean } & ((error: Error) => void); @@ -103,20 +86,19 @@ export function makeErrorHandler(client: NodeClient, options: OnUncaughtExceptio // exit behaviour of the SDK accordingly: // - If other listeners are attached, do not exit. // - If the only listener attached is ours, exit. - const userProvidedListenersCount = ( - global.process.listeners('uncaughtException') as TaggedListener[] - ).reduce((acc, listener) => { - if ( + const userProvidedListenersCount = (global.process.listeners('uncaughtException') as TaggedListener[]).filter( + listener => { // There are 3 listeners we ignore: - listener.name === 'domainUncaughtExceptionClear' || // as soon as we're using domains this listener is attached by node itself - (listener.tag && listener.tag === 'sentry_tracingErrorCallback') || // the handler we register for tracing - (listener as ErrorHandler)._errorHandler // the handler we register in this integration - ) { - return acc; - } else { - return acc + 1; - } - }, 0); + return ( + // as soon as we're using domains this listener is attached by node itself + listener.name !== 'domainUncaughtExceptionClear' && + // the handler we register for tracing + listener.tag !== 'sentry_tracingErrorCallback' && + // the handler we register in this integration + (listener as ErrorHandler)._errorHandler !== true + ); + }, + ).length; const processWouldExit = userProvidedListenersCount === 0; const shouldApplyFatalHandlingLogic = options.exitEvenIfOtherHandlersAreRegistered || processWouldExit; diff --git a/packages/node/src/integrations/onunhandledrejection.ts b/packages/node/src/integrations/onunhandledrejection.ts index 9f3801b7a0cf..e1bc0b4145cf 100644 --- a/packages/node/src/integrations/onunhandledrejection.ts +++ b/packages/node/src/integrations/onunhandledrejection.ts @@ -1,8 +1,7 @@ -import { captureException, convertIntegrationFnToClass, defineIntegration, getClient } from '@sentry/core'; -import type { Client, Integration, IntegrationClass, IntegrationFn } from '@sentry/types'; +import { captureException, defineIntegration, getClient } from '@sentry/core'; +import type { Client, IntegrationFn } from '@sentry/types'; import { consoleSandbox } from '@sentry/utils'; - -import { logAndExitProcess } from './utils/errorhandling'; +import { logAndExitProcess } from '../utils/errorhandling'; type UnhandledRejectionMode = 'none' | 'warn' | 'strict'; @@ -27,22 +26,10 @@ const _onUnhandledRejectionIntegration = ((options: Partial void }> & { - new (options?: Partial<{ mode: UnhandledRejectionMode }>): Integration; -}; - -// eslint-disable-next-line deprecation/deprecation -export type OnUnhandledRejection = typeof OnUnhandledRejection; +export const onUnhandledRejectionIntegration = defineIntegration(_onUnhandledRejectionIntegration); /** * Send an exception with reason diff --git a/packages/node/src/integrations/spotlight.ts b/packages/node/src/integrations/spotlight.ts index eb9c34260b61..21629ad340ac 100644 --- a/packages/node/src/integrations/spotlight.ts +++ b/packages/node/src/integrations/spotlight.ts @@ -1,7 +1,6 @@ import * as http from 'http'; -import { URL } from 'url'; -import { convertIntegrationFnToClass, defineIntegration } from '@sentry/core'; -import type { Client, Envelope, Integration, IntegrationClass, IntegrationFn } from '@sentry/types'; +import { defineIntegration } from '@sentry/core'; +import type { Client, Envelope, IntegrationFn } from '@sentry/types'; import { logger, serializeEnvelope } from '@sentry/utils'; type SpotlightConnectionOptions = { @@ -30,30 +29,14 @@ const _spotlightIntegration = ((options: Partial = { }; }) satisfies IntegrationFn; -export const spotlightIntegration = defineIntegration(_spotlightIntegration); - /** * Use this integration to send errors and transactions to Spotlight. * * Learn more about spotlight at https://spotlightjs.com * * Important: This integration only works with Node 18 or newer. - * - * @deprecated Use `spotlightIntegration()` instead. */ -// eslint-disable-next-line deprecation/deprecation -export const Spotlight = convertIntegrationFnToClass(INTEGRATION_NAME, spotlightIntegration) as IntegrationClass< - Integration & { setup: (client: Client) => void } -> & { - new ( - options?: Partial<{ - sidecarUrl?: string; - }>, - ): Integration; -}; - -// eslint-disable-next-line deprecation/deprecation -export type Spotlight = typeof Spotlight; +export const spotlightIntegration = defineIntegration(_spotlightIntegration); function connectToSpotlight(client: Client, options: Required): void { const spotlightUrl = parseSidecarUrl(options.sidecarUrl); diff --git a/packages/node-experimental/src/integrations/tracing/express.ts b/packages/node/src/integrations/tracing/express.ts similarity index 100% rename from packages/node-experimental/src/integrations/tracing/express.ts rename to packages/node/src/integrations/tracing/express.ts diff --git a/packages/node-experimental/src/integrations/tracing/fastify.ts b/packages/node/src/integrations/tracing/fastify.ts similarity index 100% rename from packages/node-experimental/src/integrations/tracing/fastify.ts rename to packages/node/src/integrations/tracing/fastify.ts diff --git a/packages/node-experimental/src/integrations/tracing/graphql.ts b/packages/node/src/integrations/tracing/graphql.ts similarity index 100% rename from packages/node-experimental/src/integrations/tracing/graphql.ts rename to packages/node/src/integrations/tracing/graphql.ts diff --git a/packages/node-experimental/src/integrations/tracing/hapi/index.ts b/packages/node/src/integrations/tracing/hapi/index.ts similarity index 100% rename from packages/node-experimental/src/integrations/tracing/hapi/index.ts rename to packages/node/src/integrations/tracing/hapi/index.ts diff --git a/packages/node-experimental/src/integrations/tracing/hapi/types.ts b/packages/node/src/integrations/tracing/hapi/types.ts similarity index 100% rename from packages/node-experimental/src/integrations/tracing/hapi/types.ts rename to packages/node/src/integrations/tracing/hapi/types.ts diff --git a/packages/node-experimental/src/integrations/tracing/index.ts b/packages/node/src/integrations/tracing/index.ts similarity index 100% rename from packages/node-experimental/src/integrations/tracing/index.ts rename to packages/node/src/integrations/tracing/index.ts diff --git a/packages/node-experimental/src/integrations/tracing/koa.ts b/packages/node/src/integrations/tracing/koa.ts similarity index 100% rename from packages/node-experimental/src/integrations/tracing/koa.ts rename to packages/node/src/integrations/tracing/koa.ts diff --git a/packages/node-experimental/src/integrations/tracing/mongo.ts b/packages/node/src/integrations/tracing/mongo.ts similarity index 100% rename from packages/node-experimental/src/integrations/tracing/mongo.ts rename to packages/node/src/integrations/tracing/mongo.ts diff --git a/packages/node-experimental/src/integrations/tracing/mongoose.ts b/packages/node/src/integrations/tracing/mongoose.ts similarity index 100% rename from packages/node-experimental/src/integrations/tracing/mongoose.ts rename to packages/node/src/integrations/tracing/mongoose.ts diff --git a/packages/node-experimental/src/integrations/tracing/mysql.ts b/packages/node/src/integrations/tracing/mysql.ts similarity index 100% rename from packages/node-experimental/src/integrations/tracing/mysql.ts rename to packages/node/src/integrations/tracing/mysql.ts diff --git a/packages/node-experimental/src/integrations/tracing/mysql2.ts b/packages/node/src/integrations/tracing/mysql2.ts similarity index 100% rename from packages/node-experimental/src/integrations/tracing/mysql2.ts rename to packages/node/src/integrations/tracing/mysql2.ts diff --git a/packages/node-experimental/src/integrations/tracing/nest.ts b/packages/node/src/integrations/tracing/nest.ts similarity index 100% rename from packages/node-experimental/src/integrations/tracing/nest.ts rename to packages/node/src/integrations/tracing/nest.ts diff --git a/packages/node-experimental/src/integrations/tracing/postgres.ts b/packages/node/src/integrations/tracing/postgres.ts similarity index 100% rename from packages/node-experimental/src/integrations/tracing/postgres.ts rename to packages/node/src/integrations/tracing/postgres.ts diff --git a/packages/node-experimental/src/integrations/tracing/prisma.ts b/packages/node/src/integrations/tracing/prisma.ts similarity index 100% rename from packages/node-experimental/src/integrations/tracing/prisma.ts rename to packages/node/src/integrations/tracing/prisma.ts diff --git a/packages/node/src/nodeVersion.ts b/packages/node/src/nodeVersion.ts index 1574237f3fb4..1f07883b771b 100644 --- a/packages/node/src/nodeVersion.ts +++ b/packages/node/src/nodeVersion.ts @@ -1,3 +1,4 @@ import { parseSemver } from '@sentry/utils'; export const NODE_VERSION = parseSemver(process.versions.node) as { major: number; minor: number; patch: number }; +export const NODE_MAJOR = NODE_VERSION.major; diff --git a/packages/node-experimental/src/otel/contextManager.ts b/packages/node/src/otel/contextManager.ts similarity index 100% rename from packages/node-experimental/src/otel/contextManager.ts rename to packages/node/src/otel/contextManager.ts diff --git a/packages/node/src/proxy/helpers.ts b/packages/node/src/proxy/helpers.ts index a5064408855d..031878511f6c 100644 --- a/packages/node/src/proxy/helpers.ts +++ b/packages/node/src/proxy/helpers.ts @@ -30,8 +30,6 @@ import * as http from 'node:http'; import * as https from 'node:https'; import type { Readable } from 'stream'; -// TODO (v8): Remove this when Node < 12 is no longer supported -import type { URL } from 'url'; export type ThenableRequest = http.ClientRequest & { then: Promise['then']; diff --git a/packages/node/src/proxy/index.ts b/packages/node/src/proxy/index.ts index 15c700ed3e62..83f72d56fb4e 100644 --- a/packages/node/src/proxy/index.ts +++ b/packages/node/src/proxy/index.ts @@ -32,8 +32,6 @@ import type * as http from 'http'; import type { OutgoingHttpHeaders } from 'http'; import * as net from 'net'; import * as tls from 'tls'; -// TODO (v8): Remove this when Node < 12 is no longer supported -import { URL } from 'url'; import { logger } from '@sentry/utils'; import { Agent } from './base'; import type { AgentConnectOpts } from './base'; diff --git a/packages/node-experimental/src/sdk/api.ts b/packages/node/src/sdk/api.ts similarity index 100% rename from packages/node-experimental/src/sdk/api.ts rename to packages/node/src/sdk/api.ts diff --git a/packages/node-experimental/src/sdk/client.ts b/packages/node/src/sdk/client.ts similarity index 100% rename from packages/node-experimental/src/sdk/client.ts rename to packages/node/src/sdk/client.ts diff --git a/packages/node-experimental/src/sdk/init.ts b/packages/node/src/sdk/init.ts similarity index 100% rename from packages/node-experimental/src/sdk/init.ts rename to packages/node/src/sdk/init.ts diff --git a/packages/node-experimental/src/sdk/initOtel.ts b/packages/node/src/sdk/initOtel.ts similarity index 100% rename from packages/node-experimental/src/sdk/initOtel.ts rename to packages/node/src/sdk/initOtel.ts diff --git a/packages/node-experimental/src/sdk/scope.ts b/packages/node/src/sdk/scope.ts similarity index 100% rename from packages/node-experimental/src/sdk/scope.ts rename to packages/node/src/sdk/scope.ts diff --git a/packages/node/src/transports/http-module.ts b/packages/node/src/transports/http-module.ts index 64b255cc869c..f5cbe6fd35f9 100644 --- a/packages/node/src/transports/http-module.ts +++ b/packages/node/src/transports/http-module.ts @@ -1,7 +1,5 @@ -import type { IncomingHttpHeaders, RequestOptions as HTTPRequestOptions } from 'http'; -import type { RequestOptions as HTTPSRequestOptions } from 'https'; -import type { Writable } from 'stream'; -import type { URL } from 'url'; +import type { ClientRequest, IncomingHttpHeaders, RequestOptions as HTTPRequestOptions } from 'node:http'; +import type { RequestOptions as HTTPSRequestOptions } from 'node:https'; export type HTTPModuleRequestOptions = HTTPRequestOptions | HTTPSRequestOptions | string | URL; @@ -26,15 +24,5 @@ export interface HTTPModule { * @param options These are {@see TransportOptions} * @param callback Callback when request is finished */ - request(options: HTTPModuleRequestOptions, callback?: (res: HTTPModuleRequestIncomingMessage) => void): Writable; - - // This is the type for nodejs versions that handle the URL argument - // (v10.9.0+), but we do not use it just yet because we support older node - // versions: - - // request( - // url: string | URL, - // options: http.RequestOptions | https.RequestOptions, - // callback?: (res: http.IncomingMessage) => void, - // ): http.ClientRequest; + request(options: HTTPModuleRequestOptions, callback?: (res: HTTPModuleRequestIncomingMessage) => void): ClientRequest; } diff --git a/packages/node/src/transports/http.ts b/packages/node/src/transports/http.ts index a6a05fc07c95..4cbe7ece1f60 100644 --- a/packages/node/src/transports/http.ts +++ b/packages/node/src/transports/http.ts @@ -1,8 +1,9 @@ import * as http from 'node:http'; import * as https from 'node:https'; import { Readable } from 'stream'; -import { URL } from 'url'; import { createGzip } from 'zlib'; +import { context } from '@opentelemetry/api'; +import { suppressTracing } from '@opentelemetry/core'; import { createTransport } from '@sentry/core'; import type { BaseTransportOptions, @@ -13,7 +14,6 @@ import type { } from '@sentry/types'; import { consoleSandbox } from '@sentry/utils'; import { HttpsProxyAgent } from '../proxy'; - import type { HTTPModule } from './http-module'; export interface NodeTransportOptions extends BaseTransportOptions { @@ -81,8 +81,11 @@ export function makeNodeTransport(options: NodeTransportOptions): Transport { ? (new HttpsProxyAgent(proxy) as http.Agent) : new nativeHttpModule.Agent({ keepAlive, maxSockets: 30, timeout: 2000 }); - const requestExecutor = createRequestExecutor(options, options.httpModule ?? nativeHttpModule, agent); - return createTransport(options, requestExecutor); + // This ensures we do not generate any spans in OpenTelemetry for the transport + return context.with(suppressTracing(context.active()), () => { + const requestExecutor = createRequestExecutor(options, options.httpModule ?? nativeHttpModule, agent); + return createTransport(options, requestExecutor); + }); } /** diff --git a/packages/node/src/types.ts b/packages/node/src/types.ts index 01f91fb46cbe..d78e1761fd79 100644 --- a/packages/node/src/types.ts +++ b/packages/node/src/types.ts @@ -1,6 +1,7 @@ -import type { ClientOptions, Options, SamplingContext, TracePropagationTargets } from '@sentry/types'; +import type { Span as WriteableSpan } from '@opentelemetry/api'; +import type { ReadableSpan } from '@opentelemetry/sdk-trace-base'; +import type { ClientOptions, Options, SamplingContext, Scope, Span, TracePropagationTargets } from '@sentry/types'; -import type { NodeClient } from './client'; import type { NodeTransportOptions } from './transports'; export interface BaseNodeOptions { @@ -51,14 +52,6 @@ export interface BaseNodeOptions { */ includeLocalVariables?: boolean; - /** - * Specify a custom NodeClient to be used. Must extend NodeClient! - * This is not a public, supported API, but used internally only. - * - * @hidden - * */ - clientClass?: typeof NodeClient; - /** * If you use Spotlight by Sentry during development, use * this option to forward captured Sentry events to Spotlight. @@ -71,23 +64,15 @@ export interface BaseNodeOptions { */ spotlight?: boolean | string; - // TODO (v8): Remove this in v8 /** - * @deprecated Moved to constructor options of the `Http` and `Undici` integration. - * @example - * ```js - * Sentry.init({ - * integrations: [ - * new Sentry.Integrations.Http({ - * tracing: { - * shouldCreateSpanForRequest: (url: string) => false, - * } - * }); - * ], - * }); - * ``` + * If this is set to true, the SDK will not set up OpenTelemetry automatically. + * In this case, you _have_ to ensure to set it up correctly yourself, including: + * * The `SentrySpanProcessor` + * * The `SentryPropagator` + * * The `SentryContextManager` + * * The `SentrySampler` */ - shouldCreateSpanForRequest?(this: void, url: string): boolean; + skipOpenTelemetrySetup?: boolean; /** Callback that is executed when a fatal global error occurs. */ onFatalError?(this: void, error: Error): void; @@ -104,3 +89,19 @@ export interface NodeOptions extends Options, BaseNodeOpti * @see NodeClient for more information. */ export interface NodeClientOptions extends ClientOptions, BaseNodeOptions {} + +export interface CurrentScopes { + scope: Scope; + isolationScope: Scope; +} + +/** + * The base `Span` type is basically a `WriteableSpan`. + * There are places where we basically want to allow passing _any_ span, + * so in these cases we type this as `AbstractSpan` which could be either a regular `Span` or a `ReadableSpan`. + * You'll have to make sur to check revelant fields before accessing them. + * + * Note that technically, the `Span` exported from `@opentelemwetry/sdk-trace-base` matches this, + * but we cannot be 100% sure that we are actually getting such a span, so this type is more defensive. + */ +export type AbstractSpan = WriteableSpan | ReadableSpan | Span; diff --git a/packages/node-experimental/src/utils/addOriginToSpan.ts b/packages/node/src/utils/addOriginToSpan.ts similarity index 100% rename from packages/node-experimental/src/utils/addOriginToSpan.ts rename to packages/node/src/utils/addOriginToSpan.ts diff --git a/packages/node-experimental/src/utils/errorhandling.ts b/packages/node/src/utils/errorhandling.ts similarity index 100% rename from packages/node-experimental/src/utils/errorhandling.ts rename to packages/node/src/utils/errorhandling.ts diff --git a/packages/node-experimental/src/utils/getRequestUrl.ts b/packages/node/src/utils/getRequestUrl.ts similarity index 100% rename from packages/node-experimental/src/utils/getRequestUrl.ts rename to packages/node/src/utils/getRequestUrl.ts diff --git a/packages/node/src/module.ts b/packages/node/src/utils/module.ts similarity index 100% rename from packages/node/src/module.ts rename to packages/node/src/utils/module.ts diff --git a/packages/node-experimental/src/utils/prepareEvent.ts b/packages/node/src/utils/prepareEvent.ts similarity index 100% rename from packages/node-experimental/src/utils/prepareEvent.ts rename to packages/node/src/utils/prepareEvent.ts diff --git a/packages/node-experimental/test/helpers/error.ts b/packages/node/test/helpers/error.ts similarity index 100% rename from packages/node-experimental/test/helpers/error.ts rename to packages/node/test/helpers/error.ts diff --git a/packages/node-experimental/test/helpers/getDefaultNodeClientOptions.ts b/packages/node/test/helpers/getDefaultNodeClientOptions.ts similarity index 100% rename from packages/node-experimental/test/helpers/getDefaultNodeClientOptions.ts rename to packages/node/test/helpers/getDefaultNodeClientOptions.ts diff --git a/packages/node-experimental/test/helpers/mockSdkInit.ts b/packages/node/test/helpers/mockSdkInit.ts similarity index 100% rename from packages/node-experimental/test/helpers/mockSdkInit.ts rename to packages/node/test/helpers/mockSdkInit.ts diff --git a/packages/node-experimental/test/integration/breadcrumbs.test.ts b/packages/node/test/integration/breadcrumbs.test.ts similarity index 100% rename from packages/node-experimental/test/integration/breadcrumbs.test.ts rename to packages/node/test/integration/breadcrumbs.test.ts diff --git a/packages/node-experimental/test/integration/scope.test.ts b/packages/node/test/integration/scope.test.ts similarity index 100% rename from packages/node-experimental/test/integration/scope.test.ts rename to packages/node/test/integration/scope.test.ts diff --git a/packages/node-experimental/test/integration/transactions.test.ts b/packages/node/test/integration/transactions.test.ts similarity index 100% rename from packages/node-experimental/test/integration/transactions.test.ts rename to packages/node/test/integration/transactions.test.ts diff --git a/packages/node/test/integrations/contextlines.test.ts b/packages/node/test/integrations/contextlines.test.ts index 67f3ad793fbc..c4ef1efaa292 100644 --- a/packages/node/test/integrations/contextlines.test.ts +++ b/packages/node/test/integrations/contextlines.test.ts @@ -1,30 +1,37 @@ -import * as fs from 'fs'; -import type { Event, Integration, StackFrame } from '@sentry/types'; +import { promises } from 'fs'; +import type { StackFrame } from '@sentry/types'; import { parseStackFrames } from '@sentry/utils'; -import { contextLinesIntegration } from '../../src'; -import { resetFileContentCache } from '../../src/integrations/contextlines'; -import { defaultStackParser } from '../../src/sdk'; -import { getError } from '../helper/error'; +import { _contextLinesIntegration, resetFileContentCache } from '../../src/integrations/contextlines'; +import { defaultStackParser } from '../../src/sdk/api'; +import { getError } from '../helpers/error'; + +jest.mock('fs', () => { + const actual = jest.requireActual('fs'); + return { + ...actual, + promises: { + ...actual.promises, + readFile: jest.fn(actual.promises), + }, + }; +}); describe('ContextLines', () => { - let readFileSpy: jest.SpyInstance; - let contextLines: Integration; + const readFileSpy = promises.readFile as unknown as jest.SpyInstance; + let contextLines: ReturnType; async function addContext(frames: StackFrame[]): Promise { - await (contextLines as Integration & { processEvent: (event: Event) => Promise }).processEvent({ - exception: { values: [{ stacktrace: { frames } }] }, - }); + await contextLines.processEvent({ exception: { values: [{ stacktrace: { frames } }] } }); } beforeEach(() => { - readFileSpy = jest.spyOn(fs, 'readFile'); - contextLines = contextLinesIntegration(); + contextLines = _contextLinesIntegration(); resetFileContentCache(); }); afterEach(() => { - jest.restoreAllMocks(); + jest.clearAllMocks(); }); describe('lru file cache', () => { @@ -101,7 +108,7 @@ describe('ContextLines', () => { }); test('parseStack with no context', async () => { - contextLines = contextLinesIntegration({ frameContextLines: 0 }); + contextLines = _contextLinesIntegration({ frameContextLines: 0 }); expect.assertions(1); const frames = parseStackFrames(defaultStackParser, new Error('test')); @@ -113,7 +120,6 @@ describe('ContextLines', () => { test('does not attempt to readfile multiple times if it fails', async () => { expect.assertions(1); - contextLines = contextLinesIntegration(); readFileSpy.mockImplementation(() => { throw new Error("ENOENT: no such file or directory, open '/does/not/exist.js'"); diff --git a/packages/node-experimental/test/integrations/express.test.ts b/packages/node/test/integrations/express.test.ts similarity index 100% rename from packages/node-experimental/test/integrations/express.test.ts rename to packages/node/test/integrations/express.test.ts diff --git a/packages/node/test/integrations/localvariables.test.ts b/packages/node/test/integrations/localvariables.test.ts index abc1d241f842..db9385214d42 100644 --- a/packages/node/test/integrations/localvariables.test.ts +++ b/packages/node/test/integrations/localvariables.test.ts @@ -1,12 +1,12 @@ import { createRateLimiter } from '../../src/integrations/local-variables/common'; import { createCallbackList } from '../../src/integrations/local-variables/local-variables-sync'; -import { NODE_VERSION } from '../../src/nodeVersion'; +import { NODE_MAJOR } from '../../src/nodeVersion'; jest.setTimeout(20_000); const describeIf = (condition: boolean) => (condition ? describe : describe.skip); -describeIf(NODE_VERSION.major >= 18)('LocalVariables', () => { +describeIf(NODE_MAJOR >= 18)('LocalVariables', () => { describe('createCallbackList', () => { it('Should call callbacks in reverse order', done => { const log: number[] = []; diff --git a/packages/node/test/integrations/spotlight.test.ts b/packages/node/test/integrations/spotlight.test.ts index a892677d0dd0..6b888c22edcd 100644 --- a/packages/node/test/integrations/spotlight.test.ts +++ b/packages/node/test/integrations/spotlight.test.ts @@ -2,9 +2,9 @@ import * as http from 'http'; import type { Envelope, EventEnvelope } from '@sentry/types'; import { createEnvelope, logger } from '@sentry/utils'; -import { NodeClient, spotlightIntegration } from '../../src'; -import { Spotlight } from '../../src/integrations'; -import { getDefaultNodeClientOptions } from '../helper/node-client-options'; +import { spotlightIntegration } from '../../src/integrations/spotlight'; +import { NodeClient } from '../../src/sdk/client'; +import { getDefaultNodeClientOptions } from '../helpers/getDefaultNodeClientOptions'; describe('Spotlight', () => { const loggerSpy = jest.spyOn(logger, 'warn'); @@ -17,12 +17,9 @@ describe('Spotlight', () => { const options = getDefaultNodeClientOptions(); const client = new NodeClient(options); - it('has a name and id', () => { - // eslint-disable-next-line deprecation/deprecation - const integration = new Spotlight(); + it('has a name', () => { + const integration = spotlightIntegration(); expect(integration.name).toEqual('Spotlight'); - // eslint-disable-next-line deprecation/deprecation - expect(Spotlight.id).toEqual('Spotlight'); }); it('registers a callback on the `beforeEnvelope` hook', () => { diff --git a/packages/node-experimental/test/sdk/api.test.ts b/packages/node/test/sdk/api.test.ts similarity index 100% rename from packages/node-experimental/test/sdk/api.test.ts rename to packages/node/test/sdk/api.test.ts diff --git a/packages/node-experimental/test/sdk/client.test.ts b/packages/node/test/sdk/client.test.ts similarity index 100% rename from packages/node-experimental/test/sdk/client.test.ts rename to packages/node/test/sdk/client.test.ts diff --git a/packages/node-experimental/test/sdk/init.test.ts b/packages/node/test/sdk/init.test.ts similarity index 100% rename from packages/node-experimental/test/sdk/init.test.ts rename to packages/node/test/sdk/init.test.ts diff --git a/packages/node-experimental/test/sdk/scope.test.ts b/packages/node/test/sdk/scope.test.ts similarity index 100% rename from packages/node-experimental/test/sdk/scope.test.ts rename to packages/node/test/sdk/scope.test.ts diff --git a/packages/node/test/transports/http.test.ts b/packages/node/test/transports/http.test.ts index ddf73039a009..e945c086959a 100644 --- a/packages/node/test/transports/http.test.ts +++ b/packages/node/test/transports/http.test.ts @@ -1,6 +1,4 @@ -/* eslint-disable deprecation/deprecation */ import * as http from 'http'; - import { createGunzip } from 'zlib'; import { createTransport } from '@sentry/core'; import type { EventEnvelope, EventItem } from '@sentry/types'; @@ -51,19 +49,18 @@ function setupTestServer( res.end(); // also terminate socket because keepalive hangs connection a bit - if (res.connection) { - res.connection.end(); - } + // eslint-disable-next-line deprecation/deprecation + res.connection?.end(); }); - testServer.listen(18099); + testServer.listen(18101); return new Promise(resolve => { testServer?.on('listening', resolve); }); } -const TEST_SERVER_URL = 'http://localhost:18099'; +const TEST_SERVER_URL = 'http://localhost:18101'; const EVENT_ENVELOPE = createEnvelope({ event_id: 'aa3ff046696b4bc6b609ce6d28fde9e2', sent_at: '123' }, [ [{ type: 'event' }, { event_id: 'aa3ff046696b4bc6b609ce6d28fde9e2' }] as EventItem, diff --git a/packages/node/test/transports/https.test.ts b/packages/node/test/transports/https.test.ts index a45319c40e42..8b0d3312ba54 100644 --- a/packages/node/test/transports/https.test.ts +++ b/packages/node/test/transports/https.test.ts @@ -1,4 +1,3 @@ -/* eslint-disable deprecation/deprecation */ import * as http from 'http'; import * as https from 'https'; import { createTransport } from '@sentry/core'; @@ -50,19 +49,18 @@ function setupTestServer( res.end(); // also terminate socket because keepalive hangs connection a bit - if (res.connection) { - res.connection.end(); - } + // eslint-disable-next-line deprecation/deprecation + res.connection?.end(); }); - testServer.listen(8099); + testServer.listen(8100); return new Promise(resolve => { testServer?.on('listening', resolve); }); } -const TEST_SERVER_URL = 'https://localhost:8099'; +const TEST_SERVER_URL = 'https://localhost:8100'; const EVENT_ENVELOPE = createEnvelope({ event_id: 'aa3ff046696b4bc6b609ce6d28fde9e2', sent_at: '123' }, [ [{ type: 'event' }, { event_id: 'aa3ff046696b4bc6b609ce6d28fde9e2' }] as EventItem, diff --git a/packages/node-experimental/test/utils/getRequestUrl.test.ts b/packages/node/test/utils/getRequestUrl.test.ts similarity index 100% rename from packages/node-experimental/test/utils/getRequestUrl.test.ts rename to packages/node/test/utils/getRequestUrl.test.ts diff --git a/packages/node/tsconfig.json b/packages/node/tsconfig.json index 89a9b9e0e2fe..8f38d240197e 100644 --- a/packages/node/tsconfig.json +++ b/packages/node/tsconfig.json @@ -4,6 +4,7 @@ "include": ["src/**/*"], "compilerOptions": { - "lib": ["es2018"] + "lib": ["es2018"], + "module": "Node16" } } diff --git a/packages/node/tsconfig.test.json b/packages/node/tsconfig.test.json index 52333183eb70..87f6afa06b86 100644 --- a/packages/node/tsconfig.test.json +++ b/packages/node/tsconfig.test.json @@ -1,7 +1,7 @@ { "extends": "./tsconfig.json", - "include": ["test/**/*", "src/**/*.d.ts"], + "include": ["test/**/*"], "compilerOptions": { // should include all types from `./tsconfig.json` plus types for all test frameworks used diff --git a/packages/remix/test/integration/package.json b/packages/remix/test/integration/package.json index e5a763551fe9..a78fe7479c9b 100644 --- a/packages/remix/test/integration/package.json +++ b/packages/remix/test/integration/package.json @@ -25,7 +25,7 @@ "resolutions": { "@sentry/browser": "file:../../../browser", "@sentry/core": "file:../../../core", - "@sentry/node": "file:../../../node-experimental", + "@sentry/node": "file:../../../node", "@sentry/opentelemetry": "file:../../../opentelemetry", "@sentry/react": "file:../../../react", "@sentry-internal/replay": "file:../../../replay-internal",