From 895850d69926c410492bb43dd76bb855cd0ba0c8 Mon Sep 17 00:00:00 2001 From: Andrei Borza Date: Wed, 2 Jul 2025 23:25:20 +0200 Subject: [PATCH 01/13] feat(node-core): Add node-core SDK From 72dac23c276f71ac640add45e5fbbc96b53398f6 Mon Sep 17 00:00:00 2001 From: Andrei Borza Date: Wed, 2 Jul 2025 23:28:35 +0200 Subject: [PATCH 02/13] Create node-core SDK and base node SDK on it --- package.json | 1 + packages/node-core/.eslintrc.js | 9 + packages/node-core/LICENSE | 21 + packages/node-core/README.md | 116 +++ packages/node-core/package.json | 109 +++ .../node-core/rollup.anr-worker.config.mjs | 31 + packages/node-core/rollup.npm.config.mjs | 43 ++ .../{node => node-core}/src/cron/common.ts | 0 packages/{node => node-core}/src/cron/cron.ts | 0 .../{node => node-core}/src/cron/index.ts | 0 .../{node => node-core}/src/cron/node-cron.ts | 0 .../src/cron/node-schedule.ts | 0 packages/node-core/src/debug-build.ts | 8 + packages/node-core/src/index.ts | 151 ++++ packages/node-core/src/init.ts | 9 + .../src/integrations/anr/common.ts | 0 .../src/integrations/anr/index.ts | 0 .../src/integrations/anr/worker.ts | 0 .../src/integrations/childProcess.ts | 0 .../src/integrations/context.ts | 0 .../src/integrations/contextlines.ts | 0 .../src/integrations/diagnostic_channel.d.ts | 0 .../http/SentryHttpInstrumentation.ts | 0 .../node-core/src/integrations/http/index.ts | 141 ++++ .../integrations/local-variables/common.ts | 0 .../src/integrations/local-variables/index.ts | 0 .../local-variables/inspector.d.ts | 0 .../local-variables/local-variables-async.ts | 0 .../local-variables/local-variables-sync.ts | 0 .../integrations/local-variables/worker.ts | 0 .../src/integrations/modules.ts | 0 .../SentryNodeFetchInstrumentation.ts | 5 +- .../src/integrations/node-fetch/index.ts | 39 + .../src/integrations/node-fetch/types.ts | 47 ++ .../src/integrations/onuncaughtexception.ts | 0 .../src/integrations/onunhandledrejection.ts | 0 .../src/integrations/processSession.ts | 0 .../src/integrations/spotlight.ts | 0 .../src/integrations/winston.ts | 0 .../{node => node-core}/src/logs/capture.ts | 0 .../{node => node-core}/src/logs/exports.ts | 0 .../{node => node-core}/src/nodeVersion.ts | 0 .../src/otel/contextManager.ts | 0 .../src/otel/instrument.ts | 1 + .../{node => node-core}/src/proxy/base.ts | 0 .../{node => node-core}/src/proxy/helpers.ts | 0 .../{node => node-core}/src/proxy/index.ts | 2 + .../src/proxy/parse-proxy-response.ts | 0 packages/{node => node-core}/src/sdk/api.ts | 0 .../{node => node-core}/src/sdk/client.ts | 5 +- packages/node-core/src/sdk/esmLoader.ts | 31 + packages/node-core/src/sdk/index.ts | 267 +++++++ packages/{node => node-core}/src/sdk/scope.ts | 0 .../src/transports/http-module.ts | 0 .../src/transports/http.ts | 0 .../src/transports/index.ts | 0 packages/node-core/src/types.ts | 178 +++++ .../src/utils/addOriginToSpan.ts | 0 .../{node => node-core}/src/utils/baggage.ts | 0 .../{node => node-core}/src/utils/commonjs.ts | 0 .../createMissingInstrumentationContext.ts | 0 .../{node => node-core}/src/utils/debug.ts | 0 .../src/utils/ensureIsWrapped.ts | 0 .../src/utils/entry-point.ts | 0 .../src/utils/envToBool.ts | 0 .../src/utils/errorhandling.ts | 0 .../src/utils/getRequestUrl.ts | 0 .../{node => node-core}/src/utils/module.ts | 0 .../src/utils/prepareEvent.ts | 0 packages/node-core/test/cron.test.ts | 224 ++++++ .../node-core/test/helpers/conditional.ts | 19 + packages/node-core/test/helpers/error.ts | 4 + .../helpers/getDefaultNodeClientOptions.ts | 13 + .../node-core/test/helpers/mockSdkInit.ts | 143 ++++ .../test/integration/breadcrumbs.test.ts | 358 +++++++++ .../node-core/test/integration/scope.test.ts | 684 +++++++++++++++++ .../test/integration/transactions.test.ts | 685 ++++++++++++++++++ .../test/integrations/context.test.ts | 0 .../test/integrations/contextlines.test.ts | 0 .../test/integrations/localvariables.test.ts | 0 .../request-session-tracking.test.ts | 0 .../test/integrations/spotlight.test.ts | 0 .../test/logs/exports.test.ts | 0 packages/node-core/test/sdk/api.test.ts | 104 +++ packages/node-core/test/sdk/client.test.ts | 324 +++++++++ packages/node-core/test/sdk/init.test.ts | 269 +++++++ .../test/transports/http.test.ts | 0 .../test/transports/https.test.ts | 0 .../test/transports/test-server-certs.ts | 48 ++ packages/node-core/test/tsconfig.json | 3 + .../test/utils/ensureIsWrapped.test.ts | 0 .../test/utils/entry-point.test.ts | 0 .../test/utils/envToBool.test.ts | 0 .../test/utils/getRequestUrl.test.ts | 0 .../test/utils/instrument.test.ts | 0 packages/node-core/test/utils/module.test.ts | 47 ++ packages/node-core/tsconfig.json | 10 + packages/node-core/tsconfig.test.json | 12 + packages/node-core/tsconfig.types.json | 10 + packages/node-core/vite.config.ts | 8 + packages/node/package.json | 1 + packages/node/rollup.npm.config.mjs | 26 - packages/node/src/index.ts | 49 +- packages/node/src/integrations/fs.ts | 2 +- packages/node/src/integrations/http/index.ts | 17 +- .../node/src/integrations/node-fetch/index.ts | 5 +- .../node/src/integrations/tracing/amqplib.ts | 3 +- .../node/src/integrations/tracing/connect.ts | 3 +- .../src/integrations/tracing/dataloader.ts | 2 +- .../node/src/integrations/tracing/express.ts | 4 +- .../src/integrations/tracing/fastify/index.ts | 2 +- .../src/integrations/tracing/genericPool.ts | 2 +- .../node/src/integrations/tracing/graphql.ts | 3 +- .../src/integrations/tracing/hapi/index.ts | 3 +- .../node/src/integrations/tracing/kafka.ts | 3 +- .../node/src/integrations/tracing/knex.ts | 2 +- packages/node/src/integrations/tracing/koa.ts | 4 +- .../src/integrations/tracing/lrumemoizer.ts | 2 +- .../node/src/integrations/tracing/mongo.ts | 3 +- .../node/src/integrations/tracing/mongoose.ts | 3 +- .../node/src/integrations/tracing/mysql.ts | 2 +- .../node/src/integrations/tracing/mysql2.ts | 3 +- .../node/src/integrations/tracing/postgres.ts | 3 +- .../src/integrations/tracing/postgresjs.ts | 3 +- .../node/src/integrations/tracing/prisma.ts | 2 +- .../node/src/integrations/tracing/redis.ts | 2 +- .../node/src/integrations/tracing/tedious.ts | 2 +- .../integrations/tracing/vercelai/index.ts | 3 +- packages/node/src/preload.ts | 2 +- packages/node/src/sdk/index.ts | 247 +------ packages/node/src/sdk/initOtel.ts | 28 +- packages/node/src/types.ts | 2 +- .../test/integrations/tracing/graphql.test.ts | 2 +- .../test/integrations/tracing/koa.test.ts | 2 +- .../test/integrations/tracing/mongo.test.ts | 2 +- packages/node/test/sdk/init.test.ts | 5 +- packages/remix/test/integration/package.json | 1 + 137 files changed, 4271 insertions(+), 358 deletions(-) create mode 100644 packages/node-core/.eslintrc.js create mode 100644 packages/node-core/LICENSE create mode 100644 packages/node-core/README.md create mode 100644 packages/node-core/package.json create mode 100644 packages/node-core/rollup.anr-worker.config.mjs create mode 100644 packages/node-core/rollup.npm.config.mjs rename packages/{node => node-core}/src/cron/common.ts (100%) rename packages/{node => node-core}/src/cron/cron.ts (100%) rename packages/{node => node-core}/src/cron/index.ts (100%) rename packages/{node => node-core}/src/cron/node-cron.ts (100%) rename packages/{node => node-core}/src/cron/node-schedule.ts (100%) create mode 100644 packages/node-core/src/debug-build.ts create mode 100644 packages/node-core/src/index.ts create mode 100644 packages/node-core/src/init.ts rename packages/{node => node-core}/src/integrations/anr/common.ts (100%) rename packages/{node => node-core}/src/integrations/anr/index.ts (100%) rename packages/{node => node-core}/src/integrations/anr/worker.ts (100%) rename packages/{node => node-core}/src/integrations/childProcess.ts (100%) rename packages/{node => node-core}/src/integrations/context.ts (100%) rename packages/{node => node-core}/src/integrations/contextlines.ts (100%) rename packages/{node => node-core}/src/integrations/diagnostic_channel.d.ts (100%) rename packages/{node => node-core}/src/integrations/http/SentryHttpInstrumentation.ts (100%) create mode 100644 packages/node-core/src/integrations/http/index.ts rename packages/{node => node-core}/src/integrations/local-variables/common.ts (100%) rename packages/{node => node-core}/src/integrations/local-variables/index.ts (100%) rename packages/{node => node-core}/src/integrations/local-variables/inspector.d.ts (100%) rename packages/{node => node-core}/src/integrations/local-variables/local-variables-async.ts (100%) rename packages/{node => node-core}/src/integrations/local-variables/local-variables-sync.ts (100%) rename packages/{node => node-core}/src/integrations/local-variables/worker.ts (100%) rename packages/{node => node-core}/src/integrations/modules.ts (100%) rename packages/{node => node-core}/src/integrations/node-fetch/SentryNodeFetchInstrumentation.ts (98%) create mode 100644 packages/node-core/src/integrations/node-fetch/index.ts create mode 100644 packages/node-core/src/integrations/node-fetch/types.ts rename packages/{node => node-core}/src/integrations/onuncaughtexception.ts (100%) rename packages/{node => node-core}/src/integrations/onunhandledrejection.ts (100%) rename packages/{node => node-core}/src/integrations/processSession.ts (100%) rename packages/{node => node-core}/src/integrations/spotlight.ts (100%) rename packages/{node => node-core}/src/integrations/winston.ts (100%) rename packages/{node => node-core}/src/logs/capture.ts (100%) rename packages/{node => node-core}/src/logs/exports.ts (100%) rename packages/{node => node-core}/src/nodeVersion.ts (100%) rename packages/{node => node-core}/src/otel/contextManager.ts (100%) rename packages/{node => node-core}/src/otel/instrument.ts (98%) rename packages/{node => node-core}/src/proxy/base.ts (100%) rename packages/{node => node-core}/src/proxy/helpers.ts (100%) rename packages/{node => node-core}/src/proxy/index.ts (98%) rename packages/{node => node-core}/src/proxy/parse-proxy-response.ts (100%) rename packages/{node => node-core}/src/sdk/api.ts (100%) rename packages/{node => node-core}/src/sdk/client.ts (97%) create mode 100644 packages/node-core/src/sdk/esmLoader.ts create mode 100644 packages/node-core/src/sdk/index.ts rename packages/{node => node-core}/src/sdk/scope.ts (100%) rename packages/{node => node-core}/src/transports/http-module.ts (100%) rename packages/{node => node-core}/src/transports/http.ts (100%) rename packages/{node => node-core}/src/transports/index.ts (100%) create mode 100644 packages/node-core/src/types.ts rename packages/{node => node-core}/src/utils/addOriginToSpan.ts (100%) rename packages/{node => node-core}/src/utils/baggage.ts (100%) rename packages/{node => node-core}/src/utils/commonjs.ts (100%) rename packages/{node => node-core}/src/utils/createMissingInstrumentationContext.ts (100%) rename packages/{node => node-core}/src/utils/debug.ts (100%) rename packages/{node => node-core}/src/utils/ensureIsWrapped.ts (100%) rename packages/{node => node-core}/src/utils/entry-point.ts (100%) rename packages/{node => node-core}/src/utils/envToBool.ts (100%) rename packages/{node => node-core}/src/utils/errorhandling.ts (100%) rename packages/{node => node-core}/src/utils/getRequestUrl.ts (100%) rename packages/{node => node-core}/src/utils/module.ts (100%) rename packages/{node => node-core}/src/utils/prepareEvent.ts (100%) create mode 100644 packages/node-core/test/cron.test.ts create mode 100644 packages/node-core/test/helpers/conditional.ts create mode 100644 packages/node-core/test/helpers/error.ts create mode 100644 packages/node-core/test/helpers/getDefaultNodeClientOptions.ts create mode 100644 packages/node-core/test/helpers/mockSdkInit.ts create mode 100644 packages/node-core/test/integration/breadcrumbs.test.ts create mode 100644 packages/node-core/test/integration/scope.test.ts create mode 100644 packages/node-core/test/integration/transactions.test.ts rename packages/{node => node-core}/test/integrations/context.test.ts (100%) rename packages/{node => node-core}/test/integrations/contextlines.test.ts (100%) rename packages/{node => node-core}/test/integrations/localvariables.test.ts (100%) rename packages/{node => node-core}/test/integrations/request-session-tracking.test.ts (100%) rename packages/{node => node-core}/test/integrations/spotlight.test.ts (100%) rename packages/{node => node-core}/test/logs/exports.test.ts (100%) create mode 100644 packages/node-core/test/sdk/api.test.ts create mode 100644 packages/node-core/test/sdk/client.test.ts create mode 100644 packages/node-core/test/sdk/init.test.ts rename packages/{node => node-core}/test/transports/http.test.ts (100%) rename packages/{node => node-core}/test/transports/https.test.ts (100%) create mode 100644 packages/node-core/test/transports/test-server-certs.ts create mode 100644 packages/node-core/test/tsconfig.json rename packages/{node => node-core}/test/utils/ensureIsWrapped.test.ts (100%) rename packages/{node => node-core}/test/utils/entry-point.test.ts (100%) rename packages/{node => node-core}/test/utils/envToBool.test.ts (100%) rename packages/{node => node-core}/test/utils/getRequestUrl.test.ts (100%) rename packages/{node => node-core}/test/utils/instrument.test.ts (100%) create mode 100644 packages/node-core/test/utils/module.test.ts create mode 100644 packages/node-core/tsconfig.json create mode 100644 packages/node-core/tsconfig.test.json create mode 100644 packages/node-core/tsconfig.types.json create mode 100644 packages/node-core/vite.config.ts diff --git a/package.json b/package.json index d6b8178ecc71..bdcadb520b22 100644 --- a/package.json +++ b/package.json @@ -68,6 +68,7 @@ "packages/nestjs", "packages/nextjs", "packages/node", + "packages/node-core", "packages/node-native", "packages/nuxt", "packages/opentelemetry", diff --git a/packages/node-core/.eslintrc.js b/packages/node-core/.eslintrc.js new file mode 100644 index 000000000000..6da218bd8641 --- /dev/null +++ b/packages/node-core/.eslintrc.js @@ -0,0 +1,9 @@ +module.exports = { + env: { + node: true, + }, + extends: ['../../.eslintrc.js'], + rules: { + '@sentry-internal/sdk/no-class-field-initializers': 'off', + }, +}; diff --git a/packages/node-core/LICENSE b/packages/node-core/LICENSE new file mode 100644 index 000000000000..b3c4b18a6317 --- /dev/null +++ b/packages/node-core/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 Functional Software, Inc. dba Sentry + +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 rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/packages/node-core/README.md b/packages/node-core/README.md new file mode 100644 index 000000000000..72ab816b21aa --- /dev/null +++ b/packages/node-core/README.md @@ -0,0 +1,116 @@ +

+ + Sentry + +

+ +# Official Sentry SDK for Node-Core + +[![npm version](https://img.shields.io/npm/v/@sentry/node-core.svg)](https://www.npmjs.com/package/@sentry/node-core) +[![npm dm](https://img.shields.io/npm/dm/@sentry/node-core.svg)](https://www.npmjs.com/package/@sentry/node-core) +[![npm dt](https://img.shields.io/npm/dt/@sentry/node-core.svg)](https://www.npmjs.com/package/@sentry/node-core) + +> [!CAUTION] +> This package is in alpha state and may be subject to breaking changes. + +Unlike the `@sentry/node` SDK, this SDK comes with no OpenTelemetry auto-instrumentation out of the box. It requires the following OpenTelemetry dependencies and supports both v1 and v2 of OpenTelemetry: + +- `@opentelemetry/api` +- `@opentelemetry/context-async-hooks` +- `@opentelemetry/core` +- `@opentelemetry/instrumentation` +- `@opentelemetry/resources` +- `@opentelemetry/sdk-trace-base` +- `@opentelemetry/semantic-conventions`. + +## Installation + +```bash +npm install @sentry/node-core @sentry/opentelemetry @opentelemetry/api @opentelemetry/core @opentelemetry/context-async-hooks @opentelemetry/instrumentation @opentelemetry/resources @opentelemetry/sdk-trace-base @opentelemetry/semantic-conventions + +# Or yarn +yarn add @sentry/node-core @sentry/opentelemetry @opentelemetry/api @opentelemetry/core @opentelemetry/context-async-hooks @opentelemetry/instrumentation @opentelemetry/resources @opentelemetry/sdk-trace-base @opentelemetry/semantic-conventions +``` + +## Usage + +Sentry should be initialized as early in your app as possible. It is essential that you call `Sentry.init` before you +require any other modules in your application, otherwise any auto-instrumentation will **not** work. +You also need to set up OpenTelemetry, if you prefer not to, consider using the `@sentry/node` SDK instead. + +You need to create a file named `instrument.js` that imports and initializes Sentry: + +```js +// CJS Syntax +const { trace, propagation, context } = require('@opentelemetry/api'); +const { NodeTracerProvider } = require('@opentelemetry/sdk-trace-node'); +const Sentry = require('@sentry/node-core'); +const { SentrySpanProcessor, SentryPropagator, SentrySampler } = require('@sentry/opentelemetry'); +// ESM Syntax +import { context, propagation, trace } from '@opentelemetry/api'; +import { NodeTracerProvider } from '@opentelemetry/sdk-trace-node'; +import * as Sentry from '@sentry/node-core'; +import { SentrySpanProcessor, SentryPropagator, SentrySampler } from '@sentry/opentelemetry'; + +const sentryClient = Sentry.init({ + dsn: '__DSN__', + // ... +}); + +// Note: This could be BasicTracerProvider or any other provider depending on how you want to use the +// OpenTelemetry SDK +const provider = new NodeTracerProvider({ + // Ensure the correct subset of traces is sent to Sentry + // This also ensures trace propagation works as expected + sampler: sentryClient ? new SentrySampler(sentryClient) : undefined, + spanProcessors: [ + // Ensure spans are correctly linked & sent to Sentry + new SentrySpanProcessor(), + // Add additional processors here + ], +}); + +trace.setGlobalTracerProvider(provider); +propagation.setGlobalPropagator(new SentryPropagator()); +context.setGlobalContextManager(new Sentry.SentryContextManager()); + +Sentry.validateOpenTelemetrySetup(); +``` + +You need to require or import the `instrument.js` file before importing any other modules in your application. This is +necessary to ensure that Sentry can automatically instrument all modules in your application: + +```js +// Import this first! +import './instrument'; + +// Now import other modules +import http from 'http'; + +// Your application code goes here +``` + +### ESM Support + +When running your application in ESM mode, you should use the Node.js +[`--import`](https://nodejs.org/api/cli.html#--importmodule) command line option to ensure that Sentry is loaded before +the application code is evaluated. + +Adjust the Node.js call for your application to use the `--import` parameter and point it at `instrument.js`, which +contains your `Sentry.init`() code: + +```bash +# Note: This is only available for Node v18.19.0 onwards. +node --import ./instrument.mjs app.mjs +``` + +If it is not possible for you to pass the `--import` flag to the Node.js binary, you can alternatively use the +`NODE_OPTIONS` environment variable as follows: + +```bash +NODE_OPTIONS="--import ./instrument.mjs" npm run start +``` + +## Links + +- [Official SDK Docs](https://docs.sentry.io/quickstart/) diff --git a/packages/node-core/package.json b/packages/node-core/package.json new file mode 100644 index 000000000000..531485ede044 --- /dev/null +++ b/packages/node-core/package.json @@ -0,0 +1,109 @@ +{ + "name": "@sentry/node-core", + "version": "9.34.0", + "description": "Sentry Node-Core SDK", + "repository": "git://github.com/getsentry/sentry-javascript.git", + "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/node-core", + "author": "Sentry", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "files": [ + "/build" + ], + "main": "build/cjs/index.js", + "module": "build/esm/index.js", + "types": "build/types/index.d.ts", + "exports": { + "./package.json": "./package.json", + ".": { + "import": { + "types": "./build/types/index.d.ts", + "default": "./build/esm/index.js" + }, + "require": { + "types": "./build/types/index.d.ts", + "default": "./build/cjs/index.js" + } + }, + "./import": { + "import": { + "default": "./build/import-hook.mjs" + } + }, + "./loader": { + "import": { + "default": "./build/loader-hook.mjs" + } + }, + "./init": { + "import": { + "default": "./build/esm/init.js" + }, + "require": { + "default": "./build/cjs/init.js" + } + } + }, + "typesVersions": { + "<5.0": { + "build/types/index.d.ts": [ + "build/types-ts3.8/index.d.ts" + ] + } + }, + "publishConfig": { + "access": "public" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.9.0", + "@opentelemetry/context-async-hooks": "^1.30.1 || ^2.0.0", + "@opentelemetry/core": "^1.30.1 || ^2.0.0", + "@opentelemetry/instrumentation": "^0.57.1 || ^0.200.0", + "@opentelemetry/resources": "^1.30.1 || ^2.0.0", + "@opentelemetry/sdk-trace-base": "^1.30.1 || ^2.0.0", + "@opentelemetry/semantic-conventions": "^1.34.0" + }, + "dependencies": { + "@sentry/core": "9.34.0", + "@sentry/opentelemetry": "9.34.0", + "import-in-the-middle": "^1.14.2" + }, + "devDependencies": { + "@opentelemetry/api": "^1.9.0", + "@opentelemetry/context-async-hooks": "^1.30.1", + "@opentelemetry/core": "^1.30.1", + "@opentelemetry/instrumentation": "^0.57.2", + "@opentelemetry/resources": "^1.30.1", + "@opentelemetry/sdk-trace-base": "^1.30.1", + "@opentelemetry/semantic-conventions": "^1.34.0", + "@types/node": "^18.19.1" + }, + "scripts": { + "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", + "build:types:core": "tsc -p tsconfig.types.json", + "build:types:downlevel": "yarn downlevel-dts build/types build/types-ts3.8 --to ts3.8", + "build:watch": "run-p build:transpile:watch build:types:watch", + "build:dev:watch": "yarn build:watch", + "build:transpile:watch": "rollup -c rollup.npm.config.mjs --watch", + "build:types:watch": "tsc -p tsconfig.types.json --watch", + "build:tarball": "npm pack", + "circularDepCheck": "madge --circular src/index.ts", + "clean": "rimraf build coverage sentry-node-*.tgz", + "fix": "eslint . --format stylish --fix", + "lint": "eslint . --format stylish", + "lint:es-compatibility": "es-check es2022 ./build/cjs/*.js && es-check es2022 ./build/esm/*.js --module", + "test": "yarn test:unit", + "test:unit": "vitest run", + "test:watch": "vitest --watch", + "yalc:publish": "yalc publish --push --sig" + }, + "volta": { + "extends": "../../package.json" + }, + "sideEffects": false +} diff --git a/packages/node-core/rollup.anr-worker.config.mjs b/packages/node-core/rollup.anr-worker.config.mjs new file mode 100644 index 000000000000..e12c21f5ae72 --- /dev/null +++ b/packages/node-core/rollup.anr-worker.config.mjs @@ -0,0 +1,31 @@ +import { makeBaseBundleConfig } from '@sentry-internal/rollup-utils'; + +export function createWorkerCodeBuilder(entry, outDir) { + let base64Code; + + return [ + makeBaseBundleConfig({ + bundleType: 'node-worker', + entrypoints: [entry], + licenseTitle: '@sentry/node-core', + outputFileBase: () => 'worker-script.js', + packageSpecificConfig: { + output: { + dir: outDir, + sourcemap: false, + }, + plugins: [ + { + name: 'output-base64-worker-script', + renderChunk(code) { + base64Code = Buffer.from(code).toString('base64'); + }, + }, + ], + }, + }), + () => { + return base64Code; + }, + ]; +} diff --git a/packages/node-core/rollup.npm.config.mjs b/packages/node-core/rollup.npm.config.mjs new file mode 100644 index 000000000000..8e18333836ef --- /dev/null +++ b/packages/node-core/rollup.npm.config.mjs @@ -0,0 +1,43 @@ +import replace from '@rollup/plugin-replace'; +import { makeBaseNPMConfig, makeNPMConfigVariants, makeOtelLoaders } from '@sentry-internal/rollup-utils'; +import { createWorkerCodeBuilder } from './rollup.anr-worker.config.mjs'; + +const [anrWorkerConfig, getAnrBase64Code] = createWorkerCodeBuilder( + 'src/integrations/anr/worker.ts', + 'build/esm/integrations/anr', +); + +const [localVariablesWorkerConfig, getLocalVariablesBase64Code] = createWorkerCodeBuilder( + 'src/integrations/local-variables/worker.ts', + 'build/esm/integrations/local-variables', +); + +export default [ + ...makeOtelLoaders('./build', 'otel'), + // The workers needs to be built first since it's their output is copied in the main bundle. + anrWorkerConfig, + localVariablesWorkerConfig, + ...makeNPMConfigVariants( + makeBaseNPMConfig({ + entrypoints: ['src/index.ts', 'src/init.ts'], + packageSpecificConfig: { + output: { + // set exports to 'named' or 'auto' so that rollup doesn't warn + exports: 'named', + preserveModules: true, + }, + plugins: [ + replace({ + delimiters: ['###', '###'], + // removes some rollup warnings + preventAssignment: true, + values: { + AnrWorkerScript: () => getAnrBase64Code(), + LocalVariablesWorkerScript: () => getLocalVariablesBase64Code(), + }, + }), + ], + }, + }), + ), +]; diff --git a/packages/node/src/cron/common.ts b/packages/node-core/src/cron/common.ts similarity index 100% rename from packages/node/src/cron/common.ts rename to packages/node-core/src/cron/common.ts diff --git a/packages/node/src/cron/cron.ts b/packages/node-core/src/cron/cron.ts similarity index 100% rename from packages/node/src/cron/cron.ts rename to packages/node-core/src/cron/cron.ts diff --git a/packages/node/src/cron/index.ts b/packages/node-core/src/cron/index.ts similarity index 100% rename from packages/node/src/cron/index.ts rename to packages/node-core/src/cron/index.ts diff --git a/packages/node/src/cron/node-cron.ts b/packages/node-core/src/cron/node-cron.ts similarity index 100% rename from packages/node/src/cron/node-cron.ts rename to packages/node-core/src/cron/node-cron.ts diff --git a/packages/node/src/cron/node-schedule.ts b/packages/node-core/src/cron/node-schedule.ts similarity index 100% rename from packages/node/src/cron/node-schedule.ts rename to packages/node-core/src/cron/node-schedule.ts diff --git a/packages/node-core/src/debug-build.ts b/packages/node-core/src/debug-build.ts new file mode 100644 index 000000000000..60aa50940582 --- /dev/null +++ b/packages/node-core/src/debug-build.ts @@ -0,0 +1,8 @@ +declare const __DEBUG_BUILD__: boolean; + +/** + * This serves as a build time flag that will be true by default, but false in non-debug builds or if users replace `__SENTRY_DEBUG__` in their generated code. + * + * ATTENTION: This constant must never cross package boundaries (i.e. be exported) to guarantee that it can be used for tree shaking. + */ +export const DEBUG_BUILD = __DEBUG_BUILD__; diff --git a/packages/node-core/src/index.ts b/packages/node-core/src/index.ts new file mode 100644 index 000000000000..2e8e16daa04c --- /dev/null +++ b/packages/node-core/src/index.ts @@ -0,0 +1,151 @@ +import * as logger from './logs/exports'; + +export { httpIntegration } from './integrations/http'; +export { + SentryHttpInstrumentation, + type SentryHttpInstrumentationOptions, +} from './integrations/http/SentryHttpInstrumentation'; +export { nativeNodeFetchIntegration } from './integrations/node-fetch'; +export { + SentryNodeFetchInstrumentation, + type SentryNodeFetchInstrumentationOptions, +} from './integrations/node-fetch/SentryNodeFetchInstrumentation'; + +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, disableAnrDetectionForCallback } from './integrations/anr'; + +export { spotlightIntegration } from './integrations/spotlight'; +export { childProcessIntegration } from './integrations/childProcess'; +export { createSentryWinstonTransport } from './integrations/winston'; + +export { SentryContextManager } from './otel/contextManager'; +export { generateInstrumentOnce, instrumentWhenWrapped, INSTRUMENTED } from './otel/instrument'; + +export { init, getDefaultIntegrations, initWithoutDefaultIntegrations, validateOpenTelemetrySetup } from './sdk'; +export { setIsolationScope } from './sdk/scope'; +export { getSentryRelease, defaultStackParser } from './sdk/api'; +export { createGetModuleFromFilename } from './utils/module'; +export { addOriginToSpan } from './utils/addOriginToSpan'; +export { getRequestUrl } from './utils/getRequestUrl'; +export { isCjs } from './utils/commonjs'; +export { ensureIsWrapped } from './utils/ensureIsWrapped'; +export { createMissingInstrumentationContext } from './utils/createMissingInstrumentationContext'; +export { envToBool } from './utils/envToBool'; +export { makeNodeTransport, type NodeTransportOptions } from './transports'; +export type { HTTPModuleRequestIncomingMessage } from './transports/http-module'; +export { NodeClient } from './sdk/client'; +export { cron } from './cron'; +export { NODE_VERSION } from './nodeVersion'; + +export type { NodeOptions } from './types'; + +export { + // This needs exporting so the NodeClient can be used without calling init + setOpenTelemetryContextAsyncContextStrategy as setNodeAsyncContextStrategy, +} from '@sentry/opentelemetry'; + +export { + addBreadcrumb, + isInitialized, + isEnabled, + getGlobalScope, + lastEventId, + close, + createTransport, + flush, + SDK_VERSION, + getSpanStatusFromHttpCode, + setHttpStatus, + captureCheckIn, + withMonitor, + requestDataIntegration, + functionToStringIntegration, + // eslint-disable-next-line deprecation/deprecation + inboundFiltersIntegration, + eventFiltersIntegration, + linkedErrorsIntegration, + addEventProcessor, + setContext, + setExtra, + setExtras, + setTag, + setTags, + setUser, + SEMANTIC_ATTRIBUTE_SENTRY_OP, + SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, + SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, + SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE, + setCurrentClient, + Scope, + setMeasurement, + getSpanDescendants, + parameterize, + getClient, + getCurrentScope, + getIsolationScope, + getTraceData, + getTraceMetaTags, + continueTrace, + withScope, + withIsolationScope, + captureException, + captureEvent, + captureMessage, + captureFeedback, + captureConsoleIntegration, + dedupeIntegration, + extraErrorDataIntegration, + rewriteFramesIntegration, + startSession, + captureSession, + endSession, + addIntegration, + startSpan, + startSpanManual, + startInactiveSpan, + startNewTrace, + suppressTracing, + getActiveSpan, + withActiveSpan, + getRootSpan, + spanToJSON, + spanToTraceHeader, + spanToBaggageHeader, + trpcMiddleware, + updateSpanName, + supabaseIntegration, + instrumentSupabaseClient, + zodErrorsIntegration, + profiler, + consoleLoggingIntegration, + consoleIntegration, + wrapMcpServerWithSentry, + featureFlagsIntegration, +} from '@sentry/core'; + +export type { + Breadcrumb, + BreadcrumbHint, + PolymorphicRequest, + RequestEventData, + SdkInfo, + Event, + EventHint, + ErrorEvent, + Exception, + Session, + SeverityLevel, + StackFrame, + Stacktrace, + Thread, + User, + Span, + FeatureFlagsIntegration, +} from '@sentry/core'; + +export { logger }; diff --git a/packages/node-core/src/init.ts b/packages/node-core/src/init.ts new file mode 100644 index 000000000000..7dc0cb1546a3 --- /dev/null +++ b/packages/node-core/src/init.ts @@ -0,0 +1,9 @@ +import { init } from './sdk'; + +/** + * The @sentry/node-core/init export can be used with the node --import and --require args to initialize the SDK entirely via + * environment variables. + * + * > SENTRY_DSN=https://examplePublicKey@o0.ingest.sentry.io/0 SENTRY_TRACES_SAMPLE_RATE=1.0 node --import=@sentry/node/init app.mjs + */ +init(); diff --git a/packages/node/src/integrations/anr/common.ts b/packages/node-core/src/integrations/anr/common.ts similarity index 100% rename from packages/node/src/integrations/anr/common.ts rename to packages/node-core/src/integrations/anr/common.ts diff --git a/packages/node/src/integrations/anr/index.ts b/packages/node-core/src/integrations/anr/index.ts similarity index 100% rename from packages/node/src/integrations/anr/index.ts rename to packages/node-core/src/integrations/anr/index.ts diff --git a/packages/node/src/integrations/anr/worker.ts b/packages/node-core/src/integrations/anr/worker.ts similarity index 100% rename from packages/node/src/integrations/anr/worker.ts rename to packages/node-core/src/integrations/anr/worker.ts diff --git a/packages/node/src/integrations/childProcess.ts b/packages/node-core/src/integrations/childProcess.ts similarity index 100% rename from packages/node/src/integrations/childProcess.ts rename to packages/node-core/src/integrations/childProcess.ts diff --git a/packages/node/src/integrations/context.ts b/packages/node-core/src/integrations/context.ts similarity index 100% rename from packages/node/src/integrations/context.ts rename to packages/node-core/src/integrations/context.ts diff --git a/packages/node/src/integrations/contextlines.ts b/packages/node-core/src/integrations/contextlines.ts similarity index 100% rename from packages/node/src/integrations/contextlines.ts rename to packages/node-core/src/integrations/contextlines.ts diff --git a/packages/node/src/integrations/diagnostic_channel.d.ts b/packages/node-core/src/integrations/diagnostic_channel.d.ts similarity index 100% rename from packages/node/src/integrations/diagnostic_channel.d.ts rename to packages/node-core/src/integrations/diagnostic_channel.d.ts diff --git a/packages/node/src/integrations/http/SentryHttpInstrumentation.ts b/packages/node-core/src/integrations/http/SentryHttpInstrumentation.ts similarity index 100% rename from packages/node/src/integrations/http/SentryHttpInstrumentation.ts rename to packages/node-core/src/integrations/http/SentryHttpInstrumentation.ts diff --git a/packages/node-core/src/integrations/http/index.ts b/packages/node-core/src/integrations/http/index.ts new file mode 100644 index 000000000000..8bd69c22a8e7 --- /dev/null +++ b/packages/node-core/src/integrations/http/index.ts @@ -0,0 +1,141 @@ +import type { IncomingMessage, RequestOptions } from 'node:http'; +import { defineIntegration } from '@sentry/core'; +import { generateInstrumentOnce } from '../../otel/instrument'; +import type { SentryHttpInstrumentationOptions } from './SentryHttpInstrumentation'; +import { SentryHttpInstrumentation } from './SentryHttpInstrumentation'; + +const INTEGRATION_NAME = 'Http'; + +interface HttpOptions { + /** + * Whether breadcrumbs should be recorded for outgoing requests. + * Defaults to true + */ + breadcrumbs?: boolean; + + /** + * Whether the integration should create [Sessions](https://docs.sentry.io/product/releases/health/#sessions) for incoming requests to track the health and crash-free rate of your releases in Sentry. + * Read more about Release Health: https://docs.sentry.io/product/releases/health/ + * + * Defaults to `true`. + */ + trackIncomingRequestsAsSessions?: boolean; + + /** + * Number of milliseconds until sessions tracked with `trackIncomingRequestsAsSessions` will be flushed as a session aggregate. + * + * Defaults to `60000` (60s). + */ + sessionFlushingDelayMS?: number; + + /** + * 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. + * + * The `url` param contains the entire URL, including query string (if any), protocol, host, etc. of the outgoing request. + * For example: `'https://someService.com/users/details?id=123'` + * + * The `request` param contains the original {@type RequestOptions} object used to make the outgoing request. + * You can use it to filter on additional properties like method, headers, etc. + */ + ignoreOutgoingRequests?: (url: string, request: RequestOptions) => boolean; + + /** + * Do not capture spans for incoming HTTP requests to URLs where the given callback returns `true`. + * Spans will be non recording if tracing is disabled. + * + * The `urlPath` param consists of the URL path and query string (if any) of the incoming request. + * For example: `'/users/details?id=123'` + * + * The `request` param contains the original {@type IncomingMessage} object of the incoming request. + * You can use it to filter on additional properties like method, headers, etc. + */ + ignoreIncomingRequests?: (urlPath: string, request: IncomingMessage) => boolean; + + /** + * Do not capture spans for incoming HTTP requests with the given status codes. + * By default, spans with 404 status code are ignored. + * Expects an array of status codes or a range of status codes, e.g. [[300,399], 404] would ignore 3xx and 404 status codes. + * + * @default `[404]` + */ + dropSpansForIncomingRequestStatusCodes?: (number | [number, number])[]; + + /** + * Do not capture the request body for incoming HTTP requests to URLs where the given callback returns `true`. + * This can be useful for long running requests where the body is not needed and we want to avoid capturing it. + * + * @param url Contains the entire URL, including query string (if any), protocol, host, etc. of the incoming request. + * @param request Contains the {@type RequestOptions} object used to make the incoming request. + */ + ignoreIncomingRequestBody?: (url: string, request: RequestOptions) => boolean; + + /** + * Controls the maximum size of incoming HTTP request bodies attached to events. + * + * Available options: + * - 'none': No request bodies will be attached + * - 'small': Request bodies up to 1,000 bytes will be attached + * - 'medium': Request bodies up to 10,000 bytes will be attached (default) + * - 'always': Request bodies will always be attached + * + * Note that even with 'always' setting, bodies exceeding 1MB will never be attached + * for performance and security reasons. + * + * @default 'medium' + */ + maxIncomingRequestBodySize?: 'none' | 'small' | 'medium' | 'always'; + + /** + * If true, do not generate spans for incoming requests at all. + * This is used by Remix to avoid generating spans for incoming requests, as it generates its own spans. + */ + disableIncomingRequestSpans?: boolean; +} + +const instrumentSentryHttp = generateInstrumentOnce( + `${INTEGRATION_NAME}.sentry`, + options => { + return new SentryHttpInstrumentation(options); + }, +); + +/** + * The http integration instruments Node's internal http and https modules. + * It creates breadcrumbs for outgoing HTTP requests which will be attached to the currently active span. + */ +export const httpIntegration = defineIntegration((options: HttpOptions = {}) => { + const dropSpansForIncomingRequestStatusCodes = options.dropSpansForIncomingRequestStatusCodes ?? [404]; + + return { + name: INTEGRATION_NAME, + setupOnce() { + instrumentSentryHttp({ + ...options, + extractIncomingTraceFromHeader: true, + propagateTraceInOutgoingRequests: true, + }); + }, + processEvent(event) { + // Drop transaction if it has a status code that should be ignored + if (event.type === 'transaction') { + const statusCode = event.contexts?.trace?.data?.['http.response.status_code']; + if ( + typeof statusCode === 'number' && + dropSpansForIncomingRequestStatusCodes.some(code => { + if (typeof code === 'number') { + return code === statusCode; + } + + const [min, max] = code; + return statusCode >= min && statusCode <= max; + }) + ) { + return null; + } + } + + return event; + }, + }; +}); diff --git a/packages/node/src/integrations/local-variables/common.ts b/packages/node-core/src/integrations/local-variables/common.ts similarity index 100% rename from packages/node/src/integrations/local-variables/common.ts rename to packages/node-core/src/integrations/local-variables/common.ts diff --git a/packages/node/src/integrations/local-variables/index.ts b/packages/node-core/src/integrations/local-variables/index.ts similarity index 100% rename from packages/node/src/integrations/local-variables/index.ts rename to packages/node-core/src/integrations/local-variables/index.ts diff --git a/packages/node/src/integrations/local-variables/inspector.d.ts b/packages/node-core/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-core/src/integrations/local-variables/inspector.d.ts diff --git a/packages/node/src/integrations/local-variables/local-variables-async.ts b/packages/node-core/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-core/src/integrations/local-variables/local-variables-async.ts diff --git a/packages/node/src/integrations/local-variables/local-variables-sync.ts b/packages/node-core/src/integrations/local-variables/local-variables-sync.ts similarity index 100% rename from packages/node/src/integrations/local-variables/local-variables-sync.ts rename to packages/node-core/src/integrations/local-variables/local-variables-sync.ts diff --git a/packages/node/src/integrations/local-variables/worker.ts b/packages/node-core/src/integrations/local-variables/worker.ts similarity index 100% rename from packages/node/src/integrations/local-variables/worker.ts rename to packages/node-core/src/integrations/local-variables/worker.ts diff --git a/packages/node/src/integrations/modules.ts b/packages/node-core/src/integrations/modules.ts similarity index 100% rename from packages/node/src/integrations/modules.ts rename to packages/node-core/src/integrations/modules.ts diff --git a/packages/node/src/integrations/node-fetch/SentryNodeFetchInstrumentation.ts b/packages/node-core/src/integrations/node-fetch/SentryNodeFetchInstrumentation.ts similarity index 98% rename from packages/node/src/integrations/node-fetch/SentryNodeFetchInstrumentation.ts rename to packages/node-core/src/integrations/node-fetch/SentryNodeFetchInstrumentation.ts index 2ee9f55c78a1..e17bbe58454a 100644 --- a/packages/node/src/integrations/node-fetch/SentryNodeFetchInstrumentation.ts +++ b/packages/node-core/src/integrations/node-fetch/SentryNodeFetchInstrumentation.ts @@ -1,5 +1,5 @@ import { context } from '@opentelemetry/api'; -import { isTracingSuppressed, VERSION } from '@opentelemetry/core'; +import { isTracingSuppressed } from '@opentelemetry/core'; import type { InstrumentationConfig } from '@opentelemetry/instrumentation'; import { InstrumentationBase } from '@opentelemetry/instrumentation'; import type { SanitizedRequestData } from '@sentry/core'; @@ -24,6 +24,9 @@ const SENTRY_BAGGAGE_HEADER = 'baggage'; // For baggage, we make sure to merge this into a possibly existing header const BAGGAGE_HEADER_REGEX = /baggage: (.*)\r\n/; +// Bump this whenever we make changes +const VERSION = '1.0.0'; + export type SentryNodeFetchInstrumentationOptions = InstrumentationConfig & { /** * Whether breadcrumbs should be recorded for requests. diff --git a/packages/node-core/src/integrations/node-fetch/index.ts b/packages/node-core/src/integrations/node-fetch/index.ts new file mode 100644 index 000000000000..4ca41bddfda8 --- /dev/null +++ b/packages/node-core/src/integrations/node-fetch/index.ts @@ -0,0 +1,39 @@ +import type { IntegrationFn } from '@sentry/core'; +import { defineIntegration } from '@sentry/core'; +import { generateInstrumentOnce } from '../../otel/instrument'; +import { SentryNodeFetchInstrumentation } from './SentryNodeFetchInstrumentation'; + +const INTEGRATION_NAME = 'NodeFetch'; + +interface NodeFetchOptions { + /** + * Whether breadcrumbs should be recorded for requests. + * Defaults to true + */ + breadcrumbs?: boolean; + + /** + * Do not capture spans or breadcrumbs for outgoing fetch requests to URLs where the given callback returns `true`. + * This controls both span & breadcrumb creation - spans will be non recording if tracing is disabled. + */ + ignoreOutgoingRequests?: (url: string) => boolean; +} + +const instrumentSentryNodeFetch = generateInstrumentOnce( + `${INTEGRATION_NAME}.sentry`, + SentryNodeFetchInstrumentation, + (options: NodeFetchOptions) => { + return options; + }, +); + +const _nativeNodeFetchIntegration = ((options: NodeFetchOptions = {}) => { + return { + name: 'NodeFetch', + setupOnce() { + instrumentSentryNodeFetch(options); + }, + }; +}) satisfies IntegrationFn; + +export const nativeNodeFetchIntegration = defineIntegration(_nativeNodeFetchIntegration); diff --git a/packages/node-core/src/integrations/node-fetch/types.ts b/packages/node-core/src/integrations/node-fetch/types.ts new file mode 100644 index 000000000000..0139dadde413 --- /dev/null +++ b/packages/node-core/src/integrations/node-fetch/types.ts @@ -0,0 +1,47 @@ +/* + * Copyright The OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Vendored from https://github.com/open-telemetry/opentelemetry-js-contrib/blob/28e209a9da36bc4e1f8c2b0db7360170ed46cb80/plugins/node/instrumentation-undici/src/types.ts + */ + +export interface UndiciRequest { + origin: string; + method: string; + path: string; + /** + * Serialized string of headers in the form `name: value\r\n` for v5 + * Array of strings v6 + */ + headers: string | string[]; + /** + * Helper method to add headers (from v6) + */ + addHeader: (name: string, value: string) => void; + throwOnError: boolean; + completed: boolean; + aborted: boolean; + idempotent: boolean; + contentLength: number | null; + contentType: string | null; + body: unknown; +} + +export interface UndiciResponse { + headers: Buffer[]; + statusCode: number; + statusText: string; +} diff --git a/packages/node/src/integrations/onuncaughtexception.ts b/packages/node-core/src/integrations/onuncaughtexception.ts similarity index 100% rename from packages/node/src/integrations/onuncaughtexception.ts rename to packages/node-core/src/integrations/onuncaughtexception.ts diff --git a/packages/node/src/integrations/onunhandledrejection.ts b/packages/node-core/src/integrations/onunhandledrejection.ts similarity index 100% rename from packages/node/src/integrations/onunhandledrejection.ts rename to packages/node-core/src/integrations/onunhandledrejection.ts diff --git a/packages/node/src/integrations/processSession.ts b/packages/node-core/src/integrations/processSession.ts similarity index 100% rename from packages/node/src/integrations/processSession.ts rename to packages/node-core/src/integrations/processSession.ts diff --git a/packages/node/src/integrations/spotlight.ts b/packages/node-core/src/integrations/spotlight.ts similarity index 100% rename from packages/node/src/integrations/spotlight.ts rename to packages/node-core/src/integrations/spotlight.ts diff --git a/packages/node/src/integrations/winston.ts b/packages/node-core/src/integrations/winston.ts similarity index 100% rename from packages/node/src/integrations/winston.ts rename to packages/node-core/src/integrations/winston.ts diff --git a/packages/node/src/logs/capture.ts b/packages/node-core/src/logs/capture.ts similarity index 100% rename from packages/node/src/logs/capture.ts rename to packages/node-core/src/logs/capture.ts diff --git a/packages/node/src/logs/exports.ts b/packages/node-core/src/logs/exports.ts similarity index 100% rename from packages/node/src/logs/exports.ts rename to packages/node-core/src/logs/exports.ts diff --git a/packages/node/src/nodeVersion.ts b/packages/node-core/src/nodeVersion.ts similarity index 100% rename from packages/node/src/nodeVersion.ts rename to packages/node-core/src/nodeVersion.ts diff --git a/packages/node/src/otel/contextManager.ts b/packages/node-core/src/otel/contextManager.ts similarity index 100% rename from packages/node/src/otel/contextManager.ts rename to packages/node-core/src/otel/contextManager.ts diff --git a/packages/node/src/otel/instrument.ts b/packages/node-core/src/otel/instrument.ts similarity index 98% rename from packages/node/src/otel/instrument.ts rename to packages/node-core/src/otel/instrument.ts index c5e94991140a..6f19190afe91 100644 --- a/packages/node/src/otel/instrument.ts +++ b/packages/node-core/src/otel/instrument.ts @@ -25,6 +25,7 @@ export function generateInstrumentOnce< */ export function generateInstrumentOnce( name: string, + // eslint-disable-next-line @typescript-eslint/no-explicit-any creatorOrClass: (new (...args: any[]) => Instrumentation) | ((options?: Options) => Instrumentation), optionsCallback?: (options: Options) => unknown, ): ((options: Options) => Instrumentation) & { id: string } { diff --git a/packages/node/src/proxy/base.ts b/packages/node-core/src/proxy/base.ts similarity index 100% rename from packages/node/src/proxy/base.ts rename to packages/node-core/src/proxy/base.ts diff --git a/packages/node/src/proxy/helpers.ts b/packages/node-core/src/proxy/helpers.ts similarity index 100% rename from packages/node/src/proxy/helpers.ts rename to packages/node-core/src/proxy/helpers.ts diff --git a/packages/node/src/proxy/index.ts b/packages/node-core/src/proxy/index.ts similarity index 98% rename from packages/node/src/proxy/index.ts rename to packages/node-core/src/proxy/index.ts index 788b302eeab3..8ff6741623b2 100644 --- a/packages/node/src/proxy/index.ts +++ b/packages/node-core/src/proxy/index.ts @@ -28,7 +28,9 @@ /* eslint-disable @typescript-eslint/explicit-member-accessibility */ /* eslint-disable @typescript-eslint/no-unused-vars */ +// eslint-disable-next-line import/no-duplicates import type * as http from 'node:http'; +// eslint-disable-next-line import/no-duplicates import type { OutgoingHttpHeaders } from 'node:http'; import * as net from 'node:net'; import * as tls from 'node:tls'; diff --git a/packages/node/src/proxy/parse-proxy-response.ts b/packages/node-core/src/proxy/parse-proxy-response.ts similarity index 100% rename from packages/node/src/proxy/parse-proxy-response.ts rename to packages/node-core/src/proxy/parse-proxy-response.ts diff --git a/packages/node/src/sdk/api.ts b/packages/node-core/src/sdk/api.ts similarity index 100% rename from packages/node/src/sdk/api.ts rename to packages/node-core/src/sdk/api.ts diff --git a/packages/node/src/sdk/client.ts b/packages/node-core/src/sdk/client.ts similarity index 97% rename from packages/node/src/sdk/client.ts rename to packages/node-core/src/sdk/client.ts index 9bfb83bc6d99..022e3cb1ac33 100644 --- a/packages/node/src/sdk/client.ts +++ b/packages/node-core/src/sdk/client.ts @@ -83,11 +83,8 @@ export class NodeClient extends ServerRuntimeClient { // eslint-disable-next-line jsdoc/require-jsdoc public async flush(timeout?: number): Promise { const provider = this.traceProvider; - const spanProcessor = provider?.activeSpanProcessor; - if (spanProcessor) { - await spanProcessor.forceFlush(); - } + await provider?.forceFlush(); if (this.getOptions().sendClientReports) { this._flushOutcomes(); diff --git a/packages/node-core/src/sdk/esmLoader.ts b/packages/node-core/src/sdk/esmLoader.ts new file mode 100644 index 000000000000..487e2ce0c613 --- /dev/null +++ b/packages/node-core/src/sdk/esmLoader.ts @@ -0,0 +1,31 @@ +import { consoleSandbox, GLOBAL_OBJ, logger } from '@sentry/core'; +import { createAddHookMessageChannel } from 'import-in-the-middle'; +import moduleModule from 'module'; + +/** Initialize the ESM loader. */ +export function maybeInitializeEsmLoader(): void { + const [nodeMajor = 0, nodeMinor = 0] = process.versions.node.split('.').map(Number); + + // Register hook was added in v20.6.0 and v18.19.0 + if (nodeMajor >= 21 || (nodeMajor === 20 && nodeMinor >= 6) || (nodeMajor === 18 && nodeMinor >= 19)) { + if (!GLOBAL_OBJ._sentryEsmLoaderHookRegistered) { + try { + const { addHookMessagePort } = createAddHookMessageChannel(); + // @ts-expect-error register is available in these versions + moduleModule.register('import-in-the-middle/hook.mjs', import.meta.url, { + data: { addHookMessagePort, include: [] }, + transferList: [addHookMessagePort], + }); + } catch (error) { + logger.warn('Failed to register ESM hook', error); + } + } + } else { + consoleSandbox(() => { + // eslint-disable-next-line no-console + console.warn( + `[Sentry] You are using Node.js v${process.versions.node} in ESM mode ("import syntax"). The Sentry Node.js SDK is not compatible with ESM in Node.js versions before 18.19.0 or before 20.6.0. Please either build your application with CommonJS ("require() syntax"), or upgrade your Node.js version.`, + ); + }); + } +} diff --git a/packages/node-core/src/sdk/index.ts b/packages/node-core/src/sdk/index.ts new file mode 100644 index 000000000000..c2e5d2c6e5c9 --- /dev/null +++ b/packages/node-core/src/sdk/index.ts @@ -0,0 +1,267 @@ +import { diag, DiagLogLevel } from '@opentelemetry/api'; +import type { Integration, Options } from '@sentry/core'; +import { + consoleIntegration, + consoleSandbox, + functionToStringIntegration, + getCurrentScope, + getIntegrationsToSetup, + hasSpansEnabled, + inboundFiltersIntegration, + linkedErrorsIntegration, + logger, + propagationContextFromHeaders, + requestDataIntegration, + stackParserFromStackParserOptions, +} from '@sentry/core'; +import { + enhanceDscWithOpenTelemetryRootSpanName, + openTelemetrySetupCheck, + setOpenTelemetryContextAsyncContextStrategy, + setupEventContextTrace, +} from '@sentry/opentelemetry'; +import { DEBUG_BUILD } from '../debug-build'; +import { childProcessIntegration } from '../integrations/childProcess'; +import { nodeContextIntegration } from '../integrations/context'; +import { contextLinesIntegration } from '../integrations/contextlines'; +import { httpIntegration } from '../integrations/http'; +import { localVariablesIntegration } from '../integrations/local-variables'; +import { modulesIntegration } from '../integrations/modules'; +import { nativeNodeFetchIntegration } from '../integrations/node-fetch'; +import { onUncaughtExceptionIntegration } from '../integrations/onuncaughtexception'; +import { onUnhandledRejectionIntegration } from '../integrations/onunhandledrejection'; +import { processSessionIntegration } from '../integrations/processSession'; +import { INTEGRATION_NAME as SPOTLIGHT_INTEGRATION_NAME, spotlightIntegration } from '../integrations/spotlight'; +import { makeNodeTransport } from '../transports'; +import type { NodeClientOptions, NodeOptions } from '../types'; +import { isCjs } from '../utils/commonjs'; +import { envToBool } from '../utils/envToBool'; +import { defaultStackParser, getSentryRelease } from './api'; +import { NodeClient } from './client'; +import { maybeInitializeEsmLoader } from './esmLoader'; + +/** + * Get default integrations for the Node-Core SDK. + */ +export function getDefaultIntegrations(): Integration[] { + return [ + // Common + // TODO(v10): Replace with `eventFiltersIntegration` once we remove the deprecated `inboundFiltersIntegration` + // eslint-disable-next-line deprecation/deprecation + inboundFiltersIntegration(), + functionToStringIntegration(), + linkedErrorsIntegration(), + requestDataIntegration(), + // Native Wrappers + consoleIntegration(), + httpIntegration(), + nativeNodeFetchIntegration(), + // Global Handlers + onUncaughtExceptionIntegration(), + onUnhandledRejectionIntegration(), + // Event Info + contextLinesIntegration(), + localVariablesIntegration(), + nodeContextIntegration(), + childProcessIntegration(), + processSessionIntegration(), + modulesIntegration(), + ]; +} + +/** + * Initialize Sentry for Node. + */ +export function init(options: NodeOptions | undefined = {}): NodeClient | undefined { + return _init(options, getDefaultIntegrations); +} + +/** + * Initialize Sentry for Node, without any integrations added by default. + */ +export function initWithoutDefaultIntegrations(options: NodeOptions | undefined = {}): NodeClient { + return _init(options, () => []); +} + +/** + * Initialize Sentry for Node, without performance instrumentation. + */ +function _init( + _options: NodeOptions | undefined = {}, + getDefaultIntegrationsImpl: (options: Options) => Integration[], +): NodeClient { + const options = getClientOptions(_options, getDefaultIntegrationsImpl); + + if (options.debug === true) { + if (DEBUG_BUILD) { + logger.enable(); + } else { + // use `console.warn` rather than `logger.warn` since by non-debug bundles have all `logger.x` statements stripped + consoleSandbox(() => { + // eslint-disable-next-line no-console + console.warn('[Sentry] Cannot initialize SDK with `debug` option using a non-debug bundle.'); + }); + } + } + + if (!isCjs() && options.registerEsmLoaderHooks !== false) { + maybeInitializeEsmLoader(); + } + + setOpenTelemetryContextAsyncContextStrategy(); + + const scope = getCurrentScope(); + scope.update(options.initialScope); + + if (options.spotlight && !options.integrations.some(({ name }) => name === SPOTLIGHT_INTEGRATION_NAME)) { + options.integrations.push( + spotlightIntegration({ + sidecarUrl: typeof options.spotlight === 'string' ? options.spotlight : undefined, + }), + ); + } + + const client = new NodeClient(options); + // The client is on the current scope, from where it generally is inherited + getCurrentScope().setClient(client); + + client.init(); + + logger.log(`Running in ${isCjs() ? 'CommonJS' : 'ESM'} mode.`); + + client.startClientReportTracking(); + + updateScopeFromEnvVariables(); + + if (options.debug) { + setupOpenTelemetryLogger(); + } + + enhanceDscWithOpenTelemetryRootSpanName(client); + setupEventContextTrace(client); + + return client; +} + +/** + * Validate that your OpenTelemetry setup is correct. + */ +export function validateOpenTelemetrySetup(): void { + if (!DEBUG_BUILD) { + return; + } + + const setup = openTelemetrySetupCheck(); + + const required: ReturnType = ['SentryContextManager', 'SentryPropagator']; + + if (hasSpansEnabled()) { + required.push('SentrySpanProcessor'); + } + + for (const k of required) { + if (!setup.includes(k)) { + logger.error( + `You have to set up the ${k}. Without this, the OpenTelemetry & Sentry integration will not work properly.`, + ); + } + } + + if (!setup.includes('SentrySampler')) { + logger.warn( + 'You have to set up the SentrySampler. Without this, the OpenTelemetry & Sentry integration may still work, but sample rates set for the Sentry SDK will not be respected. If you use a custom sampler, make sure to use `wrapSamplingDecision`.', + ); + } +} + +function getClientOptions( + options: NodeOptions, + getDefaultIntegrationsImpl: (options: Options) => Integration[], +): NodeClientOptions { + const release = getRelease(options.release); + const spotlight = + options.spotlight ?? envToBool(process.env.SENTRY_SPOTLIGHT, { strict: true }) ?? process.env.SENTRY_SPOTLIGHT; + const tracesSampleRate = getTracesSampleRate(options.tracesSampleRate); + + const mergedOptions = { + ...options, + dsn: options.dsn ?? process.env.SENTRY_DSN, + environment: options.environment ?? process.env.SENTRY_ENVIRONMENT, + sendClientReports: options.sendClientReports ?? true, + transport: options.transport ?? makeNodeTransport, + stackParser: stackParserFromStackParserOptions(options.stackParser || defaultStackParser), + release, + tracesSampleRate, + spotlight, + debug: envToBool(options.debug ?? process.env.SENTRY_DEBUG), + }; + + const integrations = options.integrations; + const defaultIntegrations = options.defaultIntegrations ?? getDefaultIntegrationsImpl(mergedOptions); + + return { + ...mergedOptions, + integrations: getIntegrationsToSetup({ + defaultIntegrations, + integrations, + }), + }; +} + +function getRelease(release: NodeOptions['release']): string | undefined { + if (release !== undefined) { + return release; + } + + const detectedRelease = getSentryRelease(); + if (detectedRelease !== undefined) { + return detectedRelease; + } + + return undefined; +} + +function getTracesSampleRate(tracesSampleRate: NodeOptions['tracesSampleRate']): number | undefined { + if (tracesSampleRate !== undefined) { + return tracesSampleRate; + } + + const sampleRateFromEnv = process.env.SENTRY_TRACES_SAMPLE_RATE; + if (!sampleRateFromEnv) { + return undefined; + } + + const parsed = parseFloat(sampleRateFromEnv); + return isFinite(parsed) ? parsed : undefined; +} + +/** + * Update scope and propagation context based on environmental variables. + * + * See https://github.com/getsentry/rfcs/blob/main/text/0071-continue-trace-over-process-boundaries.md + * for more details. + */ +function updateScopeFromEnvVariables(): void { + if (envToBool(process.env.SENTRY_USE_ENVIRONMENT) !== false) { + const sentryTraceEnv = process.env.SENTRY_TRACE; + const baggageEnv = process.env.SENTRY_BAGGAGE; + const propagationContext = propagationContextFromHeaders(sentryTraceEnv, baggageEnv); + getCurrentScope().setPropagationContext(propagationContext); + } +} + +/** + * Setup the OTEL logger to use our own logger. + */ +function setupOpenTelemetryLogger(): void { + const otelLogger = new Proxy(logger as typeof logger & { verbose: (typeof logger)['debug'] }, { + get(target, prop, receiver) { + const actualProp = prop === 'verbose' ? 'debug' : prop; + return Reflect.get(target, actualProp, receiver); + }, + }); + + // Disable diag, to ensure this works even if called multiple times + diag.disable(); + diag.setLogger(otelLogger, DiagLogLevel.DEBUG); +} diff --git a/packages/node/src/sdk/scope.ts b/packages/node-core/src/sdk/scope.ts similarity index 100% rename from packages/node/src/sdk/scope.ts rename to packages/node-core/src/sdk/scope.ts diff --git a/packages/node/src/transports/http-module.ts b/packages/node-core/src/transports/http-module.ts similarity index 100% rename from packages/node/src/transports/http-module.ts rename to packages/node-core/src/transports/http-module.ts diff --git a/packages/node/src/transports/http.ts b/packages/node-core/src/transports/http.ts similarity index 100% rename from packages/node/src/transports/http.ts rename to packages/node-core/src/transports/http.ts diff --git a/packages/node/src/transports/index.ts b/packages/node-core/src/transports/index.ts similarity index 100% rename from packages/node/src/transports/index.ts rename to packages/node-core/src/transports/index.ts diff --git a/packages/node-core/src/types.ts b/packages/node-core/src/types.ts new file mode 100644 index 000000000000..a331b876166d --- /dev/null +++ b/packages/node-core/src/types.ts @@ -0,0 +1,178 @@ +import type { Span as WriteableSpan } from '@opentelemetry/api'; +import type { Instrumentation } from '@opentelemetry/instrumentation'; +import type { ReadableSpan, SpanProcessor } from '@opentelemetry/sdk-trace-base'; +import type { ClientOptions, Options, SamplingContext, Scope, Span, TracePropagationTargets } from '@sentry/core'; +import type { NodeTransportOptions } from './transports'; + +export interface BaseNodeOptions { + /** + * 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. + * + * @example + * ```js + * Sentry.init({ + * tracePropagationTargets: ['api.site.com'], + * }); + * ``` + */ + tracePropagationTargets?: TracePropagationTargets; + + /** + * Sets profiling sample rate when @sentry/profiling-node is installed + * + * @deprecated + */ + profilesSampleRate?: number; + + /** + * Function to compute profiling sample rate dynamically and filter unwanted profiles. + * + * Profiling is enabled if either this or `profilesSampleRate` is defined. If both are defined, `profilesSampleRate` is + * ignored. + * + * Will automatically be passed a context object of default and optional custom data. + * + * @returns A sample rate between 0 and 1 (0 drops the profile, 1 guarantees it will be sent). Returning `true` is + * equivalent to returning 1 and returning `false` is equivalent to returning 0. + * + * @deprecated + */ + profilesSampler?: (samplingContext: SamplingContext) => number | boolean; + + /** + * Sets profiling session sample rate - only evaluated once per SDK initialization. + * @default 0 + */ + profileSessionSampleRate?: number; + + /** + * Set the lifecycle of the profiler. + * + * - `manual`: The profiler will be manually started and stopped. + * - `trace`: The profiler will be automatically started when when a span is sampled and stopped when there are no more sampled spans. + * + * @default 'manual' + */ + profileLifecycle?: 'manual' | 'trace'; + + /** + * If set to `false`, the SDK will not automatically detect the `serverName`. + * + * This is useful if you are using the SDK in a CLI app or Electron where the + * hostname might be considered PII. + * + * @default true + */ + includeServerName?: boolean; + + /** Sets an optional server name (device name) */ + serverName?: string; + + /** + * Include local variables with stack traces. + * + * Requires the `LocalVariables` integration. + */ + includeLocalVariables?: boolean; + + /** + * If you use Spotlight by Sentry during development, use + * this option to forward captured Sentry events to Spotlight. + * + * Either set it to true, or provide a specific Spotlight Sidecar URL. + * + * More details: https://spotlightjs.com/ + * + * IMPORTANT: Only set this option to `true` while developing, not in production! + */ + spotlight?: boolean | string; + + /** + * Provide an array of OpenTelemetry Instrumentations that should be registered. + * + * Use this option if you want to register OpenTelemetry instrumentation that the Sentry SDK does not yet have support for. + */ + openTelemetryInstrumentations?: Instrumentation[]; + + /** + * Provide an array of additional OpenTelemetry SpanProcessors that should be registered. + */ + openTelemetrySpanProcessors?: SpanProcessor[]; + + /** + * The max. duration in seconds that the SDK will wait for parent spans to be finished before discarding a span. + * The SDK will automatically clean up spans that have no finished parent after this duration. + * This is necessary to prevent memory leaks in case of parent spans that are never finished or otherwise dropped/missing. + * However, if you have very long-running spans in your application, a shorter duration might cause spans to be discarded too early. + * In this case, you can increase this duration to a value that fits your expected data. + * + * Defaults to 300 seconds (5 minutes). + */ + maxSpanWaitDuration?: number; + + /** + * Whether to register ESM loader hooks to automatically instrument libraries. + * This is necessary to auto instrument libraries that are loaded via ESM imports, but it can cause issues + * with certain libraries. If you run into problems running your app with this enabled, + * please raise an issue in https://github.com/getsentry/sentry-javascript. + * + * Defaults to `true`. + */ + registerEsmLoaderHooks?: boolean; + + /** + * Configures in which interval client reports will be flushed. Defaults to `60_000` (milliseconds). + */ + clientReportFlushInterval?: number; + + /** + * By default, the SDK will try to identify problems with your instrumentation setup and warn you about it. + * If you want to disable these warnings, set this to `true`. + */ + disableInstrumentationWarnings?: boolean; + + /** + * Controls how many milliseconds to wait before shutting down. The default is 2 seconds. Setting this too low can cause + * problems for sending events from command line applications. Setting it too + * high can cause the application to block for users with network connectivity + * problems. + */ + shutdownTimeout?: number; + + /** Callback that is executed when a fatal global error occurs. */ + onFatalError?(this: void, error: Error): void; +} + +/** + * Configuration options for the Sentry Node SDK + * @see @sentry/core Options for more information. + */ +export interface NodeOptions extends Options, BaseNodeOptions {} + +/** + * Configuration options for the Sentry Node SDK Client class + * @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 relevant fields before accessing them. + * + * Note that technically, the `Span` exported from `@opentelemetry/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/src/utils/addOriginToSpan.ts b/packages/node-core/src/utils/addOriginToSpan.ts similarity index 100% rename from packages/node/src/utils/addOriginToSpan.ts rename to packages/node-core/src/utils/addOriginToSpan.ts diff --git a/packages/node/src/utils/baggage.ts b/packages/node-core/src/utils/baggage.ts similarity index 100% rename from packages/node/src/utils/baggage.ts rename to packages/node-core/src/utils/baggage.ts diff --git a/packages/node/src/utils/commonjs.ts b/packages/node-core/src/utils/commonjs.ts similarity index 100% rename from packages/node/src/utils/commonjs.ts rename to packages/node-core/src/utils/commonjs.ts diff --git a/packages/node/src/utils/createMissingInstrumentationContext.ts b/packages/node-core/src/utils/createMissingInstrumentationContext.ts similarity index 100% rename from packages/node/src/utils/createMissingInstrumentationContext.ts rename to packages/node-core/src/utils/createMissingInstrumentationContext.ts diff --git a/packages/node/src/utils/debug.ts b/packages/node-core/src/utils/debug.ts similarity index 100% rename from packages/node/src/utils/debug.ts rename to packages/node-core/src/utils/debug.ts diff --git a/packages/node/src/utils/ensureIsWrapped.ts b/packages/node-core/src/utils/ensureIsWrapped.ts similarity index 100% rename from packages/node/src/utils/ensureIsWrapped.ts rename to packages/node-core/src/utils/ensureIsWrapped.ts diff --git a/packages/node/src/utils/entry-point.ts b/packages/node-core/src/utils/entry-point.ts similarity index 100% rename from packages/node/src/utils/entry-point.ts rename to packages/node-core/src/utils/entry-point.ts diff --git a/packages/node/src/utils/envToBool.ts b/packages/node-core/src/utils/envToBool.ts similarity index 100% rename from packages/node/src/utils/envToBool.ts rename to packages/node-core/src/utils/envToBool.ts diff --git a/packages/node/src/utils/errorhandling.ts b/packages/node-core/src/utils/errorhandling.ts similarity index 100% rename from packages/node/src/utils/errorhandling.ts rename to packages/node-core/src/utils/errorhandling.ts diff --git a/packages/node/src/utils/getRequestUrl.ts b/packages/node-core/src/utils/getRequestUrl.ts similarity index 100% rename from packages/node/src/utils/getRequestUrl.ts rename to packages/node-core/src/utils/getRequestUrl.ts diff --git a/packages/node/src/utils/module.ts b/packages/node-core/src/utils/module.ts similarity index 100% rename from packages/node/src/utils/module.ts rename to packages/node-core/src/utils/module.ts diff --git a/packages/node/src/utils/prepareEvent.ts b/packages/node-core/src/utils/prepareEvent.ts similarity index 100% rename from packages/node/src/utils/prepareEvent.ts rename to packages/node-core/src/utils/prepareEvent.ts diff --git a/packages/node-core/test/cron.test.ts b/packages/node-core/test/cron.test.ts new file mode 100644 index 000000000000..efa146b90f20 --- /dev/null +++ b/packages/node-core/test/cron.test.ts @@ -0,0 +1,224 @@ +import * as SentryCore from '@sentry/core'; +import { type MockInstance, afterEach, beforeEach, describe, expect, test, vi } from 'vitest'; +import { cron } from '../src'; +import type { CronJob, CronJobParams } from '../src/cron/cron'; +import type { NodeCron, NodeCronOptions } from '../src/cron/node-cron'; + +describe('cron check-ins', () => { + let withMonitorSpy: MockInstance; + + beforeEach(() => { + withMonitorSpy = vi.spyOn(SentryCore, 'withMonitor'); + }); + + afterEach(() => { + vi.restoreAllMocks(); + }); + + describe('cron', () => { + class CronJobMock { + constructor( + cronTime: CronJobParams['cronTime'], + onTick: CronJobParams['onTick'], + _onComplete?: CronJobParams['onComplete'], + _start?: CronJobParams['start'], + _timeZone?: CronJobParams['timeZone'], + _context?: CronJobParams['context'], + _runOnInit?: CronJobParams['runOnInit'], + _utcOffset?: CronJobParams['utcOffset'], + _unrefTimeout?: CronJobParams['unrefTimeout'], + ) { + expect(cronTime).toBe('* * * Jan,Sep Sun'); + expect(onTick).toBeInstanceOf(Function); + setImmediate(() => onTick(undefined, undefined)); + } + + static from(params: CronJobParams): CronJob { + return new CronJobMock( + params.cronTime, + params.onTick, + params.onComplete, + params.start, + params.timeZone, + params.context, + params.runOnInit, + params.utcOffset, + params.unrefTimeout, + ); + } + } + + test('new CronJob()', () => + new Promise(done => { + expect.assertions(4); + + const CronJobWithCheckIn = cron.instrumentCron(CronJobMock, 'my-cron-job'); + + new CronJobWithCheckIn( + '* * * Jan,Sep Sun', + () => { + expect(withMonitorSpy).toHaveBeenCalledTimes(1); + expect(withMonitorSpy).toHaveBeenLastCalledWith('my-cron-job', expect.anything(), { + schedule: { type: 'crontab', value: '* * * 1,9 0' }, + timezone: 'America/Los_Angeles', + }); + done(); + }, + undefined, + true, + 'America/Los_Angeles', + ); + })); + + test('CronJob.from()', () => + new Promise(done => { + expect.assertions(4); + + const CronJobWithCheckIn = cron.instrumentCron(CronJobMock, 'my-cron-job'); + + CronJobWithCheckIn.from({ + cronTime: '* * * Jan,Sep Sun', + onTick: () => { + expect(withMonitorSpy).toHaveBeenCalledTimes(1); + expect(withMonitorSpy).toHaveBeenLastCalledWith('my-cron-job', expect.anything(), { + schedule: { type: 'crontab', value: '* * * 1,9 0' }, + }); + done(); + }, + }); + })); + + test('throws with multiple jobs same name', () => { + const CronJobWithCheckIn = cron.instrumentCron(CronJobMock, 'my-cron-job'); + + CronJobWithCheckIn.from({ + cronTime: '* * * Jan,Sep Sun', + onTick: () => { + // + }, + }); + + expect(() => { + CronJobWithCheckIn.from({ + cronTime: '* * * Jan,Sep Sun', + onTick: () => { + // + }, + }); + }).toThrowError("A job named 'my-cron-job' has already been scheduled"); + }); + }); + + describe('node-cron', () => { + test('calls withMonitor', () => + new Promise(done => { + expect.assertions(5); + + const nodeCron: NodeCron = { + schedule: (expression: string, callback: () => void, options?: NodeCronOptions): unknown => { + expect(expression).toBe('* * * Jan,Sep Sun'); + expect(callback).toBeInstanceOf(Function); + expect(options?.name).toBe('my-cron-job'); + return callback(); + }, + }; + + const cronWithCheckIn = cron.instrumentNodeCron(nodeCron); + + cronWithCheckIn.schedule( + '* * * Jan,Sep Sun', + () => { + expect(withMonitorSpy).toHaveBeenCalledTimes(1); + expect(withMonitorSpy).toHaveBeenLastCalledWith('my-cron-job', expect.anything(), { + schedule: { type: 'crontab', value: '* * * 1,9 0' }, + }); + done(); + }, + { name: 'my-cron-job' }, + ); + })); + + test('throws without supplied name', () => { + const nodeCron: NodeCron = { + schedule: (): unknown => { + return undefined; + }, + }; + + const cronWithCheckIn = cron.instrumentNodeCron(nodeCron); + + expect(() => { + // @ts-expect-error Initially missing name + cronWithCheckIn.schedule('* * * * *', () => { + // + }); + }).toThrowError('Missing "name" for scheduled job. A name is required for Sentry check-in monitoring.'); + }); + }); + + describe('node-schedule', () => { + test('calls withMonitor', () => + new Promise(done => { + expect.assertions(5); + + class NodeScheduleMock { + scheduleJob( + nameOrExpression: string | Date | object, + expressionOrCallback: string | Date | object | (() => void), + callback: () => void, + ): unknown { + expect(nameOrExpression).toBe('my-cron-job'); + expect(expressionOrCallback).toBe('* * * Jan,Sep Sun'); + expect(callback).toBeInstanceOf(Function); + return callback(); + } + } + + const scheduleWithCheckIn = cron.instrumentNodeSchedule(new NodeScheduleMock()); + + scheduleWithCheckIn.scheduleJob('my-cron-job', '* * * Jan,Sep Sun', () => { + expect(withMonitorSpy).toHaveBeenCalledTimes(1); + expect(withMonitorSpy).toHaveBeenLastCalledWith('my-cron-job', expect.anything(), { + schedule: { type: 'crontab', value: '* * * 1,9 0' }, + }); + done(); + }); + })); + + test('throws without crontab string', () => { + class NodeScheduleMock { + scheduleJob(_: string, __: string | Date, ___: () => void): unknown { + return undefined; + } + } + + const scheduleWithCheckIn = cron.instrumentNodeSchedule(new NodeScheduleMock()); + + expect(() => { + scheduleWithCheckIn.scheduleJob('my-cron-job', new Date(), () => { + // + }); + }).toThrowError( + "Automatic instrumentation of 'node-schedule' requires the first parameter of 'scheduleJob' to be a job name string and the second parameter to be a crontab string", + ); + }); + + test('throws without job name', () => { + class NodeScheduleMock { + scheduleJob(_: string, __: () => void): unknown { + return undefined; + } + } + + const scheduleWithCheckIn = cron.instrumentNodeSchedule(new NodeScheduleMock()); + + expect(() => { + scheduleWithCheckIn.scheduleJob('* * * * *', () => { + // + }); + }).toThrowError( + "Automatic instrumentation of 'node-schedule' requires the first parameter of 'scheduleJob' to be a job name string and the second parameter to be a crontab string", + ); + }); + }); +}); diff --git a/packages/node-core/test/helpers/conditional.ts b/packages/node-core/test/helpers/conditional.ts new file mode 100644 index 000000000000..ceea11315db4 --- /dev/null +++ b/packages/node-core/test/helpers/conditional.ts @@ -0,0 +1,19 @@ +import { parseSemver } from '@sentry/core'; +import { it, test } from 'vitest'; + +const NODE_VERSION = parseSemver(process.versions.node).major; + +/** + * Returns`describe` or `describe.skip` depending on allowed major versions of Node. + * + * @param {{ min?: number; max?: number }} allowedVersion + */ +export const conditionalTest = (allowedVersion: { min?: number; max?: number }) => { + if (!NODE_VERSION) { + return it.skip; + } + + return NODE_VERSION < (allowedVersion.min || -Infinity) || NODE_VERSION > (allowedVersion.max || Infinity) + ? test.skip + : test; +}; diff --git a/packages/node-core/test/helpers/error.ts b/packages/node-core/test/helpers/error.ts new file mode 100644 index 000000000000..03d4150c3f11 --- /dev/null +++ b/packages/node-core/test/helpers/error.ts @@ -0,0 +1,4 @@ +/* this method is exported from an external file to be able to test contextlines when adding an external file */ +export function getError(): Error { + return new Error('mock error'); +} diff --git a/packages/node-core/test/helpers/getDefaultNodeClientOptions.ts b/packages/node-core/test/helpers/getDefaultNodeClientOptions.ts new file mode 100644 index 000000000000..8cff09d3c0ee --- /dev/null +++ b/packages/node-core/test/helpers/getDefaultNodeClientOptions.ts @@ -0,0 +1,13 @@ +import { createTransport, resolvedSyncPromise } from '@sentry/core'; +import type { NodeClientOptions } from '../../src/types'; + +export function getDefaultNodeClientOptions(options: Partial = {}): NodeClientOptions { + return { + dsn: 'https://username@domain/123', + tracesSampleRate: 1, + integrations: [], + transport: () => createTransport({ recordDroppedEvent: () => undefined }, _ => resolvedSyncPromise({})), + stackParser: () => [], + ...options, + }; +} diff --git a/packages/node-core/test/helpers/mockSdkInit.ts b/packages/node-core/test/helpers/mockSdkInit.ts new file mode 100644 index 000000000000..f627e9999946 --- /dev/null +++ b/packages/node-core/test/helpers/mockSdkInit.ts @@ -0,0 +1,143 @@ +import { context, propagation, ProxyTracerProvider, trace } from '@opentelemetry/api'; +import { Resource } from '@opentelemetry/resources'; +import { BasicTracerProvider } from '@opentelemetry/sdk-trace-base'; +import { + ATTR_SERVICE_NAME, + ATTR_SERVICE_VERSION, + SEMRESATTRS_SERVICE_NAMESPACE, +} from '@opentelemetry/semantic-conventions'; +import { + createTransport, + getClient, + getCurrentScope, + getGlobalScope, + getIsolationScope, + logger, + resolvedSyncPromise, + SDK_VERSION, +} from '@sentry/core'; +import { SentryPropagator, SentrySampler, SentrySpanProcessor } from '@sentry/opentelemetry'; +import type { NodeClient } from '../../src'; +import { SentryContextManager, validateOpenTelemetrySetup } from '../../src'; +import { init } from '../../src/sdk'; +import type { NodeClientOptions } from '../../src/types'; + +const PUBLIC_DSN = 'https://username@domain/123'; + +// About 277h - this must fit into new Array(len)! +const MAX_MAX_SPAN_WAIT_DURATION = 1_000_000; + +/** Clamp span processor timeout to reasonable values, mirroring Node SDK behavior. */ +function clampSpanProcessorTimeout(maxSpanWaitDuration: number | undefined): number | undefined { + if (maxSpanWaitDuration == null) { + return undefined; + } + + // We guard for a max. value here, because we create an array with this length + // So if this value is too large, this would fail + if (maxSpanWaitDuration > MAX_MAX_SPAN_WAIT_DURATION) { + logger.warn(`\`maxSpanWaitDuration\` is too high, using the maximum value of ${MAX_MAX_SPAN_WAIT_DURATION}`); + return MAX_MAX_SPAN_WAIT_DURATION; + } else if (maxSpanWaitDuration <= 0 || Number.isNaN(maxSpanWaitDuration)) { + logger.warn('`maxSpanWaitDuration` must be a positive number, using default value instead.'); + return undefined; + } + + return maxSpanWaitDuration; +} + +export function resetGlobals(): void { + getCurrentScope().clear(); + getCurrentScope().setClient(undefined); + getIsolationScope().clear(); + getGlobalScope().clear(); +} + +export function setupOtel(client: NodeClient): BasicTracerProvider | undefined { + if (!client) { + return undefined; + } + + const clientOptions = client.getOptions(); + const spanProcessorTimeout = clampSpanProcessorTimeout(clientOptions.maxSpanWaitDuration); + + // Create and configure TracerProvider with same config as Node SDK + const provider = new BasicTracerProvider({ + sampler: new SentrySampler(client), + resource: new Resource({ + [ATTR_SERVICE_NAME]: 'node', + // eslint-disable-next-line deprecation/deprecation + [SEMRESATTRS_SERVICE_NAMESPACE]: 'sentry', + [ATTR_SERVICE_VERSION]: SDK_VERSION, + }), + forceFlushTimeoutMillis: 500, + spanProcessors: [ + new SentrySpanProcessor({ + timeout: spanProcessorTimeout, + }), + ], + }); + + // Register as globals + trace.setGlobalTracerProvider(provider); + propagation.setGlobalPropagator(new SentryPropagator()); + context.setGlobalContextManager(new SentryContextManager()); + + validateOpenTelemetrySetup(); + + return provider; +} + +export function mockSdkInit(options?: Partial) { + resetGlobals(); + const client = init({ + dsn: PUBLIC_DSN, + defaultIntegrations: false, + // We are disabling client reports because we would be acquiring resources with every init call and that would leak + // memory every time we call init in the tests + sendClientReports: false, + // Use a mock transport to prevent network calls + transport: () => createTransport({ recordDroppedEvent: () => undefined }, _ => resolvedSyncPromise({})), + ...options, + }); + + // Always set up OpenTelemetry if we have a client + if (client) { + const provider = setupOtel(client); + // Important: Link the provider to the client so getProvider() can find it + client.traceProvider = provider; + } + + return client; +} + +export function cleanupOtel(_provider?: BasicTracerProvider): void { + const provider = getProvider(_provider); + + if (provider) { + void provider.forceFlush(); + void provider.shutdown(); + } + + // Disable all globally registered APIs + trace.disable(); + context.disable(); + propagation.disable(); + + // Reset globals to ensure clean state + resetGlobals(); +} + +export function getProvider(_provider?: BasicTracerProvider): BasicTracerProvider | undefined { + let provider = _provider || getClient()?.traceProvider || trace.getTracerProvider(); + + if (provider instanceof ProxyTracerProvider) { + provider = provider.getDelegate(); + } + + if (!(provider instanceof BasicTracerProvider)) { + return undefined; + } + + return provider; +} diff --git a/packages/node-core/test/integration/breadcrumbs.test.ts b/packages/node-core/test/integration/breadcrumbs.test.ts new file mode 100644 index 000000000000..5e6b4aff3cb4 --- /dev/null +++ b/packages/node-core/test/integration/breadcrumbs.test.ts @@ -0,0 +1,358 @@ +import { addBreadcrumb, captureException, withIsolationScope, withScope } from '@sentry/core'; +import { startSpan } from '@sentry/opentelemetry'; +import { afterEach, describe, expect, it, vi } from 'vitest'; +import { getClient } from '../../src/'; +import type { NodeClient } from '../../src/sdk/client'; +import { cleanupOtel, mockSdkInit } from '../helpers/mockSdkInit'; + +describe('Integration | breadcrumbs', () => { + const beforeSendTransaction = vi.fn(() => null); + + afterEach(() => { + cleanupOtel(); + }); + + describe('without tracing', () => { + it('correctly adds & retrieves breadcrumbs', async () => { + const beforeSend = vi.fn(() => null); + const beforeBreadcrumb = vi.fn(breadcrumb => breadcrumb); + + mockSdkInit({ beforeSend, beforeBreadcrumb }); + + const client = getClient() as NodeClient; + + addBreadcrumb({ timestamp: 123456, message: 'test1' }); + addBreadcrumb({ timestamp: 123457, message: 'test2', data: { nested: 'yes' } }); + addBreadcrumb({ timestamp: 123455, message: 'test3' }); + + const error = new Error('test'); + captureException(error); + + await client.flush(); + + expect(beforeSend).toHaveBeenCalledTimes(1); + expect(beforeBreadcrumb).toHaveBeenCalledTimes(3); + + expect(beforeSend).toHaveBeenCalledWith( + expect.objectContaining({ + breadcrumbs: [ + { message: 'test1', timestamp: 123456 }, + { data: { nested: 'yes' }, message: 'test2', timestamp: 123457 }, + { message: 'test3', timestamp: 123455 }, + ], + }), + { + event_id: expect.any(String), + originalException: error, + syntheticException: expect.any(Error), + }, + ); + }); + + it('handles parallel scopes', async () => { + const beforeSend = vi.fn(() => null); + const beforeBreadcrumb = vi.fn(breadcrumb => breadcrumb); + + mockSdkInit({ beforeSend, beforeBreadcrumb }); + + const client = getClient(); + + const error = new Error('test'); + + addBreadcrumb({ timestamp: 123456, message: 'test0' }); + + withIsolationScope(() => { + addBreadcrumb({ timestamp: 123456, message: 'test1' }); + }); + + withIsolationScope(() => { + addBreadcrumb({ timestamp: 123456, message: 'test2' }); + captureException(error); + }); + + withIsolationScope(() => { + addBreadcrumb({ timestamp: 123456, message: 'test3' }); + }); + + await client?.flush(); + + expect(beforeSend).toHaveBeenCalledTimes(1); + expect(beforeBreadcrumb).toHaveBeenCalledTimes(4); + + expect(beforeSend).toHaveBeenCalledWith( + expect.objectContaining({ + breadcrumbs: [ + { message: 'test0', timestamp: 123456 }, + { message: 'test2', timestamp: 123456 }, + ], + }), + { + event_id: expect.any(String), + originalException: error, + syntheticException: expect.any(Error), + }, + ); + }); + }); + + it('correctly adds & retrieves breadcrumbs', async () => { + const beforeSend = vi.fn(() => null); + const beforeBreadcrumb = vi.fn(breadcrumb => breadcrumb); + + mockSdkInit({ tracesSampleRate: 1, beforeSend, beforeBreadcrumb, beforeSendTransaction }); + + const client = getClient() as NodeClient; + + const error = new Error('test'); + + startSpan({ name: 'test' }, () => { + addBreadcrumb({ timestamp: 123456, message: 'test1' }); + + startSpan({ name: 'inner1' }, () => { + addBreadcrumb({ timestamp: 123457, message: 'test2', data: { nested: 'yes' } }); + }); + + startSpan({ name: 'inner2' }, () => { + addBreadcrumb({ timestamp: 123455, message: 'test3' }); + }); + + captureException(error); + }); + + await client.flush(); + + expect(beforeSend).toHaveBeenCalledTimes(1); + expect(beforeBreadcrumb).toHaveBeenCalledTimes(3); + + expect(beforeSend).toHaveBeenCalledWith( + expect.objectContaining({ + breadcrumbs: [ + { message: 'test1', timestamp: 123456 }, + { data: { nested: 'yes' }, message: 'test2', timestamp: 123457 }, + { message: 'test3', timestamp: 123455 }, + ], + }), + { + event_id: expect.any(String), + originalException: error, + syntheticException: expect.any(Error), + }, + ); + }); + + it('correctly adds & retrieves breadcrumbs for the current isolation span only', async () => { + const beforeSend = vi.fn(() => null); + const beforeBreadcrumb = vi.fn(breadcrumb => breadcrumb); + + mockSdkInit({ tracesSampleRate: 1, beforeSend, beforeBreadcrumb, beforeSendTransaction }); + + const client = getClient() as NodeClient; + + const error = new Error('test'); + + withIsolationScope(() => { + startSpan({ name: 'test1' }, () => { + addBreadcrumb({ timestamp: 123456, message: 'test1-a' }); + + startSpan({ name: 'inner1' }, () => { + addBreadcrumb({ timestamp: 123457, message: 'test1-b' }); + }); + }); + }); + + withIsolationScope(() => { + startSpan({ name: 'test2' }, () => { + addBreadcrumb({ timestamp: 123456, message: 'test2-a' }); + + startSpan({ name: 'inner2' }, () => { + addBreadcrumb({ timestamp: 123457, message: 'test2-b' }); + }); + + captureException(error); + }); + }); + + await client.flush(); + + expect(beforeSend).toHaveBeenCalledTimes(1); + expect(beforeBreadcrumb).toHaveBeenCalledTimes(4); + + expect(beforeSend).toHaveBeenCalledWith( + expect.objectContaining({ + breadcrumbs: [ + { message: 'test2-a', timestamp: 123456 }, + { message: 'test2-b', timestamp: 123457 }, + ], + }), + { + event_id: expect.any(String), + originalException: error, + syntheticException: expect.any(Error), + }, + ); + }); + + it('ignores scopes inside of root span', async () => { + const beforeSend = vi.fn(() => null); + const beforeBreadcrumb = vi.fn(breadcrumb => breadcrumb); + + mockSdkInit({ tracesSampleRate: 1, beforeSend, beforeBreadcrumb, beforeSendTransaction }); + + const client = getClient() as NodeClient; + + const error = new Error('test'); + + startSpan({ name: 'test1' }, () => { + withScope(() => { + addBreadcrumb({ timestamp: 123456, message: 'test1' }); + }); + startSpan({ name: 'inner1' }, () => { + addBreadcrumb({ timestamp: 123457, message: 'test2' }); + }); + + captureException(error); + }); + + await client.flush(); + + expect(beforeSend).toHaveBeenCalledTimes(1); + expect(beforeBreadcrumb).toHaveBeenCalledTimes(2); + + expect(beforeSend).toHaveBeenCalledWith( + expect.objectContaining({ + breadcrumbs: [ + { message: 'test1', timestamp: 123456 }, + { message: 'test2', timestamp: 123457 }, + ], + }), + { + event_id: expect.any(String), + originalException: error, + syntheticException: expect.any(Error), + }, + ); + }); + + it('handles deep nesting of scopes', async () => { + const beforeSend = vi.fn(() => null); + const beforeBreadcrumb = vi.fn(breadcrumb => breadcrumb); + + mockSdkInit({ tracesSampleRate: 1, beforeSend, beforeBreadcrumb, beforeSendTransaction }); + + const client = getClient() as NodeClient; + + const error = new Error('test'); + + startSpan({ name: 'test1' }, () => { + withScope(() => { + addBreadcrumb({ timestamp: 123456, message: 'test1' }); + }); + startSpan({ name: 'inner1' }, () => { + addBreadcrumb({ timestamp: 123457, message: 'test2' }); + + startSpan({ name: 'inner2' }, () => { + addBreadcrumb({ timestamp: 123457, message: 'test3' }); + + startSpan({ name: 'inner3' }, () => { + addBreadcrumb({ timestamp: 123457, message: 'test4' }); + + captureException(error); + + startSpan({ name: 'inner4' }, () => { + addBreadcrumb({ timestamp: 123457, message: 'test5' }); + }); + + addBreadcrumb({ timestamp: 123457, message: 'test6' }); + }); + }); + }); + + addBreadcrumb({ timestamp: 123456, message: 'test99' }); + }); + + await client.flush(); + + expect(beforeSend).toHaveBeenCalledTimes(1); + + expect(beforeSend).toHaveBeenCalledWith( + expect.objectContaining({ + breadcrumbs: [ + { message: 'test1', timestamp: 123456 }, + { message: 'test2', timestamp: 123457 }, + { message: 'test3', timestamp: 123457 }, + { message: 'test4', timestamp: 123457 }, + ], + }), + { + event_id: expect.any(String), + originalException: error, + syntheticException: expect.any(Error), + }, + ); + }); + + it('correctly adds & retrieves breadcrumbs in async spans', async () => { + const beforeSend = vi.fn(() => null); + const beforeBreadcrumb = vi.fn(breadcrumb => breadcrumb); + + mockSdkInit({ tracesSampleRate: 1, beforeSend, beforeBreadcrumb, beforeSendTransaction }); + + const client = getClient() as NodeClient; + + const error = new Error('test'); + + const promise1 = withIsolationScope(async () => { + await startSpan({ name: 'test' }, async () => { + addBreadcrumb({ timestamp: 123456, message: 'test1' }); + + await startSpan({ name: 'inner1' }, async () => { + addBreadcrumb({ timestamp: 123457, message: 'test2' }); + }); + + await startSpan({ name: 'inner2' }, async () => { + addBreadcrumb({ timestamp: 123455, message: 'test3' }); + }); + + await new Promise(resolve => setTimeout(resolve, 10)); + + captureException(error); + }); + }); + + const promise2 = withIsolationScope(async () => { + await startSpan({ name: 'test-b' }, async () => { + addBreadcrumb({ timestamp: 123456, message: 'test1-b' }); + + await startSpan({ name: 'inner1b' }, async () => { + addBreadcrumb({ timestamp: 123457, message: 'test2-b' }); + }); + + await startSpan({ name: 'inner2b' }, async () => { + addBreadcrumb({ timestamp: 123455, message: 'test3-b' }); + }); + }); + }); + + await Promise.all([promise1, promise2]); + + await client.flush(); + + expect(beforeSend).toHaveBeenCalledTimes(1); + expect(beforeBreadcrumb).toHaveBeenCalledTimes(6); + + expect(beforeSend).toHaveBeenCalledWith( + expect.objectContaining({ + breadcrumbs: [ + { message: 'test1', timestamp: 123456 }, + { message: 'test2', timestamp: 123457 }, + { message: 'test3', timestamp: 123455 }, + ], + }), + { + event_id: expect.any(String), + originalException: error, + syntheticException: expect.any(Error), + }, + ); + }); +}); diff --git a/packages/node-core/test/integration/scope.test.ts b/packages/node-core/test/integration/scope.test.ts new file mode 100644 index 000000000000..22bb1867ed52 --- /dev/null +++ b/packages/node-core/test/integration/scope.test.ts @@ -0,0 +1,684 @@ +import { getCapturedScopesOnSpan, getCurrentScope } from '@sentry/core'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import * as Sentry from '../../src/'; +import { cleanupOtel, mockSdkInit, resetGlobals } from '../helpers/mockSdkInit'; + +describe('Integration | Scope', () => { + afterEach(() => { + cleanupOtel(); + }); + + describe.each([ + ['with tracing', true], + ['without tracing', false], + ])('%s', (_name, tracingEnabled) => { + it('correctly syncs OTEL context & Sentry hub/scope', async () => { + const beforeSend = vi.fn(() => null); + const beforeSendTransaction = vi.fn(() => null); + + const client = mockSdkInit({ tracesSampleRate: tracingEnabled ? 1 : 0, beforeSend, beforeSendTransaction }); + + const rootScope = getCurrentScope(); + + const error = new Error('test error'); + let spanId: string | undefined; + let traceId: string | undefined; + + rootScope.setTag('tag1', 'val1'); + + Sentry.withScope(scope1 => { + scope1.setTag('tag2', 'val2'); + + Sentry.withScope(scope2b => { + scope2b.setTag('tag3-b', 'val3-b'); + }); + + Sentry.withScope(scope2 => { + scope2.setTag('tag3', 'val3'); + + Sentry.startSpan({ name: 'outer' }, span => { + expect(getCapturedScopesOnSpan(span).scope).toBe(tracingEnabled ? scope2 : undefined); + + spanId = span.spanContext().spanId; + traceId = span.spanContext().traceId; + + Sentry.setTag('tag4', 'val4'); + + Sentry.captureException(error); + }); + }); + }); + + await client?.flush(); + + expect(beforeSend).toHaveBeenCalledTimes(1); + + if (spanId) { + expect(beforeSend).toHaveBeenCalledWith( + expect.objectContaining({ + contexts: expect.objectContaining({ + trace: { + span_id: spanId, + trace_id: traceId, + }, + }), + }), + { + event_id: expect.any(String), + originalException: error, + syntheticException: expect.any(Error), + }, + ); + } + + expect(beforeSend).toHaveBeenCalledWith( + expect.objectContaining({ + tags: { + tag1: 'val1', + tag2: 'val2', + tag3: 'val3', + tag4: 'val4', + }, + }), + { + event_id: expect.any(String), + originalException: error, + syntheticException: expect.any(Error), + }, + ); + + if (tracingEnabled) { + expect(beforeSendTransaction).toHaveBeenCalledTimes(1); + // Note: Scope for transaction is taken at `start` time, not `finish` time + expect(beforeSendTransaction).toHaveBeenCalledWith( + expect.objectContaining({ + contexts: expect.objectContaining({ + trace: { + data: { + 'sentry.origin': 'manual', + 'sentry.source': 'custom', + 'sentry.sample_rate': 1, + }, + span_id: spanId, + status: 'ok', + trace_id: traceId, + origin: 'manual', + }, + }), + spans: [], + start_timestamp: expect.any(Number), + tags: { + tag1: 'val1', + tag2: 'val2', + tag3: 'val3', + tag4: 'val4', + }, + timestamp: expect.any(Number), + transaction_info: { source: 'custom' }, + type: 'transaction', + }), + { + event_id: expect.any(String), + }, + ); + } + }); + + it('isolates parallel root scopes', async () => { + const beforeSend = vi.fn(() => null); + const beforeSendTransaction = vi.fn(() => null); + + const client = mockSdkInit({ tracesSampleRate: tracingEnabled ? 1 : 0, beforeSend, beforeSendTransaction }); + + const rootScope = getCurrentScope(); + + const error1 = new Error('test error 1'); + const error2 = new Error('test error 2'); + let spanId1: string | undefined; + let spanId2: string | undefined; + let traceId1: string | undefined; + let traceId2: string | undefined; + + rootScope.setTag('tag1', 'val1'); + + Sentry.withScope(scope1 => { + scope1.setTag('tag2', 'val2a'); + + Sentry.withScope(scope2 => { + scope2.setTag('tag3', 'val3a'); + + Sentry.startSpan({ name: 'outer' }, span => { + spanId1 = span.spanContext().spanId; + traceId1 = span.spanContext().traceId; + + Sentry.setTag('tag4', 'val4a'); + + Sentry.captureException(error1); + }); + }); + }); + + Sentry.withScope(scope1 => { + scope1.setTag('tag2', 'val2b'); + + Sentry.withScope(scope2 => { + scope2.setTag('tag3', 'val3b'); + + Sentry.startSpan({ name: 'outer' }, span => { + spanId2 = span.spanContext().spanId; + traceId2 = span.spanContext().traceId; + + Sentry.setTag('tag4', 'val4b'); + + Sentry.captureException(error2); + }); + }); + }); + + await client?.flush(); + + expect(beforeSend).toHaveBeenCalledTimes(2); + expect(beforeSend).toHaveBeenCalledWith( + expect.objectContaining({ + contexts: expect.objectContaining({ + trace: spanId1 + ? { + span_id: spanId1, + trace_id: traceId1, + } + : expect.any(Object), + }), + tags: { + tag1: 'val1', + tag2: 'val2a', + tag3: 'val3a', + tag4: 'val4a', + }, + }), + { + event_id: expect.any(String), + originalException: error1, + syntheticException: expect.any(Error), + }, + ); + expect(beforeSend).toHaveBeenCalledWith( + expect.objectContaining({ + contexts: expect.objectContaining({ + trace: spanId2 + ? { + span_id: spanId2, + trace_id: traceId2, + } + : expect.any(Object), + }), + tags: { + tag1: 'val1', + tag2: 'val2b', + tag3: 'val3b', + tag4: 'val4b', + }, + }), + { + event_id: expect.any(String), + originalException: error2, + syntheticException: expect.any(Error), + }, + ); + + if (tracingEnabled) { + expect(beforeSendTransaction).toHaveBeenCalledTimes(2); + } + }); + }); + + describe('global scope', () => { + beforeEach(() => { + resetGlobals(); + }); + + it('works before calling init', () => { + const globalScope = Sentry.getGlobalScope(); + expect(globalScope).toBeDefined(); + // No client attached + expect(globalScope.getClient()).toBeUndefined(); + // Repeatedly returns the same instance + expect(Sentry.getGlobalScope()).toBe(globalScope); + + globalScope.setTag('tag1', 'val1'); + globalScope.setTag('tag2', 'val2'); + + expect(globalScope.getScopeData().tags).toEqual({ tag1: 'val1', tag2: 'val2' }); + + // Now when we call init, the global scope remains intact + Sentry.init({ dsn: 'https://username@domain/123', defaultIntegrations: false }); + + expect(globalScope.getClient()).toBeUndefined(); + expect(Sentry.getGlobalScope()).toBe(globalScope); + expect(globalScope.getScopeData().tags).toEqual({ tag1: 'val1', tag2: 'val2' }); + }); + + it('is applied to events', async () => { + const beforeSend = vi.fn(); + const client = mockSdkInit({ beforeSend }); + + const globalScope = Sentry.getGlobalScope(); + globalScope.setTag('tag1', 'val1'); + globalScope.setTag('tag2', 'val2'); + + const error = new Error('test error'); + Sentry.captureException(error); + + await client?.flush(); + + expect(beforeSend).toHaveBeenCalledTimes(1); + expect(beforeSend).toHaveBeenCalledWith( + expect.objectContaining({ + tags: { + tag1: 'val1', + tag2: 'val2', + }, + }), + { + event_id: expect.any(String), + originalException: error, + syntheticException: expect.any(Error), + }, + ); + }); + }); + + describe('isolation scope', () => { + beforeEach(() => { + resetGlobals(); + }); + + it('works before calling init', () => { + const isolationScope = Sentry.getIsolationScope(); + expect(isolationScope).toBeDefined(); + // No client attached + expect(isolationScope.getClient()).toBeUndefined(); + // Repeatedly returns the same instance + expect(Sentry.getIsolationScope()).toBe(isolationScope); + + isolationScope.setTag('tag1', 'val1'); + isolationScope.setTag('tag2', 'val2'); + + expect(isolationScope.getScopeData().tags).toEqual({ tag1: 'val1', tag2: 'val2' }); + + // Now when we call init, the isolation scope remains intact + Sentry.init({ dsn: 'https://username@domain/123', defaultIntegrations: false }); + + // client is only attached to global scope by default + expect(isolationScope.getClient()).toBeUndefined(); + expect(Sentry.getIsolationScope()).toBe(isolationScope); + expect(isolationScope.getScopeData().tags).toEqual({ tag1: 'val1', tag2: 'val2' }); + }); + + it('is applied to events', async () => { + const beforeSend = vi.fn(); + mockSdkInit({ beforeSend }); + const client = Sentry.getClient(); + + const isolationScope = Sentry.getIsolationScope(); + isolationScope.setTag('tag1', 'val1'); + isolationScope.setTag('tag2', 'val2'); + + const error = new Error('test error'); + Sentry.captureException(error); + + await client?.flush(); + + expect(beforeSend).toHaveBeenCalledTimes(1); + expect(beforeSend).toHaveBeenCalledWith( + expect.objectContaining({ + tags: { + tag1: 'val1', + tag2: 'val2', + }, + }), + { + event_id: expect.any(String), + originalException: error, + syntheticException: expect.any(Error), + }, + ); + }); + + it('withIsolationScope works', async () => { + const beforeSend = vi.fn(); + mockSdkInit({ beforeSend }); + const client = Sentry.getClient(); + + const initialIsolationScope = Sentry.getIsolationScope(); + initialIsolationScope.setTag('tag1', 'val1'); + initialIsolationScope.setTag('tag2', 'val2'); + + const initialCurrentScope = Sentry.getCurrentScope(); + + const error = new Error('test error'); + + Sentry.withIsolationScope(newIsolationScope => { + newIsolationScope.setTag('tag4', 'val4'); + }); + + Sentry.withIsolationScope(newIsolationScope => { + expect(Sentry.getCurrentScope()).not.toBe(initialCurrentScope); + expect(Sentry.getIsolationScope()).toBe(newIsolationScope); + expect(newIsolationScope).not.toBe(initialIsolationScope); + + // Data is forked off original isolation scope + expect(newIsolationScope.getScopeData().tags).toEqual({ tag1: 'val1', tag2: 'val2' }); + newIsolationScope.setTag('tag3', 'val3'); + + Sentry.captureException(error); + }); + + expect(initialIsolationScope.getScopeData().tags).toEqual({ tag1: 'val1', tag2: 'val2' }); + + await client?.flush(); + + expect(beforeSend).toHaveBeenCalledTimes(1); + expect(beforeSend).toHaveBeenCalledWith( + expect.objectContaining({ + tags: { + tag1: 'val1', + tag2: 'val2', + tag3: 'val3', + }, + }), + { + event_id: expect.any(String), + originalException: error, + syntheticException: expect.any(Error), + }, + ); + }); + + it('can be deeply nested', async () => { + const beforeSend = vi.fn(); + mockSdkInit({ beforeSend }); + const client = Sentry.getClient(); + + const initialIsolationScope = Sentry.getIsolationScope(); + initialIsolationScope.setTag('tag1', 'val1'); + + const error = new Error('test error'); + + Sentry.withIsolationScope(newIsolationScope => { + newIsolationScope.setTag('tag2', 'val2'); + + Sentry.withIsolationScope(newIsolationScope => { + newIsolationScope.setTag('tag3', 'val3'); + + Sentry.withIsolationScope(newIsolationScope => { + newIsolationScope.setTag('tag4', 'val4'); + }); + + Sentry.captureException(error); + }); + }); + + await client?.flush(); + + expect(beforeSend).toHaveBeenCalledTimes(1); + expect(beforeSend).toHaveBeenCalledWith( + expect.objectContaining({ + tags: { + tag1: 'val1', + tag2: 'val2', + tag3: 'val3', + }, + }), + { + event_id: expect.any(String), + originalException: error, + syntheticException: expect.any(Error), + }, + ); + }); + }); + + describe('current scope', () => { + beforeEach(() => { + resetGlobals(); + }); + + it('works before calling init', () => { + const currentScope = Sentry.getCurrentScope(); + expect(currentScope).toBeDefined(); + // No client attached + expect(currentScope.getClient()).toBeUndefined(); + // Repeatedly returns the same instance + expect(Sentry.getCurrentScope()).toBe(currentScope); + + currentScope.setTag('tag1', 'val1'); + currentScope.setTag('tag2', 'val2'); + + expect(currentScope.getScopeData().tags).toEqual({ tag1: 'val1', tag2: 'val2' }); + + // Now when we call init, the current scope remains intact + Sentry.init({ dsn: 'https://username@domain/123', defaultIntegrations: false }); + + // client is attached to current scope + expect(currentScope.getClient()).toBeDefined(); + + expect(Sentry.getCurrentScope()).toBe(currentScope); + expect(currentScope.getScopeData().tags).toEqual({ tag1: 'val1', tag2: 'val2' }); + }); + + it('is applied to events', async () => { + const beforeSend = vi.fn(); + mockSdkInit({ beforeSend }); + const client = Sentry.getClient(); + + const currentScope = Sentry.getCurrentScope(); + currentScope.setTag('tag1', 'val1'); + currentScope.setTag('tag2', 'val2'); + + const error = new Error('test error'); + Sentry.captureException(error); + + await client?.flush(); + + expect(beforeSend).toHaveBeenCalledTimes(1); + expect(beforeSend).toHaveBeenCalledWith( + expect.objectContaining({ + tags: { + tag1: 'val1', + tag2: 'val2', + }, + }), + { + event_id: expect.any(String), + originalException: error, + syntheticException: expect.any(Error), + }, + ); + }); + + it('withScope works', async () => { + const beforeSend = vi.fn(); + mockSdkInit({ beforeSend }); + const client = Sentry.getClient(); + + const isolationScope = Sentry.getIsolationScope(); + const initialCurrentScope = Sentry.getCurrentScope(); + initialCurrentScope.setTag('tag1', 'val1'); + initialCurrentScope.setTag('tag2', 'val2'); + + const error = new Error('test error'); + + Sentry.withScope(newCurrentScope => { + newCurrentScope.setTag('tag4', 'val4'); + }); + + Sentry.withScope(newCurrentScope => { + expect(Sentry.getCurrentScope()).toBe(newCurrentScope); + expect(Sentry.getIsolationScope()).toBe(isolationScope); + expect(newCurrentScope).not.toBe(initialCurrentScope); + + // Data is forked off original isolation scope + expect(newCurrentScope.getScopeData().tags).toEqual({ tag1: 'val1', tag2: 'val2' }); + newCurrentScope.setTag('tag3', 'val3'); + + Sentry.captureException(error); + }); + + expect(initialCurrentScope.getScopeData().tags).toEqual({ tag1: 'val1', tag2: 'val2' }); + + await client?.flush(); + + expect(beforeSend).toHaveBeenCalledTimes(1); + expect(beforeSend).toHaveBeenCalledWith( + expect.objectContaining({ + tags: { + tag1: 'val1', + tag2: 'val2', + tag3: 'val3', + }, + }), + { + event_id: expect.any(String), + originalException: error, + syntheticException: expect.any(Error), + }, + ); + }); + + it('can be deeply nested', async () => { + const beforeSend = vi.fn(); + mockSdkInit({ beforeSend }); + const client = Sentry.getClient(); + + const initialCurrentScope = Sentry.getCurrentScope(); + initialCurrentScope.setTag('tag1', 'val1'); + + const error = new Error('test error'); + + Sentry.withScope(currentScope => { + currentScope.setTag('tag2', 'val2'); + expect(Sentry.getCurrentScope()).toBe(currentScope); + + Sentry.withScope(currentScope => { + currentScope.setTag('tag3', 'val3'); + expect(Sentry.getCurrentScope()).toBe(currentScope); + + Sentry.withScope(currentScope => { + currentScope.setTag('tag4', 'val4'); + expect(Sentry.getCurrentScope()).toBe(currentScope); + }); + + Sentry.captureException(error); + }); + }); + + await client?.flush(); + + expect(beforeSend).toHaveBeenCalledTimes(1); + expect(beforeSend).toHaveBeenCalledWith( + expect.objectContaining({ + tags: { + tag1: 'val1', + tag2: 'val2', + tag3: 'val3', + }, + }), + { + event_id: expect.any(String), + originalException: error, + syntheticException: expect.any(Error), + }, + ); + }); + + it('automatically forks with OTEL context', async () => { + const beforeSend = vi.fn(); + mockSdkInit({ beforeSend }); + const client = Sentry.getClient(); + + const initialCurrentScope = Sentry.getCurrentScope(); + initialCurrentScope.setTag('tag1', 'val1'); + + const error = new Error('test error'); + + Sentry.startSpan({ name: 'outer' }, () => { + Sentry.getCurrentScope().setTag('tag2', 'val2'); + + Sentry.startSpan({ name: 'inner 1' }, () => { + Sentry.getCurrentScope().setTag('tag3', 'val3'); + + Sentry.startSpan({ name: 'inner 2' }, () => { + Sentry.getCurrentScope().setTag('tag4', 'val4'); + }); + + Sentry.captureException(error); + }); + }); + + await client?.flush(); + + expect(beforeSend).toHaveBeenCalledTimes(1); + expect(beforeSend).toHaveBeenCalledWith( + expect.objectContaining({ + tags: { + tag1: 'val1', + tag2: 'val2', + tag3: 'val3', + }, + }), + { + event_id: expect.any(String), + originalException: error, + syntheticException: expect.any(Error), + }, + ); + }); + }); + + describe('scope merging', () => { + beforeEach(() => { + resetGlobals(); + }); + + it('merges data from global, isolation and current scope', async () => { + const beforeSend = vi.fn(); + mockSdkInit({ beforeSend }); + const client = Sentry.getClient(); + + Sentry.getGlobalScope().setTag('tag1', 'val1'); + + const error = new Error('test error'); + + Sentry.withIsolationScope(isolationScope => { + Sentry.getCurrentScope().setTag('tag2', 'val2a'); + isolationScope.setTag('tag2', 'val2b'); + isolationScope.setTag('tag3', 'val3'); + + Sentry.withScope(currentScope => { + currentScope.setTag('tag4', 'val4'); + + Sentry.captureException(error); + }); + }); + + await client?.flush(); + + expect(beforeSend).toHaveBeenCalledTimes(1); + expect(beforeSend).toHaveBeenCalledWith( + expect.objectContaining({ + tags: { + tag1: 'val1', + tag2: 'val2a', + tag3: 'val3', + tag4: 'val4', + }, + }), + { + event_id: expect.any(String), + originalException: error, + syntheticException: expect.any(Error), + }, + ); + }); + }); +}); diff --git a/packages/node-core/test/integration/transactions.test.ts b/packages/node-core/test/integration/transactions.test.ts new file mode 100644 index 000000000000..db499cd368df --- /dev/null +++ b/packages/node-core/test/integration/transactions.test.ts @@ -0,0 +1,685 @@ +import { context, trace, TraceFlags } from '@opentelemetry/api'; +import type { SpanProcessor } from '@opentelemetry/sdk-trace-base'; +import type { TransactionEvent } from '@sentry/core'; +import { logger, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SEMANTIC_ATTRIBUTE_SENTRY_SOURCE } from '@sentry/core'; +import { SentrySpanProcessor } from '@sentry/opentelemetry'; +import { afterEach, describe, expect, it, vi } from 'vitest'; +import * as Sentry from '../../src'; +import { cleanupOtel, getProvider, mockSdkInit } from '../helpers/mockSdkInit'; + +describe('Integration | Transactions', () => { + afterEach(() => { + vi.restoreAllMocks(); + cleanupOtel(); + }); + + it('correctly creates transaction & spans', async () => { + const transactions: TransactionEvent[] = []; + const beforeSendTransaction = vi.fn(event => { + transactions.push(event); + return null; + }); + + mockSdkInit({ + tracesSampleRate: 1, + beforeSendTransaction, + release: '8.0.0', + }); + + const client = Sentry.getClient()!; + + Sentry.addBreadcrumb({ message: 'test breadcrumb 1', timestamp: 123456 }); + Sentry.setTag('outer.tag', 'test value'); + + Sentry.startSpan( + { + op: 'test op', + name: 'test name', + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'task', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.test', + }, + }, + span => { + Sentry.addBreadcrumb({ message: 'test breadcrumb 2', timestamp: 123456 }); + + span.setAttributes({ + 'test.outer': 'test value', + }); + + const subSpan = Sentry.startInactiveSpan({ name: 'inner span 1' }); + subSpan.end(); + + Sentry.setTag('test.tag', 'test value'); + + Sentry.startSpan({ name: 'inner span 2' }, innerSpan => { + Sentry.addBreadcrumb({ message: 'test breadcrumb 3', timestamp: 123456 }); + + innerSpan.setAttributes({ + 'test.inner': 'test value', + }); + }); + }, + ); + + await client.flush(); + + expect(transactions).toHaveLength(1); + const transaction = transactions[0]!; + + expect(transaction.breadcrumbs).toEqual([ + { message: 'test breadcrumb 1', timestamp: 123456 }, + { message: 'test breadcrumb 2', timestamp: 123456 }, + { message: 'test breadcrumb 3', timestamp: 123456 }, + ]); + + expect(transaction.contexts?.otel).toEqual({ + resource: { + 'service.name': 'node', + 'service.namespace': 'sentry', + 'service.version': expect.any(String), + 'telemetry.sdk.language': 'nodejs', + 'telemetry.sdk.name': 'opentelemetry', + 'telemetry.sdk.version': expect.any(String), + }, + }); + + expect(transaction.contexts?.trace).toEqual({ + data: { + 'sentry.op': 'test op', + 'sentry.origin': 'auto.test', + 'sentry.source': 'task', + 'sentry.sample_rate': 1, + 'test.outer': 'test value', + }, + op: 'test op', + span_id: expect.stringMatching(/[a-f0-9]{16}/), + status: 'ok', + trace_id: expect.stringMatching(/[a-f0-9]{32}/), + origin: 'auto.test', + }); + + expect(transaction.sdkProcessingMetadata?.sampleRate).toEqual(1); + expect(transaction.sdkProcessingMetadata?.dynamicSamplingContext).toEqual({ + environment: 'production', + public_key: expect.any(String), + sample_rate: '1', + sampled: 'true', + release: '8.0.0', + trace_id: expect.stringMatching(/[a-f0-9]{32}/), + transaction: 'test name', + sample_rand: expect.any(String), + }); + + expect(transaction.environment).toEqual('production'); + expect(transaction.event_id).toEqual(expect.any(String)); + expect(transaction.start_timestamp).toEqual(expect.any(Number)); + expect(transaction.timestamp).toEqual(expect.any(Number)); + expect(transaction.transaction).toEqual('test name'); + + expect(transaction.tags).toEqual({ + 'outer.tag': 'test value', + 'test.tag': 'test value', + }); + expect(transaction.transaction_info).toEqual({ source: 'task' }); + expect(transaction.type).toEqual('transaction'); + + expect(transaction.spans).toHaveLength(2); + const spans = transaction.spans || []; + + // note: Currently, spans do not have any context/span added to them + // This is the same behavior as for the "regular" SDKs + expect(spans).toEqual([ + { + data: { + 'sentry.origin': 'manual', + }, + description: 'inner span 1', + origin: 'manual', + parent_span_id: expect.stringMatching(/[a-f0-9]{16}/), + span_id: expect.stringMatching(/[a-f0-9]{16}/), + start_timestamp: expect.any(Number), + status: 'ok', + timestamp: expect.any(Number), + trace_id: expect.stringMatching(/[a-f0-9]{32}/), + }, + { + data: { + 'test.inner': 'test value', + 'sentry.origin': 'manual', + }, + description: 'inner span 2', + origin: 'manual', + parent_span_id: expect.stringMatching(/[a-f0-9]{16}/), + span_id: expect.stringMatching(/[a-f0-9]{16}/), + start_timestamp: expect.any(Number), + status: 'ok', + timestamp: expect.any(Number), + trace_id: expect.stringMatching(/[a-f0-9]{32}/), + }, + ]); + }); + + it('correctly creates concurrent transaction & spans', async () => { + const beforeSendTransaction = vi.fn(() => null); + + mockSdkInit({ tracesSampleRate: 1, beforeSendTransaction }); + + const client = Sentry.getClient()!; + + Sentry.addBreadcrumb({ message: 'test breadcrumb 1', timestamp: 123456 }); + + Sentry.withIsolationScope(() => { + Sentry.startSpan( + { + op: 'test op', + name: 'test name', + attributes: { + [Sentry.SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'task', + [Sentry.SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.test', + }, + }, + span => { + Sentry.addBreadcrumb({ message: 'test breadcrumb 2', timestamp: 123456 }); + + span.setAttributes({ + 'test.outer': 'test value', + }); + + const subSpan = Sentry.startInactiveSpan({ name: 'inner span 1' }); + subSpan.end(); + + Sentry.setTag('test.tag', 'test value'); + + Sentry.startSpan({ name: 'inner span 2' }, innerSpan => { + Sentry.addBreadcrumb({ message: 'test breadcrumb 3', timestamp: 123456 }); + + innerSpan.setAttributes({ + 'test.inner': 'test value', + }); + }); + }, + ); + }); + + Sentry.withIsolationScope(() => { + Sentry.startSpan({ op: 'test op b', name: 'test name b' }, span => { + Sentry.addBreadcrumb({ message: 'test breadcrumb 2b', timestamp: 123456 }); + + span.setAttributes({ + 'test.outer': 'test value b', + }); + + const subSpan = Sentry.startInactiveSpan({ name: 'inner span 1b' }); + subSpan.end(); + + Sentry.setTag('test.tag', 'test value b'); + + Sentry.startSpan({ name: 'inner span 2b' }, innerSpan => { + Sentry.addBreadcrumb({ message: 'test breadcrumb 3b', timestamp: 123456 }); + + innerSpan.setAttributes({ + 'test.inner': 'test value b', + }); + }); + }); + }); + + await client.flush(); + + expect(beforeSendTransaction).toHaveBeenCalledTimes(2); + expect(beforeSendTransaction).toHaveBeenCalledWith( + expect.objectContaining({ + breadcrumbs: [ + { message: 'test breadcrumb 1', timestamp: 123456 }, + { message: 'test breadcrumb 2', timestamp: 123456 }, + { message: 'test breadcrumb 3', timestamp: 123456 }, + ], + contexts: expect.objectContaining({ + trace: { + data: { + 'sentry.op': 'test op', + 'sentry.origin': 'auto.test', + 'sentry.source': 'task', + 'test.outer': 'test value', + 'sentry.sample_rate': 1, + }, + op: 'test op', + span_id: expect.stringMatching(/[a-f0-9]{16}/), + status: 'ok', + trace_id: expect.stringMatching(/[a-f0-9]{32}/), + origin: 'auto.test', + }, + }), + spans: [expect.any(Object), expect.any(Object)], + start_timestamp: expect.any(Number), + tags: { + 'test.tag': 'test value', + }, + timestamp: expect.any(Number), + transaction: 'test name', + transaction_info: { source: 'task' }, + type: 'transaction', + }), + { + event_id: expect.any(String), + }, + ); + + expect(beforeSendTransaction).toHaveBeenCalledWith( + expect.objectContaining({ + breadcrumbs: [ + { message: 'test breadcrumb 1', timestamp: 123456 }, + { message: 'test breadcrumb 2b', timestamp: 123456 }, + { message: 'test breadcrumb 3b', timestamp: 123456 }, + ], + contexts: expect.objectContaining({ + trace: { + data: { + 'sentry.op': 'test op b', + 'sentry.origin': 'manual', + 'sentry.source': 'custom', + 'test.outer': 'test value b', + 'sentry.sample_rate': 1, + }, + op: 'test op b', + span_id: expect.stringMatching(/[a-f0-9]{16}/), + status: 'ok', + trace_id: expect.stringMatching(/[a-f0-9]{32}/), + origin: 'manual', + }, + }), + spans: [expect.any(Object), expect.any(Object)], + start_timestamp: expect.any(Number), + tags: { + 'test.tag': 'test value b', + }, + timestamp: expect.any(Number), + transaction: 'test name b', + transaction_info: { source: 'custom' }, + type: 'transaction', + }), + { + event_id: expect.any(String), + }, + ); + }); + + it('correctly creates concurrent transaction & spans when using native OTEL tracer', async () => { + const beforeSendTransaction = vi.fn(() => null); + + mockSdkInit({ tracesSampleRate: 1, beforeSendTransaction }); + + const client = Sentry.getClient(); + + Sentry.addBreadcrumb({ message: 'test breadcrumb 1', timestamp: 123456 }); + + Sentry.withIsolationScope(() => { + client?.tracer.startActiveSpan('test name', span => { + Sentry.addBreadcrumb({ message: 'test breadcrumb 2', timestamp: 123456 }); + + span.setAttributes({ + 'test.outer': 'test value', + }); + + const subSpan = Sentry.startInactiveSpan({ name: 'inner span 1' }); + subSpan.end(); + + Sentry.setTag('test.tag', 'test value'); + + client.tracer.startActiveSpan('inner span 2', innerSpan => { + Sentry.addBreadcrumb({ message: 'test breadcrumb 3', timestamp: 123456 }); + + innerSpan.setAttributes({ + 'test.inner': 'test value', + }); + + innerSpan.end(); + }); + + span.end(); + }); + }); + + Sentry.withIsolationScope(() => { + client?.tracer.startActiveSpan('test name b', span => { + Sentry.addBreadcrumb({ message: 'test breadcrumb 2b', timestamp: 123456 }); + + span.setAttributes({ + 'test.outer': 'test value b', + }); + + const subSpan = Sentry.startInactiveSpan({ name: 'inner span 1b' }); + subSpan.end(); + + Sentry.setTag('test.tag', 'test value b'); + + client.tracer.startActiveSpan('inner span 2b', innerSpan => { + Sentry.addBreadcrumb({ message: 'test breadcrumb 3b', timestamp: 123456 }); + + innerSpan.setAttributes({ + 'test.inner': 'test value b', + }); + + innerSpan.end(); + }); + + span.end(); + }); + }); + + await client?.flush(); + + expect(beforeSendTransaction).toHaveBeenCalledTimes(2); + expect(beforeSendTransaction).toHaveBeenCalledWith( + expect.objectContaining({ + breadcrumbs: [ + { message: 'test breadcrumb 1', timestamp: 123456 }, + { message: 'test breadcrumb 2', timestamp: 123456 }, + { message: 'test breadcrumb 3', timestamp: 123456 }, + ], + contexts: expect.objectContaining({ + trace: { + data: { + 'sentry.origin': 'manual', + 'sentry.source': 'custom', + 'test.outer': 'test value', + 'sentry.sample_rate': 1, + }, + span_id: expect.stringMatching(/[a-f0-9]{16}/), + status: 'ok', + trace_id: expect.stringMatching(/[a-f0-9]{32}/), + origin: 'manual', + }, + }), + spans: [expect.any(Object), expect.any(Object)], + start_timestamp: expect.any(Number), + tags: { + 'test.tag': 'test value', + }, + timestamp: expect.any(Number), + transaction: 'test name', + type: 'transaction', + }), + { + event_id: expect.any(String), + }, + ); + + expect(beforeSendTransaction).toHaveBeenCalledWith( + expect.objectContaining({ + breadcrumbs: [ + { message: 'test breadcrumb 1', timestamp: 123456 }, + { message: 'test breadcrumb 2b', timestamp: 123456 }, + { message: 'test breadcrumb 3b', timestamp: 123456 }, + ], + contexts: expect.objectContaining({ + trace: { + data: { + 'sentry.origin': 'manual', + 'sentry.source': 'custom', + 'test.outer': 'test value b', + 'sentry.sample_rate': 1, + }, + span_id: expect.stringMatching(/[a-f0-9]{16}/), + status: 'ok', + trace_id: expect.stringMatching(/[a-f0-9]{32}/), + origin: 'manual', + }, + }), + spans: [expect.any(Object), expect.any(Object)], + start_timestamp: expect.any(Number), + tags: { + 'test.tag': 'test value b', + }, + timestamp: expect.any(Number), + transaction: 'test name b', + type: 'transaction', + }), + { + event_id: expect.any(String), + }, + ); + }); + + it('correctly creates transaction & spans with a trace header data', async () => { + const beforeSendTransaction = vi.fn(() => null); + + const traceId = 'd4cda95b652f4a1592b449d5929fda1b'; + const parentSpanId = '6e0c63257de34c92'; + + const spanContext = { + traceId, + spanId: parentSpanId, + sampled: true, + isRemote: true, + traceFlags: TraceFlags.SAMPLED, + }; + + mockSdkInit({ tracesSampleRate: 1, beforeSendTransaction }); + + const client = Sentry.getClient()!; + + // We simulate the correct context we'd normally get from the SentryPropagator + context.with(trace.setSpanContext(context.active(), spanContext), () => { + Sentry.startSpan( + { + op: 'test op', + name: 'test name', + attributes: { + [Sentry.SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'task', + [Sentry.SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.test', + }, + }, + () => { + const subSpan = Sentry.startInactiveSpan({ name: 'inner span 1' }); + subSpan.end(); + + Sentry.startSpan({ name: 'inner span 2' }, () => {}); + }, + ); + }); + + await client.flush(); + + expect(beforeSendTransaction).toHaveBeenCalledTimes(1); + expect(beforeSendTransaction).toHaveBeenLastCalledWith( + expect.objectContaining({ + contexts: expect.objectContaining({ + trace: { + data: { + 'sentry.op': 'test op', + 'sentry.origin': 'auto.test', + 'sentry.source': 'task', + }, + op: 'test op', + span_id: expect.stringMatching(/[a-f0-9]{16}/), + parent_span_id: parentSpanId, + status: 'ok', + trace_id: traceId, + origin: 'auto.test', + }, + }), + // spans are circular (they have a reference to the transaction), which leads to jest choking on this + // instead we compare them in detail below + spans: [expect.any(Object), expect.any(Object)], + start_timestamp: expect.any(Number), + timestamp: expect.any(Number), + transaction: 'test name', + transaction_info: { source: 'task' }, + type: 'transaction', + }), + { + event_id: expect.any(String), + }, + ); + + // Checking the spans here, as they are circular to the transaction... + const runArgs = beforeSendTransaction.mock.calls[0] as unknown as [TransactionEvent, unknown]; + const spans = runArgs[0].spans || []; + + // note: Currently, spans do not have any context/span added to them + // This is the same behavior as for the "regular" SDKs + expect(spans).toEqual([ + { + data: { + 'sentry.origin': 'manual', + }, + description: 'inner span 1', + origin: 'manual', + parent_span_id: expect.stringMatching(/[a-f0-9]{16}/), + span_id: expect.stringMatching(/[a-f0-9]{16}/), + start_timestamp: expect.any(Number), + status: 'ok', + timestamp: expect.any(Number), + trace_id: traceId, + }, + { + data: { + 'sentry.origin': 'manual', + }, + description: 'inner span 2', + origin: 'manual', + parent_span_id: expect.stringMatching(/[a-f0-9]{16}/), + span_id: expect.stringMatching(/[a-f0-9]{16}/), + start_timestamp: expect.any(Number), + status: 'ok', + timestamp: expect.any(Number), + trace_id: traceId, + }, + ]); + }); + + it('cleans up spans that are not flushed for over 5 mins', async () => { + const beforeSendTransaction = vi.fn(() => null); + + const now = Date.now(); + vi.useFakeTimers(); + vi.setSystemTime(now); + + const logs: unknown[] = []; + vi.spyOn(logger, 'log').mockImplementation(msg => logs.push(msg)); + + mockSdkInit({ tracesSampleRate: 1, beforeSendTransaction }); + + const provider = getProvider(); + const multiSpanProcessor = provider?.activeSpanProcessor as + | (SpanProcessor & { _spanProcessors?: SpanProcessor[] }) + | undefined; + const spanProcessor = multiSpanProcessor?.['_spanProcessors']?.find( + spanProcessor => spanProcessor instanceof SentrySpanProcessor, + ) as SentrySpanProcessor | undefined; + + const exporter = spanProcessor ? spanProcessor['_exporter'] : undefined; + + if (!exporter) { + throw new Error('No exporter found, aborting test...'); + } + + void Sentry.startSpan({ name: 'test name' }, async () => { + Sentry.startInactiveSpan({ name: 'inner span 1' }).end(); + Sentry.startInactiveSpan({ name: 'inner span 2' }).end(); + + // Pretend this is pending for 10 minutes + await new Promise(resolve => setTimeout(resolve, 10 * 60 * 1000)); + }); + + vi.advanceTimersByTime(1); + + // Child-spans have been added to the exporter, but they are pending since they are waiting for their parent + const finishedSpans1 = []; + exporter['_finishedSpanBuckets'].forEach((bucket: any) => { + if (bucket) { + finishedSpans1.push(...bucket.spans); + } + }); + expect(finishedSpans1.length).toBe(2); + expect(beforeSendTransaction).toHaveBeenCalledTimes(0); + + // Now wait for 5 mins + vi.advanceTimersByTime(5 * 60 * 1_000 + 1); + + // Adding another span will trigger the cleanup + Sentry.startSpan({ name: 'other span' }, () => {}); + + vi.advanceTimersByTime(1); + + // Old spans have been cleared away + const finishedSpans2 = []; + exporter['_finishedSpanBuckets'].forEach((bucket: any) => { + if (bucket) { + finishedSpans2.push(...bucket.spans); + } + }); + expect(finishedSpans2.length).toBe(0); + + // Called once for the 'other span' + expect(beforeSendTransaction).toHaveBeenCalledTimes(1); + + expect(logs).toEqual( + expect.arrayContaining([ + 'SpanExporter dropped 2 spans because they were pending for more than 300 seconds.', + 'SpanExporter exported 1 spans, 0 spans are waiting for their parent spans to finish', + ]), + ); + }); + + it('allows to configure `maxSpanWaitDuration` to capture long running spans', async () => { + const transactions: TransactionEvent[] = []; + const beforeSendTransaction = vi.fn(event => { + transactions.push(event); + return null; + }); + + const now = Date.now(); + vi.useFakeTimers(); + vi.setSystemTime(now); + + const logs: unknown[] = []; + vi.spyOn(logger, 'log').mockImplementation(msg => logs.push(msg)); + + mockSdkInit({ + tracesSampleRate: 1, + beforeSendTransaction, + maxSpanWaitDuration: 100 * 60, + }); + + Sentry.startSpanManual({ name: 'test name' }, rootSpan => { + const subSpan = Sentry.startInactiveSpan({ name: 'inner span 1' }); + subSpan.end(); + + Sentry.startSpanManual({ name: 'inner span 2' }, innerSpan => { + // Child span ends after 10 min + setTimeout( + () => { + innerSpan.end(); + }, + 10 * 60 * 1_000, + ); + }); + + // root span ends after 99 min + setTimeout( + () => { + rootSpan.end(); + }, + 99 * 10 * 1_000, + ); + }); + + // Now wait for 100 mins + vi.advanceTimersByTime(100 * 60 * 1_000); + + expect(beforeSendTransaction).toHaveBeenCalledTimes(1); + expect(transactions).toHaveLength(1); + const transaction = transactions[0]!; + + expect(transaction.transaction).toEqual('test name'); + const spans = transaction.spans || []; + + expect(spans).toHaveLength(2); + + expect(spans).toContainEqual(expect.objectContaining({ description: 'inner span 1' })); + expect(spans).toContainEqual(expect.objectContaining({ description: 'inner span 2' })); + }); +}); diff --git a/packages/node/test/integrations/context.test.ts b/packages/node-core/test/integrations/context.test.ts similarity index 100% rename from packages/node/test/integrations/context.test.ts rename to packages/node-core/test/integrations/context.test.ts diff --git a/packages/node/test/integrations/contextlines.test.ts b/packages/node-core/test/integrations/contextlines.test.ts similarity index 100% rename from packages/node/test/integrations/contextlines.test.ts rename to packages/node-core/test/integrations/contextlines.test.ts diff --git a/packages/node/test/integrations/localvariables.test.ts b/packages/node-core/test/integrations/localvariables.test.ts similarity index 100% rename from packages/node/test/integrations/localvariables.test.ts rename to packages/node-core/test/integrations/localvariables.test.ts diff --git a/packages/node/test/integrations/request-session-tracking.test.ts b/packages/node-core/test/integrations/request-session-tracking.test.ts similarity index 100% rename from packages/node/test/integrations/request-session-tracking.test.ts rename to packages/node-core/test/integrations/request-session-tracking.test.ts diff --git a/packages/node/test/integrations/spotlight.test.ts b/packages/node-core/test/integrations/spotlight.test.ts similarity index 100% rename from packages/node/test/integrations/spotlight.test.ts rename to packages/node-core/test/integrations/spotlight.test.ts diff --git a/packages/node/test/logs/exports.test.ts b/packages/node-core/test/logs/exports.test.ts similarity index 100% rename from packages/node/test/logs/exports.test.ts rename to packages/node-core/test/logs/exports.test.ts diff --git a/packages/node-core/test/sdk/api.test.ts b/packages/node-core/test/sdk/api.test.ts new file mode 100644 index 000000000000..5c2c32ad13d0 --- /dev/null +++ b/packages/node-core/test/sdk/api.test.ts @@ -0,0 +1,104 @@ +import type { Event } from '@sentry/core'; +import { afterEach, describe, expect, it, vi } from 'vitest'; +import { getActiveSpan, getClient, startInactiveSpan, startSpan, withActiveSpan } from '../../src'; +import { cleanupOtel, mockSdkInit } from '../helpers/mockSdkInit'; + +afterEach(() => { + vi.restoreAllMocks(); + cleanupOtel(); +}); + +describe('withActiveSpan()', () => { + it('should set the active span within the callback', () => { + mockSdkInit({ tracesSampleRate: 1 }); + + const inactiveSpan = startInactiveSpan({ name: 'inactive-span' }); + + expect(getActiveSpan()).not.toBe(inactiveSpan); + + withActiveSpan(inactiveSpan, () => { + expect(getActiveSpan()).toBe(inactiveSpan); + }); + }); + + it('should create child spans when calling startSpan within the callback', async () => { + const beforeSendTransaction = vi.fn(() => null); + mockSdkInit({ tracesSampleRate: 1, beforeSendTransaction }); + const client = getClient(); + + const inactiveSpan = startInactiveSpan({ name: 'inactive-span' }); + + withActiveSpan(inactiveSpan, () => { + startSpan({ name: 'child-span' }, () => {}); + }); + + startSpan({ name: 'floating-span' }, () => {}); + + inactiveSpan.end(); + + await client?.flush(); + + // The child span should be a child of the inactive span + expect(beforeSendTransaction).toHaveBeenCalledWith( + expect.objectContaining({ + transaction: 'inactive-span', + spans: expect.arrayContaining([expect.any(Object)]), + }), + expect.anything(), + ); + + // The floating span should be a separate transaction + expect(beforeSendTransaction).toHaveBeenCalledWith( + expect.objectContaining({ + transaction: 'floating-span', + }), + expect.anything(), + ); + }); + + it('when `null` is passed, no span should be active within the callback', () => { + expect.assertions(1); + startSpan({ name: 'parent-span' }, () => { + withActiveSpan(null, () => { + expect(getActiveSpan()).toBeUndefined(); + }); + }); + }); + + it('when `null` is passed, should start a new trace for new spans', async () => { + const transactions: Event[] = []; + const beforeSendTransaction = vi.fn((event: Event) => { + transactions.push(event); + return null; + }); + mockSdkInit({ tracesSampleRate: 1, beforeSendTransaction }); + const client = getClient(); + + startSpan({ name: 'parent-span' }, () => { + withActiveSpan(null, () => { + startSpan({ name: 'child-span' }, () => {}); + }); + }); + + await client?.flush(); + + expect(beforeSendTransaction).toHaveBeenCalledTimes(2); + + // The child span should be a child of the inactive span + expect(beforeSendTransaction).toHaveBeenCalledWith( + expect.objectContaining({ + transaction: 'parent-span', + spans: expect.not.arrayContaining([expect.objectContaining({ description: 'child-span' })]), + }), + expect.anything(), + ); + + // The floating span should be a separate transaction + expect(beforeSendTransaction).toHaveBeenCalledWith( + expect.objectContaining({ + transaction: 'child-span', + }), + expect.anything(), + ); + }); +}); diff --git a/packages/node-core/test/sdk/client.test.ts b/packages/node-core/test/sdk/client.test.ts new file mode 100644 index 000000000000..f053b1ba7e0e --- /dev/null +++ b/packages/node-core/test/sdk/client.test.ts @@ -0,0 +1,324 @@ +import { ProxyTracer } from '@opentelemetry/api'; +import * as opentelemetryInstrumentationPackage from '@opentelemetry/instrumentation'; +import type { Event, EventHint, Log } from '@sentry/core'; +import { getCurrentScope, getGlobalScope, getIsolationScope, Scope, SDK_VERSION } from '@sentry/core'; +import { setOpenTelemetryContextAsyncContextStrategy } from '@sentry/opentelemetry'; +import * as os from 'os'; +import { afterEach, beforeEach, describe, expect, it, test, vi } from 'vitest'; +import { NodeClient } from '../../src'; +import { getDefaultNodeClientOptions } from '../helpers/getDefaultNodeClientOptions'; +import { cleanupOtel } from '../helpers/mockSdkInit'; + +describe('NodeClient', () => { + beforeEach(() => { + getIsolationScope().clear(); + getGlobalScope().clear(); + getCurrentScope().clear(); + getCurrentScope().setClient(undefined); + setOpenTelemetryContextAsyncContextStrategy(); + }); + + afterEach(() => { + vi.restoreAllMocks(); + cleanupOtel(); + }); + + it('sets correct metadata', () => { + const options = getDefaultNodeClientOptions(); + const client = new NodeClient(options); + + expect(client.getOptions()).toEqual({ + dsn: expect.any(String), + integrations: [], + transport: options.transport, + stackParser: options.stackParser, + _metadata: { + sdk: { + name: 'sentry.javascript.node', + packages: [ + { + name: 'npm:@sentry/node', + version: SDK_VERSION, + }, + ], + version: SDK_VERSION, + }, + }, + platform: 'node', + runtime: { name: 'node', version: expect.any(String) }, + serverName: expect.any(String), + tracesSampleRate: 1, + }); + }); + + it('exposes a tracer', () => { + const client = new NodeClient(getDefaultNodeClientOptions()); + + const tracer = client.tracer; + expect(tracer).toBeDefined(); + expect(tracer).toBeInstanceOf(ProxyTracer); + + // Ensure we always get the same tracer instance + const tracer2 = client.tracer; + + expect(tracer2).toBe(tracer); + }); + + describe('_prepareEvent', () => { + const currentScope = new Scope(); + const isolationScope = new Scope(); + + test('adds platform to event', () => { + const options = getDefaultNodeClientOptions({}); + const client = new NodeClient(options); + + const event: Event = {}; + const hint: EventHint = {}; + client['_prepareEvent'](event, hint, currentScope, isolationScope); + + expect(event.platform).toEqual('node'); + }); + + test('adds runtime context to event', () => { + const options = getDefaultNodeClientOptions({}); + const client = new NodeClient(options); + + const event: Event = {}; + const hint: EventHint = {}; + client['_prepareEvent'](event, hint, currentScope, isolationScope); + + expect(event.contexts?.runtime).toEqual({ + name: 'node', + version: process.version, + }); + }); + + test('adds server name to event when value passed in options', () => { + const options = getDefaultNodeClientOptions({ serverName: 'foo' }); + const client = new NodeClient(options); + + const event: Event = {}; + const hint: EventHint = {}; + client['_prepareEvent'](event, hint, currentScope, isolationScope); + + expect(event.server_name).toEqual('foo'); + }); + + test('adds server name to event when value given in env', () => { + const options = getDefaultNodeClientOptions({}); + process.env.SENTRY_NAME = 'foo'; + const client = new NodeClient(options); + + const event: Event = {}; + const hint: EventHint = {}; + client['_prepareEvent'](event, hint, currentScope, isolationScope); + + expect(event.server_name).toEqual('foo'); + + delete process.env.SENTRY_NAME; + }); + + test('adds hostname as event server name when no value given', () => { + const options = getDefaultNodeClientOptions({}); + const client = new NodeClient(options); + + const event: Event = {}; + const hint: EventHint = {}; + client['_prepareEvent'](event, hint, currentScope, isolationScope); + + expect(event.server_name).toEqual(os.hostname()); + }); + + test('does not add hostname when includeServerName = false', () => { + const options = getDefaultNodeClientOptions({}); + options.includeServerName = false; + const client = new NodeClient(options); + + const event: Event = {}; + const hint: EventHint = {}; + client['_prepareEvent'](event, hint, currentScope, isolationScope); + + expect(event.server_name).toBeUndefined(); + }); + + test("doesn't clobber existing runtime data", () => { + const options = getDefaultNodeClientOptions({ serverName: 'bar' }); + const client = new NodeClient(options); + + const event: Event = { contexts: { runtime: { name: 'foo', version: '1.2.3' } } }; + const hint: EventHint = {}; + client['_prepareEvent'](event, hint, currentScope, isolationScope); + + expect(event.contexts?.runtime).toEqual({ name: 'foo', version: '1.2.3' }); + expect(event.contexts?.runtime).not.toEqual({ name: 'node', version: process.version }); + }); + + test("doesn't clobber existing server name", () => { + const options = getDefaultNodeClientOptions({ serverName: 'bar' }); + const client = new NodeClient(options); + + const event: Event = { server_name: 'foo' }; + const hint: EventHint = {}; + client['_prepareEvent'](event, hint, currentScope, isolationScope); + + expect(event.server_name).toEqual('foo'); + expect(event.server_name).not.toEqual('bar'); + }); + }); + + describe('captureCheckIn', () => { + it('sends a checkIn envelope', () => { + const options = getDefaultNodeClientOptions({ + serverName: 'bar', + release: '1.0.0', + environment: 'dev', + }); + const client = new NodeClient(options); + + const sendEnvelopeSpy = vi.spyOn(client, 'sendEnvelope'); + + const id = client.captureCheckIn( + { monitorSlug: 'foo', status: 'in_progress' }, + { + schedule: { + type: 'crontab', + value: '0 * * * *', + }, + checkinMargin: 2, + maxRuntime: 12333, + timezone: 'Canada/Eastern', + }, + ); + + expect(sendEnvelopeSpy).toHaveBeenCalledTimes(1); + expect(sendEnvelopeSpy).toHaveBeenCalledWith([ + expect.any(Object), + [ + [ + expect.any(Object), + { + check_in_id: id, + monitor_slug: 'foo', + status: 'in_progress', + release: '1.0.0', + environment: 'dev', + monitor_config: { + schedule: { + type: 'crontab', + value: '0 * * * *', + }, + checkin_margin: 2, + max_runtime: 12333, + timezone: 'Canada/Eastern', + }, + }, + ], + ], + ]); + + client.captureCheckIn({ monitorSlug: 'foo', status: 'ok', duration: 1222, checkInId: id }); + + expect(sendEnvelopeSpy).toHaveBeenCalledTimes(2); + expect(sendEnvelopeSpy).toHaveBeenCalledWith([ + expect.any(Object), + [ + [ + expect.any(Object), + { + check_in_id: id, + monitor_slug: 'foo', + duration: 1222, + status: 'ok', + release: '1.0.0', + environment: 'dev', + }, + ], + ], + ]); + }); + + it('sends a checkIn envelope for heartbeat checkIns', () => { + const options = getDefaultNodeClientOptions({ + serverName: 'server', + release: '1.0.0', + environment: 'dev', + }); + const client = new NodeClient(options); + + const sendEnvelopeSpy = vi.spyOn(client, 'sendEnvelope'); + + const id = client.captureCheckIn({ monitorSlug: 'heartbeat-monitor', status: 'ok' }); + + expect(sendEnvelopeSpy).toHaveBeenCalledTimes(1); + expect(sendEnvelopeSpy).toHaveBeenCalledWith([ + expect.any(Object), + [ + [ + expect.any(Object), + { + check_in_id: id, + monitor_slug: 'heartbeat-monitor', + status: 'ok', + release: '1.0.0', + environment: 'dev', + }, + ], + ], + ]); + }); + + it('does not send a checkIn envelope if disabled', () => { + const options = getDefaultNodeClientOptions({ serverName: 'bar', enabled: false }); + const client = new NodeClient(options); + + const sendEnvelopeSpy = vi.spyOn(client, 'sendEnvelope'); + + client.captureCheckIn({ monitorSlug: 'foo', status: 'in_progress' }); + + expect(sendEnvelopeSpy).toHaveBeenCalledTimes(0); + }); + }); + + it('registers instrumentations provided with `openTelemetryInstrumentations`', () => { + const registerInstrumentationsSpy = vi + .spyOn(opentelemetryInstrumentationPackage, 'registerInstrumentations') + .mockImplementationOnce(() => () => undefined); + const instrumentationsArray = ['foobar'] as unknown as opentelemetryInstrumentationPackage.Instrumentation[]; + + new NodeClient(getDefaultNodeClientOptions({ openTelemetryInstrumentations: instrumentationsArray })); + + expect(registerInstrumentationsSpy).toHaveBeenCalledWith( + expect.objectContaining({ + instrumentations: instrumentationsArray, + }), + ); + }); + + describe('log capture', () => { + it('adds server name to log attributes', () => { + const options = getDefaultNodeClientOptions({ _experiments: { enableLogs: true } }); + const client = new NodeClient(options); + + const log: Log = { level: 'info', message: 'test message', attributes: {} }; + client.emit('beforeCaptureLog', log); + + expect(log.attributes).toEqual({ + 'server.address': expect.any(String), + }); + }); + + it('preserves existing log attributes', () => { + const serverName = 'test-server'; + const options = getDefaultNodeClientOptions({ serverName, _experiments: { enableLogs: true } }); + const client = new NodeClient(options); + + const log: Log = { level: 'info', message: 'test message', attributes: { 'existing.attr': 'value' } }; + client.emit('beforeCaptureLog', log); + + expect(log.attributes).toEqual({ + 'existing.attr': 'value', + 'server.address': serverName, + }); + }); + }); +}); diff --git a/packages/node-core/test/sdk/init.test.ts b/packages/node-core/test/sdk/init.test.ts new file mode 100644 index 000000000000..dc523a843b92 --- /dev/null +++ b/packages/node-core/test/sdk/init.test.ts @@ -0,0 +1,269 @@ +import type { Integration } from '@sentry/core'; +import { logger } from '@sentry/core'; +import * as SentryOpentelemetry from '@sentry/opentelemetry'; +import { type Mock, afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import { getClient } from '../../src/'; +import { init, validateOpenTelemetrySetup } from '../../src/sdk'; +import { NodeClient } from '../../src/sdk/client'; +import { cleanupOtel } from '../helpers/mockSdkInit'; + +// eslint-disable-next-line no-var +declare var global: any; + +const PUBLIC_DSN = 'https://username@domain/123'; + +class MockIntegration implements Integration { + public name: string; + public setupOnce: Mock = vi.fn(); + public constructor(name: string) { + this.name = name; + } +} + +describe('init()', () => { + beforeEach(() => { + global.__SENTRY__ = {}; + }); + + afterEach(() => { + cleanupOtel(); + + vi.clearAllMocks(); + }); + + describe('integrations', () => { + it("doesn't install default integrations if told not to", () => { + init({ dsn: PUBLIC_DSN, defaultIntegrations: false }); + + const client = getClient(); + + expect(client?.getOptions()).toEqual( + expect.objectContaining({ + integrations: [], + }), + ); + }); + + it('installs merged default integrations, with overrides provided through options', () => { + const mockDefaultIntegrations = [ + new MockIntegration('Some mock integration 2.1'), + new MockIntegration('Some mock integration 2.2'), + ]; + + const mockIntegrations = [ + new MockIntegration('Some mock integration 2.1'), + new MockIntegration('Some mock integration 2.3'), + ]; + + init({ dsn: PUBLIC_DSN, integrations: mockIntegrations, defaultIntegrations: mockDefaultIntegrations }); + + expect(mockDefaultIntegrations[0]?.setupOnce as Mock).toHaveBeenCalledTimes(0); + expect(mockDefaultIntegrations[1]?.setupOnce as Mock).toHaveBeenCalledTimes(1); + expect(mockIntegrations[0]?.setupOnce as Mock).toHaveBeenCalledTimes(1); + expect(mockIntegrations[1]?.setupOnce as Mock).toHaveBeenCalledTimes(1); + }); + + it('installs integrations returned from a callback function', () => { + const mockDefaultIntegrations = [ + new MockIntegration('Some mock integration 3.1'), + new MockIntegration('Some mock integration 3.2'), + ]; + + const newIntegration = new MockIntegration('Some mock integration 3.3'); + + init({ + dsn: PUBLIC_DSN, + defaultIntegrations: mockDefaultIntegrations, + integrations: integrations => { + const newIntegrations = [...integrations]; + newIntegrations[1] = newIntegration; + return newIntegrations; + }, + }); + + expect(mockDefaultIntegrations[0]?.setupOnce as Mock).toHaveBeenCalledTimes(1); + expect(mockDefaultIntegrations[1]?.setupOnce as Mock).toHaveBeenCalledTimes(0); + expect(newIntegration.setupOnce as Mock).toHaveBeenCalledTimes(1); + }); + }); + + it('returns initialized client', () => { + const client = init({ dsn: PUBLIC_DSN, skipOpenTelemetrySetup: true }); + + expect(client).toBeInstanceOf(NodeClient); + }); + + describe('environment variable options', () => { + const originalProcessEnv = { ...process.env }; + + afterEach(() => { + process.env = originalProcessEnv; + global.__SENTRY__ = {}; + cleanupOtel(); + vi.clearAllMocks(); + }); + + it('sets debug from `SENTRY_DEBUG` env variable', () => { + process.env.SENTRY_DEBUG = '1'; + + const client = init({ dsn: PUBLIC_DSN }); + + expect(client?.getOptions()).toEqual( + expect.objectContaining({ + debug: true, + }), + ); + }); + + it('prefers `debug` option over `SENTRY_DEBUG` env variable', () => { + process.env.SENTRY_DEBUG = '1'; + + const client = init({ dsn: PUBLIC_DSN, debug: false }); + + expect(client?.getOptions()).toEqual( + expect.objectContaining({ + debug: false, + }), + ); + }); + + it('sets tracesSampleRate from `SENTRY_TRACES_SAMPLE_RATE` env variable', () => { + process.env.SENTRY_TRACES_SAMPLE_RATE = '0.5'; + + const client = init({ dsn: PUBLIC_DSN }); + + expect(client?.getOptions()).toEqual( + expect.objectContaining({ + tracesSampleRate: 0.5, + }), + ); + }); + + it('prefers `tracesSampleRate` option over `SENTRY_TRACES_SAMPLE_RATE` env variable', () => { + process.env.SENTRY_TRACES_SAMPLE_RATE = '0.5'; + + const client = init({ dsn: PUBLIC_DSN, tracesSampleRate: 0.1 }); + + expect(client?.getOptions()).toEqual( + expect.objectContaining({ + tracesSampleRate: 0.1, + }), + ); + }); + + it('sets release from `SENTRY_RELEASE` env variable', () => { + process.env.SENTRY_RELEASE = '1.0.0'; + + const client = init({ dsn: PUBLIC_DSN }); + + expect(client?.getOptions()).toEqual( + expect.objectContaining({ + release: '1.0.0', + }), + ); + }); + + it('prefers `release` option over `SENTRY_RELEASE` env variable', () => { + process.env.SENTRY_RELEASE = '1.0.0'; + + const client = init({ dsn: PUBLIC_DSN, release: '2.0.0' }); + + expect(client?.getOptions()).toEqual( + expect.objectContaining({ + release: '2.0.0', + }), + ); + }); + + it('sets environment from `SENTRY_ENVIRONMENT` env variable', () => { + process.env.SENTRY_ENVIRONMENT = 'production'; + + const client = init({ dsn: PUBLIC_DSN }); + + expect(client?.getOptions()).toEqual( + expect.objectContaining({ + environment: 'production', + }), + ); + }); + + it('prefers `environment` option over `SENTRY_ENVIRONMENT` env variable', () => { + process.env.SENTRY_ENVIRONMENT = 'production'; + + const client = init({ dsn: PUBLIC_DSN, environment: 'staging' }); + + expect(client?.getOptions()).toEqual( + expect.objectContaining({ + environment: 'staging', + }), + ); + }); + }); +}); + +describe('validateOpenTelemetrySetup', () => { + afterEach(() => { + global.__SENTRY__ = {}; + cleanupOtel(); + vi.clearAllMocks(); + }); + + it('works with correct setup', () => { + const errorSpy = vi.spyOn(logger, 'error').mockImplementation(() => {}); + const warnSpy = vi.spyOn(logger, 'warn').mockImplementation(() => {}); + + vi.spyOn(SentryOpentelemetry, 'openTelemetrySetupCheck').mockImplementation(() => { + return ['SentryContextManager', 'SentryPropagator', 'SentrySampler']; + }); + + validateOpenTelemetrySetup(); + + expect(errorSpy).toHaveBeenCalledTimes(0); + expect(warnSpy).toHaveBeenCalledTimes(0); + }); + + it('works with missing setup, without tracing', () => { + const errorSpy = vi.spyOn(logger, 'error').mockImplementation(() => {}); + const warnSpy = vi.spyOn(logger, 'warn').mockImplementation(() => {}); + + vi.spyOn(SentryOpentelemetry, 'openTelemetrySetupCheck').mockImplementation(() => { + return []; + }); + + validateOpenTelemetrySetup(); + + // Without tracing, this is expected only twice + expect(errorSpy).toHaveBeenCalledTimes(2); + expect(warnSpy).toHaveBeenCalledTimes(1); + + expect(errorSpy).toBeCalledWith(expect.stringContaining('You have to set up the SentryContextManager.')); + expect(errorSpy).toBeCalledWith(expect.stringContaining('You have to set up the SentryPropagator.')); + expect(warnSpy).toBeCalledWith(expect.stringContaining('You have to set up the SentrySampler.')); + }); + + it('works with missing setup, with tracing', () => { + const errorSpy = vi.spyOn(logger, 'error').mockImplementation(() => {}); + const warnSpy = vi.spyOn(logger, 'warn').mockImplementation(() => {}); + + vi.spyOn(SentryOpentelemetry, 'openTelemetrySetupCheck').mockImplementation(() => { + return []; + }); + + init({ dsn: PUBLIC_DSN, skipOpenTelemetrySetup: true, tracesSampleRate: 1 }); + + validateOpenTelemetrySetup(); + + expect(errorSpy).toHaveBeenCalledTimes(3); + expect(warnSpy).toHaveBeenCalledTimes(1); + + expect(errorSpy).toBeCalledWith(expect.stringContaining('You have to set up the SentryContextManager.')); + expect(errorSpy).toBeCalledWith(expect.stringContaining('You have to set up the SentryPropagator.')); + expect(errorSpy).toBeCalledWith(expect.stringContaining('You have to set up the SentrySpanProcessor.')); + expect(warnSpy).toBeCalledWith(expect.stringContaining('You have to set up the SentrySampler.')); + }); + + // Regression test for https://github.com/getsentry/sentry-javascript/issues/15558 + it('accepts an undefined transport', () => { + init({ dsn: PUBLIC_DSN, transport: undefined }); + }); +}); diff --git a/packages/node/test/transports/http.test.ts b/packages/node-core/test/transports/http.test.ts similarity index 100% rename from packages/node/test/transports/http.test.ts rename to packages/node-core/test/transports/http.test.ts diff --git a/packages/node/test/transports/https.test.ts b/packages/node-core/test/transports/https.test.ts similarity index 100% rename from packages/node/test/transports/https.test.ts rename to packages/node-core/test/transports/https.test.ts diff --git a/packages/node-core/test/transports/test-server-certs.ts b/packages/node-core/test/transports/test-server-certs.ts new file mode 100644 index 000000000000..a5ce436c4234 --- /dev/null +++ b/packages/node-core/test/transports/test-server-certs.ts @@ -0,0 +1,48 @@ +export default { + key: `-----BEGIN RSA PRIVATE KEY----- +MIIEowIBAAKCAQEAuMunjXC2tu2d4x8vKuPQbHwPjYG6pVvAUs7wzpDnMEGo3o2A +bZpL7vUAkQWZ86M84rX9b65cVvT35uqM9uxnJKQhSdGARxEcrz9yxjc9RaIO9xM4 +6WdFd6pcVHW9MF6njnc19jyIoSGXRADJjreNZHyMobAHyL2ZbFiptknUWFW3YT4t +q9bQD5yfhZ94fRt1IbdBAn5Bmz6x61BYudWU2KA3G1akPUmzj0OwZwaIrnGbfLUH +M5F50dNUYfCdmxtE8YRBPyWwcg+KOWa/P8C84p1UQ+/0GHNqUTa4wXBgKeUXNjth +AhV/4JgDDdec+/W0Z1UdEqxZvKfAYnjveFpxEwIDAQABAoIBADLsjEPB59gJKxVH +pqvfE7SRi4enVFP1MM6hEGMcM1ls/qg1vkp11q8G/Rz5ui8VsNWY6To5hmDAKQCN +akMxaksCn9nDzeHHqWvxxCMzXcMuoYkc1vYa613KqJ7twzDtJKdx2oD8tXoR06l9 +vg2CL4idefOkmsCK3xioZjxBpC6jF6ybvlY241MGhaAGRHmP6ik1uFJ+6Y8smh6R +AQKO0u0oQPy6bka9F6DTP6BMUeZ+OA/oOrrb5FxTHu8AHcyCSk2wHnCkB9EF/Ou2 +xSWrnu0O0/0Px6OO9oEsNSq2/fKNV9iuEU8LeAoDVm4ysyMrPce2c4ZsB4U244bj +yQpQZ6ECgYEA9KwA7Lmyf+eeZHxEM4MNSqyeXBtSKu4Zyk0RRY1j69ConjHKet3Q +ylVedXQ0/FJAHHKEm4zFGZtnaaxrzCIcQSKJBCoaA+cN44MM3D1nKmHjgPy8R/yE +BNgIVwJB1MmVSGa+NYnQgUomcCIEr/guNMIxV7p2iybqoxaEHKLfGFUCgYEAwVn1 +8LARsZihLUdxxbAc9+v/pBeMTrkTw1eN1ki9VWYoRam2MLozehEzabt677cU4h7+ +bjdKCKo1x2liY9zmbIiVHssv9Jf3E9XhcajsXB42m1+kjUYVPh8o9lDXcatV9EKt +DZK8wfRY9boyDKB2zRyo6bvIEK3qWbas31W3a8cCgYA6w0TFliPkzEAiaiYHKSZ8 +FNFD1dv6K41OJQxM5BRngom81MCImdWXgsFY/DvtjeOP8YEfysNbzxMbMioBsP+Q +NTcrJOFypn+TcNoZ2zV33GLDi++8ak1azHfUTdp5vKB57xMn0J2fL6vjqoftq3GN +gkZPh50I9qPL35CDQCrMsQKBgC6tFfc1uf/Cld5FagzMOCINodguKxvyB/hXUZFS +XAqar8wpbScUPEsSjfPPY50s+GiiDM/0nvW6iWMLaMos0J+Q1VbqvDfy2525O0Ri +ADU4wfv+Oc41BfnKMexMlcYGE6j006v8KX81Cqi/e0ebETLw4UITp/eG1JU1yUPd +AHuPAoGBAL25v4/onoH0FBLdEwb2BAENxc+0g4In1T+83jfHbfD0gOF3XTbgH4FF +MduIG8qBoZC5whiZ3qH7YJK7sydaM1bDwiesqIik+gEUE65T7S2ZF84y5GC5JjTf +z6v6i+DMCIJXDY5/gjzOED6UllV2Jrn2pDoV++zVyR6KAwXpCmK6 +-----END RSA PRIVATE KEY-----`, + cert: `-----BEGIN CERTIFICATE----- +MIIDETCCAfkCFCMI53aBdS2kWTrw39Kkv93ErG3iMA0GCSqGSIb3DQEBCwUAMEUx +CzAJBgNVBAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRl +cm5ldCBXaWRnaXRzIFB0eSBMdGQwHhcNMjIwMzI4MDgzODQwWhcNNDkwODEyMDgz +ODQwWjBFMQswCQYDVQQGEwJBVTETMBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UE +CgwYSW50ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMIIBIjANBgkqhkiG9w0BAQEFAAOC +AQ8AMIIBCgKCAQEAuMunjXC2tu2d4x8vKuPQbHwPjYG6pVvAUs7wzpDnMEGo3o2A +bZpL7vUAkQWZ86M84rX9b65cVvT35uqM9uxnJKQhSdGARxEcrz9yxjc9RaIO9xM4 +6WdFd6pcVHW9MF6njnc19jyIoSGXRADJjreNZHyMobAHyL2ZbFiptknUWFW3YT4t +q9bQD5yfhZ94fRt1IbdBAn5Bmz6x61BYudWU2KA3G1akPUmzj0OwZwaIrnGbfLUH +M5F50dNUYfCdmxtE8YRBPyWwcg+KOWa/P8C84p1UQ+/0GHNqUTa4wXBgKeUXNjth +AhV/4JgDDdec+/W0Z1UdEqxZvKfAYnjveFpxEwIDAQABMA0GCSqGSIb3DQEBCwUA +A4IBAQBh4BKiByhyvAc5uHj5bkSqspY2xZWW8xiEGaCaQWDMlyjP9mVVWFHfE3XL +lzsJdZVnHDZUliuA5L+qTEpLJ5GmgDWqnKp3HdhtkL16mPbPyJLPY0X+m7wvoZRt +RwLfFCx1E13m0ktYWWgmSCnBl+rI7pyagDhZ2feyxsMrecCazyG/llFBuyWSOnIi +OHxjdHV7be5c8uOOp1iNB9j++LW1pRVrSCWOKRLcsUBal73FW+UvhM5+1If/F9pF +GNQrMhVRA8aHD0JAu3tpjYRKRuOpAbbqtiAUSbDPsJBQy/K9no2K83G7+AV+aGai +HXfQqFFJS6xGKU79azH51wLVEGXq +-----END CERTIFICATE-----`, +}; diff --git a/packages/node-core/test/tsconfig.json b/packages/node-core/test/tsconfig.json new file mode 100644 index 000000000000..38ca0b13bcdd --- /dev/null +++ b/packages/node-core/test/tsconfig.json @@ -0,0 +1,3 @@ +{ + "extends": "../tsconfig.test.json" +} diff --git a/packages/node/test/utils/ensureIsWrapped.test.ts b/packages/node-core/test/utils/ensureIsWrapped.test.ts similarity index 100% rename from packages/node/test/utils/ensureIsWrapped.test.ts rename to packages/node-core/test/utils/ensureIsWrapped.test.ts diff --git a/packages/node/test/utils/entry-point.test.ts b/packages/node-core/test/utils/entry-point.test.ts similarity index 100% rename from packages/node/test/utils/entry-point.test.ts rename to packages/node-core/test/utils/entry-point.test.ts diff --git a/packages/node/test/utils/envToBool.test.ts b/packages/node-core/test/utils/envToBool.test.ts similarity index 100% rename from packages/node/test/utils/envToBool.test.ts rename to packages/node-core/test/utils/envToBool.test.ts diff --git a/packages/node/test/utils/getRequestUrl.test.ts b/packages/node-core/test/utils/getRequestUrl.test.ts similarity index 100% rename from packages/node/test/utils/getRequestUrl.test.ts rename to packages/node-core/test/utils/getRequestUrl.test.ts diff --git a/packages/node/test/utils/instrument.test.ts b/packages/node-core/test/utils/instrument.test.ts similarity index 100% rename from packages/node/test/utils/instrument.test.ts rename to packages/node-core/test/utils/instrument.test.ts diff --git a/packages/node-core/test/utils/module.test.ts b/packages/node-core/test/utils/module.test.ts new file mode 100644 index 000000000000..73404c37673e --- /dev/null +++ b/packages/node-core/test/utils/module.test.ts @@ -0,0 +1,47 @@ +import { describe, expect, it } from 'vitest'; +import { createGetModuleFromFilename } from '../../src'; + +describe('createGetModuleFromFilename', () => { + it.each([ + ['/path/to/file.js', 'file'], + ['/path/to/file.mjs', 'file'], + ['/path/to/file.cjs', 'file'], + ['file.js', 'file'], + ])('returns the module name from a filename %s', (filename, expected) => { + const getModule = createGetModuleFromFilename(); + expect(getModule(filename)).toBe(expected); + }); + + it('applies the given base path', () => { + const getModule = createGetModuleFromFilename('/path/to/base'); + expect(getModule('/path/to/base/file.js')).toBe('file'); + }); + + it('decodes URI-encoded file names', () => { + const getModule = createGetModuleFromFilename(); + expect(getModule('/path%20with%space/file%20with%20spaces(1).js')).toBe('file with spaces(1)'); + }); + + it('returns undefined if no filename is provided', () => { + const getModule = createGetModuleFromFilename(); + expect(getModule(undefined)).toBeUndefined(); + }); + + it.each([ + ['/path/to/base/node_modules/@sentry/test/file.js', '@sentry.test:file'], + ['/path/to/base/node_modules/somePkg/file.js', 'somePkg:file'], + ])('handles node_modules file paths %s', (filename, expected) => { + const getModule = createGetModuleFromFilename(); + expect(getModule(filename)).toBe(expected); + }); + + it('handles windows paths with passed basePath and node_modules', () => { + const getModule = createGetModuleFromFilename('C:\\path\\to\\base', true); + expect(getModule('C:\\path\\to\\base\\node_modules\\somePkg\\file.js')).toBe('somePkg:file'); + }); + + it('handles windows paths with default basePath', () => { + const getModule = createGetModuleFromFilename(undefined, true); + expect(getModule('C:\\path\\to\\base\\somePkg\\file.js')).toBe('file'); + }); +}); diff --git a/packages/node-core/tsconfig.json b/packages/node-core/tsconfig.json new file mode 100644 index 000000000000..b9683a850600 --- /dev/null +++ b/packages/node-core/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "../../tsconfig.json", + + "include": ["src/**/*"], + + "compilerOptions": { + "lib": ["es2018", "es2020.string"], + "module": "Node16" + } +} diff --git a/packages/node-core/tsconfig.test.json b/packages/node-core/tsconfig.test.json new file mode 100644 index 000000000000..3f2ffb86f0f7 --- /dev/null +++ b/packages/node-core/tsconfig.test.json @@ -0,0 +1,12 @@ +{ + "extends": "./tsconfig.json", + + "include": ["test/**/*", "./src/integrations/diagnostic_channel.d.ts", "vite.config.ts"], + + "compilerOptions": { + // should include all types from `./tsconfig.json` plus types for all test frameworks used + "types": ["node"] + + // other package-specific, test-specific options + } +} diff --git a/packages/node-core/tsconfig.types.json b/packages/node-core/tsconfig.types.json new file mode 100644 index 000000000000..65455f66bd75 --- /dev/null +++ b/packages/node-core/tsconfig.types.json @@ -0,0 +1,10 @@ +{ + "extends": "./tsconfig.json", + + "compilerOptions": { + "declaration": true, + "declarationMap": true, + "emitDeclarationOnly": true, + "outDir": "build/types" + } +} diff --git a/packages/node-core/vite.config.ts b/packages/node-core/vite.config.ts new file mode 100644 index 000000000000..f18ec92095bc --- /dev/null +++ b/packages/node-core/vite.config.ts @@ -0,0 +1,8 @@ +import baseConfig from '../../vite/vite.config'; + +export default { + ...baseConfig, + test: { + ...baseConfig.test, + }, +}; diff --git a/packages/node/package.json b/packages/node/package.json index d7519d947bf3..c04ae2b758f8 100644 --- a/packages/node/package.json +++ b/packages/node/package.json @@ -96,6 +96,7 @@ "@opentelemetry/semantic-conventions": "^1.34.0", "@prisma/instrumentation": "6.10.1", "@sentry/core": "9.35.0", + "@sentry/node-core": "9.35.0", "@sentry/opentelemetry": "9.35.0", "import-in-the-middle": "^1.14.2", "minimatch": "^9.0.0" diff --git a/packages/node/rollup.npm.config.mjs b/packages/node/rollup.npm.config.mjs index e0483c673d1c..93fd1d8c16ca 100644 --- a/packages/node/rollup.npm.config.mjs +++ b/packages/node/rollup.npm.config.mjs @@ -1,22 +1,7 @@ -import replace from '@rollup/plugin-replace'; import { makeBaseNPMConfig, makeNPMConfigVariants, makeOtelLoaders } from '@sentry-internal/rollup-utils'; -import { createWorkerCodeBuilder } from './rollup.anr-worker.config.mjs'; - -const [anrWorkerConfig, getAnrBase64Code] = createWorkerCodeBuilder( - 'src/integrations/anr/worker.ts', - 'build/esm/integrations/anr', -); - -const [localVariablesWorkerConfig, getLocalVariablesBase64Code] = createWorkerCodeBuilder( - 'src/integrations/local-variables/worker.ts', - 'build/esm/integrations/local-variables', -); export default [ ...makeOtelLoaders('./build', 'otel'), - // The workers needs to be built first since it's their output is copied in the main bundle. - anrWorkerConfig, - localVariablesWorkerConfig, ...makeNPMConfigVariants( makeBaseNPMConfig({ entrypoints: ['src/index.ts', 'src/init.ts', 'src/preload.ts'], @@ -26,17 +11,6 @@ export default [ exports: 'named', preserveModules: true, }, - plugins: [ - replace({ - delimiters: ['###', '###'], - // removes some rollup warnings - preventAssignment: true, - values: { - AnrWorkerScript: () => getAnrBase64Code(), - LocalVariablesWorkerScript: () => getLocalVariablesBase64Code(), - }, - }), - ], }, }), ), diff --git a/packages/node/src/index.ts b/packages/node/src/index.ts index 71970174721c..7173b971f6d5 100644 --- a/packages/node/src/index.ts +++ b/packages/node/src/index.ts @@ -1,17 +1,6 @@ -import * as logger from './logs/exports'; - export { httpIntegration } from './integrations/http'; export { nativeNodeFetchIntegration } from './integrations/node-fetch'; export { fsIntegration } from './integrations/fs'; - -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, disableAnrDetectionForCallback } from './integrations/anr'; - export { expressIntegration, expressErrorHandler, setupExpressErrorHandler } from './integrations/tracing/express'; export { fastifyIntegration, setupFastifyErrorHandler } from './integrations/tracing/fastify'; export { graphqlIntegration } from './integrations/tracing/graphql'; @@ -28,15 +17,12 @@ export { prismaIntegration } from './integrations/tracing/prisma'; export { hapiIntegration, setupHapiErrorHandler } from './integrations/tracing/hapi'; export { koaIntegration, setupKoaErrorHandler } from './integrations/tracing/koa'; export { connectIntegration, setupConnectErrorHandler } from './integrations/tracing/connect'; -export { spotlightIntegration } from './integrations/spotlight'; export { knexIntegration } from './integrations/tracing/knex'; export { tediousIntegration } from './integrations/tracing/tedious'; export { genericPoolIntegration } from './integrations/tracing/genericPool'; export { dataloaderIntegration } from './integrations/tracing/dataloader'; export { amqplibIntegration } from './integrations/tracing/amqplib'; export { vercelAIIntegration } from './integrations/tracing/vercelai'; -export { childProcessIntegration } from './integrations/childProcess'; -export { createSentryWinstonTransport } from './integrations/winston'; export { launchDarklyIntegration, buildLaunchDarklyFlagUsedHandler, @@ -46,24 +32,14 @@ export { unleashIntegration, } from './integrations/featureFlagShims'; -export { SentryContextManager } from './otel/contextManager'; -export { generateInstrumentOnce } from './otel/instrument'; - export { init, getDefaultIntegrations, getDefaultIntegrationsWithoutPerformance, initWithoutDefaultIntegrations, - validateOpenTelemetrySetup, } from './sdk'; export { initOpenTelemetry, preloadOpenTelemetry } 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 { NODE_VERSION } from './nodeVersion'; export type { NodeOptions } from './types'; @@ -171,4 +147,27 @@ export type { FeatureFlagsIntegration, } from '@sentry/core'; -export { logger }; +export { + logger, + nodeContextIntegration, + contextLinesIntegration, + localVariablesIntegration, + modulesIntegration, + onUncaughtExceptionIntegration, + onUnhandledRejectionIntegration, + anrIntegration, + disableAnrDetectionForCallback, + spotlightIntegration, + childProcessIntegration, + createSentryWinstonTransport, + SentryContextManager, + generateInstrumentOnce, + getSentryRelease, + defaultStackParser, + createGetModuleFromFilename, + makeNodeTransport, + NodeClient, + cron, + NODE_VERSION, + validateOpenTelemetrySetup, +} from '@sentry/node-core'; diff --git a/packages/node/src/integrations/fs.ts b/packages/node/src/integrations/fs.ts index 5ceedd571283..2fd05ad0a09d 100644 --- a/packages/node/src/integrations/fs.ts +++ b/packages/node/src/integrations/fs.ts @@ -1,6 +1,6 @@ import { FsInstrumentation } from '@opentelemetry/instrumentation-fs'; import { defineIntegration, SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core'; -import { generateInstrumentOnce } from '../otel/instrument'; +import { generateInstrumentOnce } from '@sentry/node-core'; const INTEGRATION_NAME = 'FileSystem'; diff --git a/packages/node/src/integrations/http/index.ts b/packages/node/src/integrations/http/index.ts index 96e9b84315be..e46e830f9d16 100644 --- a/packages/node/src/integrations/http/index.ts +++ b/packages/node/src/integrations/http/index.ts @@ -4,15 +4,16 @@ import type { HttpInstrumentationConfig } from '@opentelemetry/instrumentation-h import { HttpInstrumentation } from '@opentelemetry/instrumentation-http'; import type { Span } from '@sentry/core'; import { defineIntegration, getClient, hasSpansEnabled } from '@sentry/core'; -import { NODE_VERSION } from '../../nodeVersion'; -import { generateInstrumentOnce } from '../../otel/instrument'; -import type { NodeClient } from '../../sdk/client'; -import type { HTTPModuleRequestIncomingMessage } from '../../transports/http-module'; +import type { HTTPModuleRequestIncomingMessage, NodeClient } from '@sentry/node-core'; +import { + type SentryHttpInstrumentationOptions, + addOriginToSpan, + generateInstrumentOnce, + getRequestUrl, + NODE_VERSION, + SentryHttpInstrumentation, +} from '@sentry/node-core'; import type { NodeClientOptions } from '../../types'; -import { addOriginToSpan } from '../../utils/addOriginToSpan'; -import { getRequestUrl } from '../../utils/getRequestUrl'; -import type { SentryHttpInstrumentationOptions } from './SentryHttpInstrumentation'; -import { SentryHttpInstrumentation } from './SentryHttpInstrumentation'; const INTEGRATION_NAME = 'Http'; diff --git a/packages/node/src/integrations/node-fetch/index.ts b/packages/node/src/integrations/node-fetch/index.ts index e85ce34dfb35..7929d00c87e0 100644 --- a/packages/node/src/integrations/node-fetch/index.ts +++ b/packages/node/src/integrations/node-fetch/index.ts @@ -2,10 +2,9 @@ import type { UndiciInstrumentationConfig } from '@opentelemetry/instrumentation import { UndiciInstrumentation } from '@opentelemetry/instrumentation-undici'; import type { IntegrationFn } from '@sentry/core'; import { defineIntegration, getClient, hasSpansEnabled, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core'; -import { generateInstrumentOnce } from '../../otel/instrument'; -import type { NodeClient } from '../../sdk/client'; +import type { NodeClient } from '@sentry/node-core'; +import { generateInstrumentOnce, SentryNodeFetchInstrumentation } from '@sentry/node-core'; import type { NodeClientOptions } from '../../types'; -import { SentryNodeFetchInstrumentation } from './SentryNodeFetchInstrumentation'; const INTEGRATION_NAME = 'NodeFetch'; diff --git a/packages/node/src/integrations/tracing/amqplib.ts b/packages/node/src/integrations/tracing/amqplib.ts index 78f5ab627081..bbbdb0b4ae71 100644 --- a/packages/node/src/integrations/tracing/amqplib.ts +++ b/packages/node/src/integrations/tracing/amqplib.ts @@ -2,8 +2,7 @@ import type { Span } from '@opentelemetry/api'; import { type AmqplibInstrumentationConfig, AmqplibInstrumentation } from '@opentelemetry/instrumentation-amqplib'; import type { IntegrationFn } from '@sentry/core'; import { defineIntegration } from '@sentry/core'; -import { generateInstrumentOnce } from '../../otel/instrument'; -import { addOriginToSpan } from '../../utils/addOriginToSpan'; +import { addOriginToSpan, generateInstrumentOnce } from '@sentry/node-core'; const INTEGRATION_NAME = 'Amqplib'; diff --git a/packages/node/src/integrations/tracing/connect.ts b/packages/node/src/integrations/tracing/connect.ts index 80b31f8f1930..c2ab3716bd33 100644 --- a/packages/node/src/integrations/tracing/connect.ts +++ b/packages/node/src/integrations/tracing/connect.ts @@ -8,8 +8,7 @@ import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, spanToJSON, } from '@sentry/core'; -import { generateInstrumentOnce } from '../../otel/instrument'; -import { ensureIsWrapped } from '../../utils/ensureIsWrapped'; +import { ensureIsWrapped, generateInstrumentOnce } from '@sentry/node-core'; type ConnectApp = { // eslint-disable-next-line @typescript-eslint/no-explicit-any diff --git a/packages/node/src/integrations/tracing/dataloader.ts b/packages/node/src/integrations/tracing/dataloader.ts index f1420e74b1f7..4c541d0e9dec 100644 --- a/packages/node/src/integrations/tracing/dataloader.ts +++ b/packages/node/src/integrations/tracing/dataloader.ts @@ -6,7 +6,7 @@ import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, spanToJSON, } from '@sentry/core'; -import { generateInstrumentOnce, instrumentWhenWrapped } from '../../otel/instrument'; +import { generateInstrumentOnce, instrumentWhenWrapped } from '@sentry/node-core'; const INTEGRATION_NAME = 'Dataloader'; diff --git a/packages/node/src/integrations/tracing/express.ts b/packages/node/src/integrations/tracing/express.ts index e13ce6212b79..52dfc373d470 100644 --- a/packages/node/src/integrations/tracing/express.ts +++ b/packages/node/src/integrations/tracing/express.ts @@ -13,10 +13,8 @@ import { SEMANTIC_ATTRIBUTE_SENTRY_OP, spanToJSON, } from '@sentry/core'; +import { addOriginToSpan, ensureIsWrapped, generateInstrumentOnce } from '@sentry/node-core'; import { DEBUG_BUILD } from '../../debug-build'; -import { generateInstrumentOnce } from '../../otel/instrument'; -import { addOriginToSpan } from '../../utils/addOriginToSpan'; -import { ensureIsWrapped } from '../../utils/ensureIsWrapped'; import { ExpressInstrumentationV5 } from './express-v5/instrumentation'; const INTEGRATION_NAME = 'Express'; diff --git a/packages/node/src/integrations/tracing/fastify/index.ts b/packages/node/src/integrations/tracing/fastify/index.ts index b514cb80d32e..b2202a915a47 100644 --- a/packages/node/src/integrations/tracing/fastify/index.ts +++ b/packages/node/src/integrations/tracing/fastify/index.ts @@ -11,8 +11,8 @@ import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, spanToJSON, } from '@sentry/core'; +import { generateInstrumentOnce } from '@sentry/node-core'; import { DEBUG_BUILD } from '../../../debug-build'; -import { generateInstrumentOnce } from '../../../otel/instrument'; import { FastifyOtelInstrumentation } from './fastify-otel/index'; import type { FastifyInstance, FastifyReply, FastifyRequest } from './types'; import { FastifyInstrumentationV3 } from './v3/instrumentation'; diff --git a/packages/node/src/integrations/tracing/genericPool.ts b/packages/node/src/integrations/tracing/genericPool.ts index c5f31d385247..811913f31587 100644 --- a/packages/node/src/integrations/tracing/genericPool.ts +++ b/packages/node/src/integrations/tracing/genericPool.ts @@ -1,7 +1,7 @@ import { GenericPoolInstrumentation } from '@opentelemetry/instrumentation-generic-pool'; import type { IntegrationFn } from '@sentry/core'; import { defineIntegration, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, spanToJSON } from '@sentry/core'; -import { generateInstrumentOnce, instrumentWhenWrapped } from '../../otel/instrument'; +import { generateInstrumentOnce, instrumentWhenWrapped } from '@sentry/node-core'; const INTEGRATION_NAME = 'GenericPool'; diff --git a/packages/node/src/integrations/tracing/graphql.ts b/packages/node/src/integrations/tracing/graphql.ts index 5301ad5180b0..db5760329e8d 100644 --- a/packages/node/src/integrations/tracing/graphql.ts +++ b/packages/node/src/integrations/tracing/graphql.ts @@ -3,9 +3,8 @@ import { SpanStatusCode } from '@opentelemetry/api'; import { GraphQLInstrumentation } from '@opentelemetry/instrumentation-graphql'; import type { IntegrationFn } from '@sentry/core'; import { defineIntegration, getRootSpan, spanToJSON } from '@sentry/core'; +import { addOriginToSpan, generateInstrumentOnce } from '@sentry/node-core'; import { SEMANTIC_ATTRIBUTE_SENTRY_GRAPHQL_OPERATION } from '@sentry/opentelemetry'; -import { generateInstrumentOnce } from '../../otel/instrument'; -import { addOriginToSpan } from '../../utils/addOriginToSpan'; interface GraphqlOptions { /** diff --git a/packages/node/src/integrations/tracing/hapi/index.ts b/packages/node/src/integrations/tracing/hapi/index.ts index 8748840b83a3..f06bfeb18478 100644 --- a/packages/node/src/integrations/tracing/hapi/index.ts +++ b/packages/node/src/integrations/tracing/hapi/index.ts @@ -12,9 +12,8 @@ import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, spanToJSON, } from '@sentry/core'; +import { ensureIsWrapped, generateInstrumentOnce } from '@sentry/node-core'; import { DEBUG_BUILD } from '../../../debug-build'; -import { generateInstrumentOnce } from '../../../otel/instrument'; -import { ensureIsWrapped } from '../../../utils/ensureIsWrapped'; import type { Request, RequestEvent, Server } from './types'; const INTEGRATION_NAME = 'Hapi'; diff --git a/packages/node/src/integrations/tracing/kafka.ts b/packages/node/src/integrations/tracing/kafka.ts index 37d976f5d4e1..94787d92b5b1 100644 --- a/packages/node/src/integrations/tracing/kafka.ts +++ b/packages/node/src/integrations/tracing/kafka.ts @@ -1,8 +1,7 @@ import { KafkaJsInstrumentation } from '@opentelemetry/instrumentation-kafkajs'; import type { IntegrationFn } from '@sentry/core'; import { defineIntegration } from '@sentry/core'; -import { generateInstrumentOnce } from '../../otel/instrument'; -import { addOriginToSpan } from '../../utils/addOriginToSpan'; +import { addOriginToSpan, generateInstrumentOnce } from '@sentry/node-core'; const INTEGRATION_NAME = 'Kafka'; diff --git a/packages/node/src/integrations/tracing/knex.ts b/packages/node/src/integrations/tracing/knex.ts index ce3b1e1a5a5f..433e10e7ee70 100644 --- a/packages/node/src/integrations/tracing/knex.ts +++ b/packages/node/src/integrations/tracing/knex.ts @@ -1,7 +1,7 @@ import { KnexInstrumentation } from '@opentelemetry/instrumentation-knex'; import type { IntegrationFn } from '@sentry/core'; import { defineIntegration, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, spanToJSON } from '@sentry/core'; -import { generateInstrumentOnce, instrumentWhenWrapped } from '../../otel/instrument'; +import { generateInstrumentOnce, instrumentWhenWrapped } from '@sentry/node-core'; const INTEGRATION_NAME = 'Knex'; diff --git a/packages/node/src/integrations/tracing/koa.ts b/packages/node/src/integrations/tracing/koa.ts index 43b901afebee..049cc9064f9a 100644 --- a/packages/node/src/integrations/tracing/koa.ts +++ b/packages/node/src/integrations/tracing/koa.ts @@ -11,10 +11,8 @@ import { SEMANTIC_ATTRIBUTE_SENTRY_OP, spanToJSON, } from '@sentry/core'; +import { addOriginToSpan, ensureIsWrapped, generateInstrumentOnce } from '@sentry/node-core'; import { DEBUG_BUILD } from '../../debug-build'; -import { generateInstrumentOnce } from '../../otel/instrument'; -import { addOriginToSpan } from '../../utils/addOriginToSpan'; -import { ensureIsWrapped } from '../../utils/ensureIsWrapped'; interface KoaOptions { /** diff --git a/packages/node/src/integrations/tracing/lrumemoizer.ts b/packages/node/src/integrations/tracing/lrumemoizer.ts index 29c67299c101..134a595495e3 100644 --- a/packages/node/src/integrations/tracing/lrumemoizer.ts +++ b/packages/node/src/integrations/tracing/lrumemoizer.ts @@ -1,7 +1,7 @@ import { LruMemoizerInstrumentation } from '@opentelemetry/instrumentation-lru-memoizer'; import type { IntegrationFn } from '@sentry/core'; import { defineIntegration } from '@sentry/core'; -import { generateInstrumentOnce } from '../../otel/instrument'; +import { generateInstrumentOnce } from '@sentry/node-core'; const INTEGRATION_NAME = 'LruMemoizer'; diff --git a/packages/node/src/integrations/tracing/mongo.ts b/packages/node/src/integrations/tracing/mongo.ts index 7a1072dda187..7cc66c28e22f 100644 --- a/packages/node/src/integrations/tracing/mongo.ts +++ b/packages/node/src/integrations/tracing/mongo.ts @@ -1,8 +1,7 @@ import { MongoDBInstrumentation } from '@opentelemetry/instrumentation-mongodb'; import type { IntegrationFn } from '@sentry/core'; import { defineIntegration } from '@sentry/core'; -import { generateInstrumentOnce } from '../../otel/instrument'; -import { addOriginToSpan } from '../../utils/addOriginToSpan'; +import { addOriginToSpan, generateInstrumentOnce } from '@sentry/node-core'; const INTEGRATION_NAME = 'Mongo'; diff --git a/packages/node/src/integrations/tracing/mongoose.ts b/packages/node/src/integrations/tracing/mongoose.ts index e4ec855a79de..8e81e43b83d7 100644 --- a/packages/node/src/integrations/tracing/mongoose.ts +++ b/packages/node/src/integrations/tracing/mongoose.ts @@ -1,8 +1,7 @@ import { MongooseInstrumentation } from '@opentelemetry/instrumentation-mongoose'; import type { IntegrationFn } from '@sentry/core'; import { defineIntegration } from '@sentry/core'; -import { generateInstrumentOnce } from '../../otel/instrument'; -import { addOriginToSpan } from '../../utils/addOriginToSpan'; +import { addOriginToSpan, generateInstrumentOnce } from '@sentry/node-core'; const INTEGRATION_NAME = 'Mongoose'; diff --git a/packages/node/src/integrations/tracing/mysql.ts b/packages/node/src/integrations/tracing/mysql.ts index 0c2354b3f94a..fbceb4b6cbaf 100644 --- a/packages/node/src/integrations/tracing/mysql.ts +++ b/packages/node/src/integrations/tracing/mysql.ts @@ -1,7 +1,7 @@ import { MySQLInstrumentation } from '@opentelemetry/instrumentation-mysql'; import type { IntegrationFn } from '@sentry/core'; import { defineIntegration } from '@sentry/core'; -import { generateInstrumentOnce } from '../../otel/instrument'; +import { generateInstrumentOnce } from '@sentry/node-core'; const INTEGRATION_NAME = 'Mysql'; diff --git a/packages/node/src/integrations/tracing/mysql2.ts b/packages/node/src/integrations/tracing/mysql2.ts index ccf534402a54..437c582f56b1 100644 --- a/packages/node/src/integrations/tracing/mysql2.ts +++ b/packages/node/src/integrations/tracing/mysql2.ts @@ -1,8 +1,7 @@ import { MySQL2Instrumentation } from '@opentelemetry/instrumentation-mysql2'; import type { IntegrationFn } from '@sentry/core'; import { defineIntegration } from '@sentry/core'; -import { generateInstrumentOnce } from '../../otel/instrument'; -import { addOriginToSpan } from '../../utils/addOriginToSpan'; +import { addOriginToSpan, generateInstrumentOnce } from '@sentry/node-core'; const INTEGRATION_NAME = 'Mysql2'; diff --git a/packages/node/src/integrations/tracing/postgres.ts b/packages/node/src/integrations/tracing/postgres.ts index 46c1c850a8b3..d3b3c0cc0edf 100644 --- a/packages/node/src/integrations/tracing/postgres.ts +++ b/packages/node/src/integrations/tracing/postgres.ts @@ -1,8 +1,7 @@ import { PgInstrumentation } from '@opentelemetry/instrumentation-pg'; import type { IntegrationFn } from '@sentry/core'; import { defineIntegration } from '@sentry/core'; -import { generateInstrumentOnce } from '../../otel/instrument'; -import { addOriginToSpan } from '../../utils/addOriginToSpan'; +import { addOriginToSpan, generateInstrumentOnce } from '@sentry/node-core'; const INTEGRATION_NAME = 'Postgres'; diff --git a/packages/node/src/integrations/tracing/postgresjs.ts b/packages/node/src/integrations/tracing/postgresjs.ts index c5efb7f6bef7..45d581edc45f 100644 --- a/packages/node/src/integrations/tracing/postgresjs.ts +++ b/packages/node/src/integrations/tracing/postgresjs.ts @@ -26,8 +26,7 @@ import { SPAN_STATUS_ERROR, startSpanManual, } from '@sentry/core'; -import { generateInstrumentOnce } from '../../otel/instrument'; -import { addOriginToSpan } from '../../utils/addOriginToSpan'; +import { addOriginToSpan, generateInstrumentOnce } from '@sentry/node-core'; const INTEGRATION_NAME = 'PostgresJs'; const SUPPORTED_VERSIONS = ['>=3.0.0 <4']; diff --git a/packages/node/src/integrations/tracing/prisma.ts b/packages/node/src/integrations/tracing/prisma.ts index c674e0bfdfbe..bc845362b6bb 100644 --- a/packages/node/src/integrations/tracing/prisma.ts +++ b/packages/node/src/integrations/tracing/prisma.ts @@ -1,7 +1,7 @@ import type { Instrumentation } from '@opentelemetry/instrumentation'; import { PrismaInstrumentation } from '@prisma/instrumentation'; import { consoleSandbox, defineIntegration, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, spanToJSON } from '@sentry/core'; -import { generateInstrumentOnce } from '../../otel/instrument'; +import { generateInstrumentOnce } from '@sentry/node-core'; import type { PrismaV5TracingHelper } from './prisma/vendor/v5-tracing-helper'; import type { PrismaV6TracingHelper } from './prisma/vendor/v6-tracing-helper'; diff --git a/packages/node/src/integrations/tracing/redis.ts b/packages/node/src/integrations/tracing/redis.ts index b6ec8a89ec60..66c95705e457 100644 --- a/packages/node/src/integrations/tracing/redis.ts +++ b/packages/node/src/integrations/tracing/redis.ts @@ -13,7 +13,7 @@ import { spanToJSON, truncate, } from '@sentry/core'; -import { generateInstrumentOnce } from '../../otel/instrument'; +import { generateInstrumentOnce } from '@sentry/node-core'; import { calculateCacheItemSize, GET_COMMANDS, diff --git a/packages/node/src/integrations/tracing/tedious.ts b/packages/node/src/integrations/tracing/tedious.ts index 0585ec01a9c5..d9ae7d186410 100644 --- a/packages/node/src/integrations/tracing/tedious.ts +++ b/packages/node/src/integrations/tracing/tedious.ts @@ -1,7 +1,7 @@ import { TediousInstrumentation } from '@opentelemetry/instrumentation-tedious'; import type { IntegrationFn } from '@sentry/core'; import { defineIntegration, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, spanToJSON } from '@sentry/core'; -import { generateInstrumentOnce, instrumentWhenWrapped } from '../../otel/instrument'; +import { generateInstrumentOnce, instrumentWhenWrapped } from '@sentry/node-core'; const TEDIUS_INSTRUMENTED_METHODS = new Set([ 'callProcedure', diff --git a/packages/node/src/integrations/tracing/vercelai/index.ts b/packages/node/src/integrations/tracing/vercelai/index.ts index 9ee5fb29f11d..5588384e22c4 100644 --- a/packages/node/src/integrations/tracing/vercelai/index.ts +++ b/packages/node/src/integrations/tracing/vercelai/index.ts @@ -1,7 +1,6 @@ import type { Client, IntegrationFn } from '@sentry/core'; import { addVercelAiProcessors, defineIntegration } from '@sentry/core'; -import { generateInstrumentOnce } from '../../../otel/instrument'; -import type { modulesIntegration } from '../../modules'; +import { type modulesIntegration, generateInstrumentOnce } from '@sentry/node-core'; import { INTEGRATION_NAME } from './constants'; import { SentryVercelAiInstrumentation } from './instrumentation'; import type { VercelAiOptions } from './types'; diff --git a/packages/node/src/preload.ts b/packages/node/src/preload.ts index 0e4af146197f..615ab0c1a008 100644 --- a/packages/node/src/preload.ts +++ b/packages/node/src/preload.ts @@ -1,5 +1,5 @@ +import { envToBool } from '@sentry/node-core'; import { preloadOpenTelemetry } from './sdk/initOtel'; -import { envToBool } from './utils/envToBool'; const debug = envToBool(process.env.SENTRY_DEBUG); const integrationsStr = process.env.SENTRY_PRELOAD_INTEGRATIONS; diff --git a/packages/node/src/sdk/index.ts b/packages/node/src/sdk/index.ts index e693d3976fe4..7afa959b2ce8 100644 --- a/packages/node/src/sdk/index.ts +++ b/packages/node/src/sdk/index.ts @@ -1,72 +1,27 @@ import type { Integration, Options } from '@sentry/core'; +import { hasSpansEnabled } from '@sentry/core'; +import type { NodeClient } from '@sentry/node-core'; import { - consoleIntegration, - consoleSandbox, - functionToStringIntegration, - getCurrentScope, - getIntegrationsToSetup, - hasSpansEnabled, - inboundFiltersIntegration, - linkedErrorsIntegration, - logger, - propagationContextFromHeaders, - requestDataIntegration, - stackParserFromStackParserOptions, -} from '@sentry/core'; -import { - enhanceDscWithOpenTelemetryRootSpanName, - openTelemetrySetupCheck, - setOpenTelemetryContextAsyncContextStrategy, - setupEventContextTrace, -} from '@sentry/opentelemetry'; -import { DEBUG_BUILD } from '../debug-build'; -import { childProcessIntegration } from '../integrations/childProcess'; -import { nodeContextIntegration } from '../integrations/context'; -import { contextLinesIntegration } from '../integrations/contextlines'; + getDefaultIntegrations as getNodeCoreDefaultIntegrations, + init as initNodeCore, + validateOpenTelemetrySetup, +} from '@sentry/node-core'; import { httpIntegration } from '../integrations/http'; -import { localVariablesIntegration } from '../integrations/local-variables'; -import { modulesIntegration } from '../integrations/modules'; import { nativeNodeFetchIntegration } from '../integrations/node-fetch'; -import { onUncaughtExceptionIntegration } from '../integrations/onuncaughtexception'; -import { onUnhandledRejectionIntegration } from '../integrations/onunhandledrejection'; -import { processSessionIntegration } from '../integrations/processSession'; -import { INTEGRATION_NAME as SPOTLIGHT_INTEGRATION_NAME, spotlightIntegration } from '../integrations/spotlight'; import { getAutoPerformanceIntegrations } from '../integrations/tracing'; -import { makeNodeTransport } from '../transports'; -import type { NodeClientOptions, NodeOptions } from '../types'; -import { isCjs } from '../utils/commonjs'; -import { envToBool } from '../utils/envToBool'; -import { defaultStackParser, getSentryRelease } from './api'; -import { NodeClient } from './client'; -import { initOpenTelemetry, maybeInitializeEsmLoader } from './initOtel'; +import type { NodeOptions } from '../types'; +import { initOpenTelemetry } from './initOtel'; /** * Get default integrations, excluding performance. */ export function getDefaultIntegrationsWithoutPerformance(): Integration[] { - return [ - // Common - // TODO(v10): Replace with `eventFiltersIntegration` once we remove the deprecated `inboundFiltersIntegration` - // eslint-disable-next-line deprecation/deprecation - inboundFiltersIntegration(), - functionToStringIntegration(), - linkedErrorsIntegration(), - requestDataIntegration(), - // Native Wrappers - consoleIntegration(), - httpIntegration(), - nativeNodeFetchIntegration(), - // Global Handlers - onUncaughtExceptionIntegration(), - onUnhandledRejectionIntegration(), - // Event Info - contextLinesIntegration(), - localVariablesIntegration(), - nodeContextIntegration(), - childProcessIntegration(), - processSessionIntegration(), - modulesIntegration(), - ]; + const nodeCoreIntegrations = getNodeCoreDefaultIntegrations(); + + // Filter out the node-core HTTP and NodeFetch integrations and replace them with Node SDK's composite versions + return nodeCoreIntegrations + .filter(integration => integration.name !== 'Http' && integration.name !== 'NodeFetch') + .concat(httpIntegration(), nativeNodeFetchIntegration()); } /** Get the default integrations for the Node SDK. */ @@ -89,180 +44,32 @@ export function init(options: NodeOptions | undefined = {}): NodeClient | undefi } /** - * Initialize Sentry for Node, without any integrations added by default. - */ -export function initWithoutDefaultIntegrations(options: NodeOptions | undefined = {}): NodeClient { - return _init(options, () => []); -} - -/** - * Initialize Sentry for Node, without performance instrumentation. + * Internal initialization function. */ function _init( - _options: NodeOptions | undefined = {}, + options: NodeOptions | undefined = {}, getDefaultIntegrationsImpl: (options: Options) => Integration[], -): NodeClient { - const options = getClientOptions(_options, getDefaultIntegrationsImpl); - - if (options.debug === true) { - if (DEBUG_BUILD) { - logger.enable(); - } else { - // use `console.warn` rather than `logger.warn` since by non-debug bundles have all `logger.x` statements stripped - consoleSandbox(() => { - // eslint-disable-next-line no-console - console.warn('[Sentry] Cannot initialize SDK with `debug` option using a non-debug bundle.'); - }); - } - } - - if (!isCjs() && options.registerEsmLoaderHooks !== false) { - maybeInitializeEsmLoader(); - } - - setOpenTelemetryContextAsyncContextStrategy(); - - const scope = getCurrentScope(); - scope.update(options.initialScope); - - if (options.spotlight && !options.integrations.some(({ name }) => name === SPOTLIGHT_INTEGRATION_NAME)) { - options.integrations.push( - spotlightIntegration({ - sidecarUrl: typeof options.spotlight === 'string' ? options.spotlight : undefined, - }), - ); - } - - const client = new NodeClient(options); - // The client is on the current scope, from where it generally is inherited - getCurrentScope().setClient(client); - - client.init(); - - logger.log(`Running in ${isCjs() ? 'CommonJS' : 'ESM'} mode.`); - - client.startClientReportTracking(); - - updateScopeFromEnvVariables(); +): NodeClient | undefined { + const client = initNodeCore({ + ...options, + // Only use Node SDK defaults if none provided + defaultIntegrations: options.defaultIntegrations ?? getDefaultIntegrationsImpl(options), + }); - // If users opt-out of this, they _have_ to set up OpenTelemetry themselves - // There is no way to use this SDK without OpenTelemetry! - if (!options.skipOpenTelemetrySetup) { + // Add Node SDK specific OpenTelemetry setup + if (client && !options.skipOpenTelemetrySetup) { initOpenTelemetry(client, { spanProcessors: options.openTelemetrySpanProcessors, }); validateOpenTelemetrySetup(); } - enhanceDscWithOpenTelemetryRootSpanName(client); - setupEventContextTrace(client); - return client; } /** - * Validate that your OpenTelemetry setup is correct. - */ -export function validateOpenTelemetrySetup(): void { - if (!DEBUG_BUILD) { - return; - } - - const setup = openTelemetrySetupCheck(); - - const required: ReturnType = ['SentryContextManager', 'SentryPropagator']; - - if (hasSpansEnabled()) { - required.push('SentrySpanProcessor'); - } - - for (const k of required) { - if (!setup.includes(k)) { - logger.error( - `You have to set up the ${k}. Without this, the OpenTelemetry & Sentry integration will not work properly.`, - ); - } - } - - if (!setup.includes('SentrySampler')) { - logger.warn( - 'You have to set up the SentrySampler. Without this, the OpenTelemetry & Sentry integration may still work, but sample rates set for the Sentry SDK will not be respected. If you use a custom sampler, make sure to use `wrapSamplingDecision`.', - ); - } -} - -function getClientOptions( - options: NodeOptions, - getDefaultIntegrationsImpl: (options: Options) => Integration[], -): NodeClientOptions { - const release = getRelease(options.release); - const spotlight = - options.spotlight ?? envToBool(process.env.SENTRY_SPOTLIGHT, { strict: true }) ?? process.env.SENTRY_SPOTLIGHT; - const tracesSampleRate = getTracesSampleRate(options.tracesSampleRate); - - const mergedOptions = { - ...options, - dsn: options.dsn ?? process.env.SENTRY_DSN, - environment: options.environment ?? process.env.SENTRY_ENVIRONMENT, - sendClientReports: options.sendClientReports ?? true, - transport: options.transport ?? makeNodeTransport, - stackParser: stackParserFromStackParserOptions(options.stackParser || defaultStackParser), - release, - tracesSampleRate, - spotlight, - debug: envToBool(options.debug ?? process.env.SENTRY_DEBUG), - }; - - const integrations = options.integrations; - const defaultIntegrations = options.defaultIntegrations ?? getDefaultIntegrationsImpl(mergedOptions); - - return { - ...mergedOptions, - integrations: getIntegrationsToSetup({ - defaultIntegrations, - integrations, - }), - }; -} - -function getRelease(release: NodeOptions['release']): string | undefined { - if (release !== undefined) { - return release; - } - - const detectedRelease = getSentryRelease(); - if (detectedRelease !== undefined) { - return detectedRelease; - } - - return undefined; -} - -function getTracesSampleRate(tracesSampleRate: NodeOptions['tracesSampleRate']): number | undefined { - if (tracesSampleRate !== undefined) { - return tracesSampleRate; - } - - const sampleRateFromEnv = process.env.SENTRY_TRACES_SAMPLE_RATE; - if (!sampleRateFromEnv) { - return undefined; - } - - const parsed = parseFloat(sampleRateFromEnv); - return isFinite(parsed) ? parsed : undefined; -} - -/** - * Update scope and propagation context based on environmental variables. - * - * See https://github.com/getsentry/rfcs/blob/main/text/0071-continue-trace-over-process-boundaries.md - * for more details. + * Initialize Sentry for Node, without any integrations added by default. */ -function updateScopeFromEnvVariables(): void { - if (envToBool(process.env.SENTRY_USE_ENVIRONMENT) !== false) { - const sentryTraceEnv = process.env.SENTRY_TRACE; - const baggageEnv = process.env.SENTRY_BAGGAGE; - const propagationContext = propagationContextFromHeaders(sentryTraceEnv, baggageEnv); - getCurrentScope().setPropagationContext(propagationContext); - } +export function initWithoutDefaultIntegrations(options: NodeOptions | undefined = {}): NodeClient | undefined { + return _init(options, () => []); } diff --git a/packages/node/src/sdk/initOtel.ts b/packages/node/src/sdk/initOtel.ts index 26b9cfa71f72..e6f2b8d15786 100644 --- a/packages/node/src/sdk/initOtel.ts +++ b/packages/node/src/sdk/initOtel.ts @@ -1,4 +1,4 @@ -import { context, diag, DiagLogLevel, propagation, trace } from '@opentelemetry/api'; +import { context, propagation, trace } from '@opentelemetry/api'; import { Resource } from '@opentelemetry/resources'; import type { SpanProcessor } from '@opentelemetry/sdk-trace-base'; import { BasicTracerProvider } from '@opentelemetry/sdk-trace-base'; @@ -8,14 +8,13 @@ import { SEMRESATTRS_SERVICE_NAMESPACE, } from '@opentelemetry/semantic-conventions'; import { consoleSandbox, GLOBAL_OBJ, logger, SDK_VERSION } from '@sentry/core'; +import type { NodeClient } from '@sentry/node-core'; +import { isCjs, SentryContextManager } from '@sentry/node-core'; import { SentryPropagator, SentrySampler, SentrySpanProcessor } from '@sentry/opentelemetry'; import { createAddHookMessageChannel } from 'import-in-the-middle'; import moduleModule from 'module'; import { DEBUG_BUILD } from '../debug-build'; import { getOpenTelemetryInstrumentationToPreload } from '../integrations/tracing'; -import { SentryContextManager } from '../otel/contextManager'; -import { isCjs } from '../utils/commonjs'; -import type { NodeClient } from './client'; // About 277h - this must fit into new Array(len)! const MAX_MAX_SPAN_WAIT_DURATION = 1_000_000; @@ -29,10 +28,6 @@ interface AdditionalOpenTelemetryOptions { * Initialize OpenTelemetry for Node. */ export function initOpenTelemetry(client: NodeClient, options: AdditionalOpenTelemetryOptions = {}): void { - if (client.getOptions().debug) { - setupOpenTelemetryLogger(); - } - const provider = setupOtel(client, options); client.traceProvider = provider; } @@ -80,7 +75,6 @@ export function preloadOpenTelemetry(options: NodePreloadOptions = {}): void { if (debug) { logger.enable(); - setupOpenTelemetryLogger(); } if (!isCjs()) { @@ -154,19 +148,3 @@ export function _clampSpanProcessorTimeout(maxSpanWaitDuration: number | undefin return maxSpanWaitDuration; } - -/** - * Setup the OTEL logger to use our own logger. - */ -function setupOpenTelemetryLogger(): void { - const otelLogger = new Proxy(logger as typeof logger & { verbose: (typeof logger)['debug'] }, { - get(target, prop, receiver) { - const actualProp = prop === 'verbose' ? 'debug' : prop; - return Reflect.get(target, actualProp, receiver); - }, - }); - - // Disable diag, to ensure this works even if called multiple times - diag.disable(); - diag.setLogger(otelLogger, DiagLogLevel.DEBUG); -} diff --git a/packages/node/src/types.ts b/packages/node/src/types.ts index 1a2afabdc1c6..3d3463d0b5cf 100644 --- a/packages/node/src/types.ts +++ b/packages/node/src/types.ts @@ -2,7 +2,7 @@ import type { Span as WriteableSpan } from '@opentelemetry/api'; import type { Instrumentation } from '@opentelemetry/instrumentation'; import type { ReadableSpan, SpanProcessor } from '@opentelemetry/sdk-trace-base'; import type { ClientOptions, Options, SamplingContext, Scope, Span, TracePropagationTargets } from '@sentry/core'; -import type { NodeTransportOptions } from './transports'; +import type { NodeTransportOptions } from '@sentry/node-core'; export interface BaseNodeOptions { /** diff --git a/packages/node/test/integrations/tracing/graphql.test.ts b/packages/node/test/integrations/tracing/graphql.test.ts index 10b8783c3702..925aed91b4ac 100644 --- a/packages/node/test/integrations/tracing/graphql.test.ts +++ b/packages/node/test/integrations/tracing/graphql.test.ts @@ -1,7 +1,7 @@ import { GraphQLInstrumentation } from '@opentelemetry/instrumentation-graphql'; +import { INSTRUMENTED } from '@sentry/node-core'; import { type MockInstance, beforeEach, describe, expect, it, vi } from 'vitest'; import { graphqlIntegration, instrumentGraphql } from '../../../src/integrations/tracing/graphql'; -import { INSTRUMENTED } from '../../../src/otel/instrument'; vi.mock('@opentelemetry/instrumentation-graphql'); diff --git a/packages/node/test/integrations/tracing/koa.test.ts b/packages/node/test/integrations/tracing/koa.test.ts index 9ca221dfba03..39fe81caef90 100644 --- a/packages/node/test/integrations/tracing/koa.test.ts +++ b/packages/node/test/integrations/tracing/koa.test.ts @@ -1,7 +1,7 @@ import { KoaInstrumentation } from '@opentelemetry/instrumentation-koa'; +import { INSTRUMENTED } from '@sentry/node-core'; import { type MockInstance, beforeEach, describe, expect, it, vi } from 'vitest'; import { instrumentKoa, koaIntegration } from '../../../src/integrations/tracing/koa'; -import { INSTRUMENTED } from '../../../src/otel/instrument'; vi.mock('@opentelemetry/instrumentation-koa'); diff --git a/packages/node/test/integrations/tracing/mongo.test.ts b/packages/node/test/integrations/tracing/mongo.test.ts index 34f3048713ff..7710a4ad8721 100644 --- a/packages/node/test/integrations/tracing/mongo.test.ts +++ b/packages/node/test/integrations/tracing/mongo.test.ts @@ -1,11 +1,11 @@ import { MongoDBInstrumentation } from '@opentelemetry/instrumentation-mongodb'; +import { INSTRUMENTED } from '@sentry/node-core'; import { type MockInstance, beforeEach, describe, expect, it, vi } from 'vitest'; import { _defaultDbStatementSerializer, instrumentMongo, mongoIntegration, } from '../../../src/integrations/tracing/mongo'; -import { INSTRUMENTED } from '../../../src/otel/instrument'; vi.mock('@opentelemetry/instrumentation-mongodb'); diff --git a/packages/node/test/sdk/init.test.ts b/packages/node/test/sdk/init.test.ts index 71f0f12e9e30..078f2d1ec44d 100644 --- a/packages/node/test/sdk/init.test.ts +++ b/packages/node/test/sdk/init.test.ts @@ -2,10 +2,9 @@ import type { Integration } from '@sentry/core'; import { logger } from '@sentry/core'; import * as SentryOpentelemetry from '@sentry/opentelemetry'; import { type Mock, type MockInstance, afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; -import { getClient } from '../../src/'; +import { getClient, NodeClient, validateOpenTelemetrySetup } from '../../src/'; import * as auto from '../../src/integrations/tracing'; -import { init, validateOpenTelemetrySetup } from '../../src/sdk'; -import { NodeClient } from '../../src/sdk/client'; +import { init } from '../../src/sdk'; import { cleanupOtel } from '../helpers/mockSdkInit'; // eslint-disable-next-line no-var diff --git a/packages/remix/test/integration/package.json b/packages/remix/test/integration/package.json index 7f4172b0b42f..9aea12728dd0 100644 --- a/packages/remix/test/integration/package.json +++ b/packages/remix/test/integration/package.json @@ -27,6 +27,7 @@ "@sentry/browser": "file:../../../browser", "@sentry/core": "file:../../../core", "@sentry/node": "file:../../../node", + "@sentry/node-core": "file:../../../node-core", "@sentry/opentelemetry": "file:../../../opentelemetry", "@sentry/react": "file:../../../react", "@sentry-internal/browser-utils": "file:../../../browser-utils", From 4a17cd014789d9bc4cc5dda016b1b188650c2dc1 Mon Sep 17 00:00:00 2001 From: Andrei Borza Date: Wed, 2 Jul 2025 23:30:04 +0200 Subject: [PATCH 03/13] Add node-core integration tests --- .../node-core-integration-tests/.eslintrc.js | 40 + .../node-core-integration-tests/.gitignore | 1 + .../node-core-integration-tests/README.md | 66 ++ .../node-core-integration-tests/package.json | 67 ++ .../rollup.npm.config.mjs | 3 + .../scripts/clean.js | 19 + .../scripts/use-ts-3_8.js | 39 + .../node-core-integration-tests/src/index.ts | 57 ++ .../suites/anr/app-path.mjs | 37 + .../suites/anr/basic-multiple.mjs | 37 + .../suites/anr/basic-session.js | 32 + .../suites/anr/basic.js | 34 + .../suites/anr/basic.mjs | 38 + .../suites/anr/forked.js | 33 + .../suites/anr/forker.js | 7 + .../suites/anr/indefinite.mjs | 32 + .../suites/anr/isolated.mjs | 53 ++ .../suites/anr/should-exit-forced.js | 20 + .../suites/anr/should-exit.js | 19 + .../suites/anr/stop-and-start.js | 55 ++ .../suites/anr/test.ts | 253 +++++++ .../suites/breadcrumbs/process-thread/app.mjs | 33 + .../suites/breadcrumbs/process-thread/test.ts | 50 ++ .../breadcrumbs/process-thread/worker.mjs | 1 + .../suites/child-process/child.js | 3 + .../suites/child-process/child.mjs | 3 + .../suites/child-process/fork.js | 20 + .../suites/child-process/fork.mjs | 22 + .../suites/child-process/test.ts | 66 ++ .../suites/child-process/worker.js | 20 + .../suites/child-process/worker.mjs | 22 + .../drop-reasons/before-send/scenario.ts | 26 + .../drop-reasons/before-send/test.ts | 35 + .../drop-reasons/event-processors/scenario.ts | 27 + .../drop-reasons/event-processors/test.ts | 35 + .../client-reports/periodic-send/scenario.ts | 16 + .../client-reports/periodic-send/test.ts | 24 + .../filename-with-spaces/instrument.mjs | 11 + .../scenario with space.cjs | 12 + .../scenario with space.mjs | 5 + .../contextLines/filename-with-spaces/test.ts | 85 +++ .../contextLines/memory-leak/nested-file.ts | 5 + .../contextLines/memory-leak/other-file.ts | 7 + .../contextLines/memory-leak/scenario.ts | 32 + .../suites/contextLines/memory-leak/test.ts | 18 + .../suites/cron/cron/scenario.ts | 33 + .../suites/cron/cron/test.ts | 77 ++ .../suites/cron/node-cron/scenario.ts | 39 + .../suites/cron/node-cron/test.ts | 77 ++ .../suites/cron/node-schedule/scenario.ts | 31 + .../suites/cron/node-schedule/test.ts | 77 ++ .../suites/esm/import-in-the-middle/app.mjs | 24 + .../esm/import-in-the-middle/sub-module.mjs | 2 + .../suites/esm/import-in-the-middle/test.ts | 15 + .../suites/esm/modules-integration/app.mjs | 12 + .../suites/esm/modules-integration/test.ts | 12 + .../suites/esm/warn-esm/server.js | 22 + .../suites/esm/warn-esm/server.mjs | 20 + .../suites/esm/warn-esm/test.ts | 42 ++ .../onError/basic/scenario.ts | 22 + .../onError/basic/test.ts | 31 + .../onError/withScope/scenario.ts | 33 + .../onError/withScope/test.ts | 38 + .../onSpan/scenario.ts | 28 + .../featureFlagsIntegration/onSpan/test.ts | 33 + .../suites/no-code/app.js | 3 + .../suites/no-code/app.mjs | 3 + .../suites/no-code/test.ts | 39 + .../suites/proxy/basic.js | 20 + .../suites/proxy/test.ts | 18 + .../LocalVariables/deny-inspector.mjs | 22 + .../LocalVariables/local-variables-caught.js | 43 ++ .../LocalVariables/local-variables-caught.mjs | 47 ++ .../local-variables-instrument.js | 11 + .../local-variables-no-sentry.js | 31 + .../LocalVariables/local-variables-rethrow.js | 48 ++ .../LocalVariables/local-variables.js | 43 ++ .../LocalVariables/no-local-variables.js | 42 ++ .../suites/public-api/LocalVariables/test.ts | 104 +++ .../additional-listener-test-script.js | 19 + .../log-entire-error-to-console.js | 10 + ...haviour-additional-listener-test-script.js | 24 + ...iour-no-additional-listener-test-script.js | 21 + .../no-additional-listener-test-script.js | 16 + .../public-api/OnUncaughtException/test.ts | 77 ++ .../addBreadcrumb/empty-obj/scenario.ts | 14 + .../addBreadcrumb/empty-obj/test.ts | 17 + .../multiple_breadcrumbs/scenario.ts | 23 + .../multiple_breadcrumbs/test.ts | 27 + .../simple_breadcrumb/scenario.ts | 19 + .../addBreadcrumb/simple_breadcrumb/test.ts | 20 + .../catched-error/scenario.ts | 17 + .../captureException/catched-error/test.ts | 45 ++ .../captureException/empty-obj/scenario.ts | 13 + .../captureException/empty-obj/test.ts | 28 + .../captureException/simple-error/scenario.ts | 13 + .../captureException/simple-error/test.ts | 31 + .../parameterized_message/scenario.ts | 16 + .../parameterized_message/test.ts | 20 + .../captureMessage/simple_message/scenario.ts | 13 + .../captureMessage/simple_message/test.ts | 18 + .../scenario.ts | 14 + .../simple_message_attachStackTrace/test.ts | 27 + .../captureMessage/with_level/scenario.ts | 18 + .../captureMessage/with_level/test.ts | 18 + .../configureScope/clear_scope/scenario.ts | 19 + .../configureScope/clear_scope/test.ts | 17 + .../configureScope/set_properties/scenario.ts | 18 + .../configureScope/set_properties/test.ts | 26 + .../mode-none.js | 16 + .../mode-strict.js | 17 + .../mode-warn-error.js | 15 + .../mode-warn-string.js | 15 + .../scenario-strict.ts | 15 + .../scenario-warn.ts | 14 + .../onUnhandledRejectionIntegration/test.ts | 126 ++++ .../scopes/initialScopes/scenario.ts | 28 + .../public-api/scopes/initialScopes/test.ts | 40 + .../scopes/isolationScope/scenario.ts | 35 + .../public-api/scopes/isolationScope/test.ts | 57 ++ .../setContext/multiple-contexts/scenario.ts | 27 + .../setContext/multiple-contexts/test.ts | 24 + .../non-serializable-context/scenario.ts | 22 + .../non-serializable-context/test.ts | 13 + .../setContext/simple-context/scenario.ts | 14 + .../setContext/simple-context/test.ts | 22 + .../setExtra/multiple-extras/scenario.ts | 22 + .../setExtra/multiple-extras/test.ts | 21 + .../non-serializable-extra/scenario.ts | 22 + .../setExtra/non-serializable-extra/test.ts | 18 + .../setExtra/simple-extra/scenario.ts | 19 + .../public-api/setExtra/simple-extra/test.ts | 25 + .../setExtras/consecutive-calls/scenario.ts | 22 + .../setExtras/consecutive-calls/test.ts | 18 + .../setExtras/multiple-extras/scenario.ts | 24 + .../setExtras/multiple-extras/test.ts | 23 + .../public-api/setMeasurement/scenario.ts | 19 + .../suites/public-api/setMeasurement/test.ts | 22 + .../setTag/with-primitives/scenario.ts | 20 + .../public-api/setTag/with-primitives/test.ts | 24 + .../setTags/with-primitives/scenario.ts | 20 + .../setTags/with-primitives/test.ts | 24 + .../public-api/setUser/unset_user/scenario.ts | 25 + .../public-api/setUser/unset_user/test.ts | 24 + .../setUser/update_user/scenario.ts | 24 + .../public-api/setUser/update_user/test.ts | 29 + .../startSpan/basic-usage/scenario.ts | 14 + .../public-api/startSpan/basic-usage/test.ts | 45 ++ .../startSpan/parallel-root-spans/scenario.ts | 33 + .../startSpan/parallel-root-spans/test.ts | 31 + .../scenario.ts | 35 + .../test.ts | 27 + .../parallel-spans-in-scope/scenario.ts | 29 + .../startSpan/parallel-spans-in-scope/test.ts | 29 + .../startSpan/updateName-method/scenario.ts | 19 + .../startSpan/updateName-method/test.ts | 26 + .../updateSpanName-function/scenario.ts | 19 + .../startSpan/updateSpanName-function/test.ts | 26 + .../startSpan/with-nested-spans/scenario.ts | 36 + .../startSpan/with-nested-spans/test.ts | 47 ++ .../withScope/nested-scopes/scenario.ts | 30 + .../withScope/nested-scopes/test.ts | 59 ++ .../errored-session-aggregate/test.ts | 29 + .../sessions/exited-session-aggregate/test.ts | 28 + .../suites/sessions/server.ts | 51 ++ .../dsc-txn-name-update/scenario-events.ts | 33 + .../dsc-txn-name-update/scenario-headers.ts | 60 ++ .../tracing/dsc-txn-name-update/test.ts | 138 ++++ .../error-active-span-unsampled/scenario.ts | 18 + .../error-active-span-unsampled/test.ts | 22 + .../error-active-span/scenario.ts | 20 + .../envelope-header/error-active-span/test.ts | 23 + .../tracing/envelope-header/error/scenario.ts | 16 + .../tracing/envelope-header/error/test.ts | 18 + .../sampleRate-propagation/server.js | 38 + .../sampleRate-propagation/test.ts | 33 + .../transaction-route/scenario.ts | 29 + .../envelope-header/transaction-route/test.ts | 22 + .../transaction-url/scenario.ts | 29 + .../envelope-header/transaction-url/test.ts | 21 + .../envelope-header/transaction/scenario.ts | 18 + .../envelope-header/transaction/test.ts | 22 + .../linking/scenario-addLink-nested.ts | 36 + .../tracing/linking/scenario-addLink.ts | 23 + .../linking/scenario-addLinks-nested.ts | 34 + .../tracing/linking/scenario-addLinks.ts | 29 + .../tracing/linking/scenario-span-options.ts | 30 + .../suites/tracing/linking/test.ts | 193 +++++ .../suites/tracing/maxSpans/scenario.ts | 18 + .../suites/tracing/maxSpans/test.ts | 20 + .../tracing/meta-tags-twp-errors/no-server.js | 23 + .../tracing/meta-tags-twp-errors/server.js | 31 + .../tracing/meta-tags-twp-errors/test.ts | 67 ++ .../tracing/meta-tags/server-sdk-disabled.js | 35 + .../meta-tags/server-tracesSampleRate-zero.js | 34 + .../suites/tracing/meta-tags/server.js | 34 + .../suites/tracing/meta-tags/test.ts | 64 ++ .../requests/fetch-breadcrumbs/instrument.mjs | 21 + .../requests/fetch-breadcrumbs/scenario.mjs | 16 + .../requests/fetch-breadcrumbs/test.ts | 83 +++ .../fetch-no-tracing-no-spans/instrument.mjs | 13 + .../fetch-no-tracing-no-spans/scenario.mjs | 12 + .../fetch-no-tracing-no-spans/test.ts | 50 ++ .../requests/fetch-no-tracing/instrument.mjs | 13 + .../requests/fetch-no-tracing/scenario.mjs | 12 + .../tracing/requests/fetch-no-tracing/test.ts | 50 ++ .../instrument.mjs | 14 + .../fetch-sampled-no-active-span/scenario.mjs | 12 + .../fetch-sampled-no-active-span/test.ts | 50 ++ .../requests/fetch-unsampled/instrument.mjs | 14 + .../requests/fetch-unsampled/scenario.mjs | 15 + .../tracing/requests/fetch-unsampled/test.ts | 50 ++ .../requests/http-breadcrumbs/instrument.mjs | 20 + .../requests/http-breadcrumbs/scenario.mjs | 45 ++ .../tracing/requests/http-breadcrumbs/test.ts | 79 ++ .../http-no-tracing-no-spans/instrument.mjs | 20 + .../http-no-tracing-no-spans/scenario.mjs | 43 ++ .../requests/http-no-tracing-no-spans/test.ts | 202 ++++++ .../requests/http-no-tracing/instrument.mjs | 20 + .../requests/http-no-tracing/scenario.mjs | 43 ++ .../tracing/requests/http-no-tracing/test.ts | 103 +++ .../instrument.mjs | 14 + .../http-sampled-no-active-span/scenario.mjs | 28 + .../http-sampled-no-active-span/test.ts | 53 ++ .../requests/http-sampled/instrument.mjs | 14 + .../requests/http-sampled/scenario.mjs | 24 + .../tracing/requests/http-sampled/test.ts | 46 ++ .../requests/http-unsampled/instrument.mjs | 14 + .../requests/http-unsampled/scenario.mjs | 31 + .../tracing/requests/http-unsampled/test.ts | 53 ++ .../tracing/sample-rand-propagation/server.js | 41 ++ .../tracing/sample-rand-propagation/test.ts | 82 +++ .../no-tracing-enabled/server.js | 40 + .../no-tracing-enabled/test.ts | 26 + .../tracesSampleRate-0/server.js | 41 ++ .../tracesSampleRate-0/test.ts | 62 ++ .../tracesSampleRate/server.js | 41 ++ .../tracesSampleRate/test.ts | 62 ++ .../server.js | 55 ++ .../test.ts | 62 ++ .../server-no-explicit-org-id.ts | 35 + .../baggage-org-id/server-no-org-id.ts | 35 + .../baggage-org-id/server.ts | 36 + .../baggage-org-id/test.ts | 42 ++ .../tracePropagationTargets/scenario.ts | 39 + .../tracing/tracePropagationTargets/test.ts | 44 ++ .../suites/tsconfig.json | 3 + .../suites/winston/subject.ts | 76 ++ .../suites/winston/test.ts | 186 +++++ .../node-core-integration-tests/test.txt | 213 ++++++ .../node-core-integration-tests/tsconfig.json | 14 + .../tsconfig.test.json | 15 + .../tsconfig.types.json | 10 + .../utils/assertions.ts | 89 +++ .../utils/index.ts | 57 ++ .../utils/runner.ts | 683 ++++++++++++++++++ .../utils/server.ts | 85 +++ .../utils/setup-tests.ts | 12 + .../utils/setupOtel.js | 17 + .../utils/setupOtel.ts | 38 + .../vite.config.ts | 31 + dev-packages/rollup-utils/npmHelpers.mjs | 11 +- package.json | 5 +- 263 files changed, 9669 insertions(+), 5 deletions(-) create mode 100644 dev-packages/node-core-integration-tests/.eslintrc.js create mode 100644 dev-packages/node-core-integration-tests/.gitignore create mode 100644 dev-packages/node-core-integration-tests/README.md create mode 100644 dev-packages/node-core-integration-tests/package.json create mode 100644 dev-packages/node-core-integration-tests/rollup.npm.config.mjs create mode 100644 dev-packages/node-core-integration-tests/scripts/clean.js create mode 100644 dev-packages/node-core-integration-tests/scripts/use-ts-3_8.js create mode 100644 dev-packages/node-core-integration-tests/src/index.ts create mode 100644 dev-packages/node-core-integration-tests/suites/anr/app-path.mjs create mode 100644 dev-packages/node-core-integration-tests/suites/anr/basic-multiple.mjs create mode 100644 dev-packages/node-core-integration-tests/suites/anr/basic-session.js create mode 100644 dev-packages/node-core-integration-tests/suites/anr/basic.js create mode 100644 dev-packages/node-core-integration-tests/suites/anr/basic.mjs create mode 100644 dev-packages/node-core-integration-tests/suites/anr/forked.js create mode 100644 dev-packages/node-core-integration-tests/suites/anr/forker.js create mode 100644 dev-packages/node-core-integration-tests/suites/anr/indefinite.mjs create mode 100644 dev-packages/node-core-integration-tests/suites/anr/isolated.mjs create mode 100644 dev-packages/node-core-integration-tests/suites/anr/should-exit-forced.js create mode 100644 dev-packages/node-core-integration-tests/suites/anr/should-exit.js create mode 100644 dev-packages/node-core-integration-tests/suites/anr/stop-and-start.js create mode 100644 dev-packages/node-core-integration-tests/suites/anr/test.ts create mode 100644 dev-packages/node-core-integration-tests/suites/breadcrumbs/process-thread/app.mjs create mode 100644 dev-packages/node-core-integration-tests/suites/breadcrumbs/process-thread/test.ts create mode 100644 dev-packages/node-core-integration-tests/suites/breadcrumbs/process-thread/worker.mjs create mode 100644 dev-packages/node-core-integration-tests/suites/child-process/child.js create mode 100644 dev-packages/node-core-integration-tests/suites/child-process/child.mjs create mode 100644 dev-packages/node-core-integration-tests/suites/child-process/fork.js create mode 100644 dev-packages/node-core-integration-tests/suites/child-process/fork.mjs create mode 100644 dev-packages/node-core-integration-tests/suites/child-process/test.ts create mode 100644 dev-packages/node-core-integration-tests/suites/child-process/worker.js create mode 100644 dev-packages/node-core-integration-tests/suites/child-process/worker.mjs create mode 100644 dev-packages/node-core-integration-tests/suites/client-reports/drop-reasons/before-send/scenario.ts create mode 100644 dev-packages/node-core-integration-tests/suites/client-reports/drop-reasons/before-send/test.ts create mode 100644 dev-packages/node-core-integration-tests/suites/client-reports/drop-reasons/event-processors/scenario.ts create mode 100644 dev-packages/node-core-integration-tests/suites/client-reports/drop-reasons/event-processors/test.ts create mode 100644 dev-packages/node-core-integration-tests/suites/client-reports/periodic-send/scenario.ts create mode 100644 dev-packages/node-core-integration-tests/suites/client-reports/periodic-send/test.ts create mode 100644 dev-packages/node-core-integration-tests/suites/contextLines/filename-with-spaces/instrument.mjs create mode 100644 dev-packages/node-core-integration-tests/suites/contextLines/filename-with-spaces/scenario with space.cjs create mode 100644 dev-packages/node-core-integration-tests/suites/contextLines/filename-with-spaces/scenario with space.mjs create mode 100644 dev-packages/node-core-integration-tests/suites/contextLines/filename-with-spaces/test.ts create mode 100644 dev-packages/node-core-integration-tests/suites/contextLines/memory-leak/nested-file.ts create mode 100644 dev-packages/node-core-integration-tests/suites/contextLines/memory-leak/other-file.ts create mode 100644 dev-packages/node-core-integration-tests/suites/contextLines/memory-leak/scenario.ts create mode 100644 dev-packages/node-core-integration-tests/suites/contextLines/memory-leak/test.ts create mode 100644 dev-packages/node-core-integration-tests/suites/cron/cron/scenario.ts create mode 100644 dev-packages/node-core-integration-tests/suites/cron/cron/test.ts create mode 100644 dev-packages/node-core-integration-tests/suites/cron/node-cron/scenario.ts create mode 100644 dev-packages/node-core-integration-tests/suites/cron/node-cron/test.ts create mode 100644 dev-packages/node-core-integration-tests/suites/cron/node-schedule/scenario.ts create mode 100644 dev-packages/node-core-integration-tests/suites/cron/node-schedule/test.ts create mode 100644 dev-packages/node-core-integration-tests/suites/esm/import-in-the-middle/app.mjs create mode 100644 dev-packages/node-core-integration-tests/suites/esm/import-in-the-middle/sub-module.mjs create mode 100644 dev-packages/node-core-integration-tests/suites/esm/import-in-the-middle/test.ts create mode 100644 dev-packages/node-core-integration-tests/suites/esm/modules-integration/app.mjs create mode 100644 dev-packages/node-core-integration-tests/suites/esm/modules-integration/test.ts create mode 100644 dev-packages/node-core-integration-tests/suites/esm/warn-esm/server.js create mode 100644 dev-packages/node-core-integration-tests/suites/esm/warn-esm/server.mjs create mode 100644 dev-packages/node-core-integration-tests/suites/esm/warn-esm/test.ts create mode 100644 dev-packages/node-core-integration-tests/suites/featureFlags/featureFlagsIntegration/onError/basic/scenario.ts create mode 100644 dev-packages/node-core-integration-tests/suites/featureFlags/featureFlagsIntegration/onError/basic/test.ts create mode 100644 dev-packages/node-core-integration-tests/suites/featureFlags/featureFlagsIntegration/onError/withScope/scenario.ts create mode 100644 dev-packages/node-core-integration-tests/suites/featureFlags/featureFlagsIntegration/onError/withScope/test.ts create mode 100644 dev-packages/node-core-integration-tests/suites/featureFlags/featureFlagsIntegration/onSpan/scenario.ts create mode 100644 dev-packages/node-core-integration-tests/suites/featureFlags/featureFlagsIntegration/onSpan/test.ts create mode 100644 dev-packages/node-core-integration-tests/suites/no-code/app.js create mode 100644 dev-packages/node-core-integration-tests/suites/no-code/app.mjs create mode 100644 dev-packages/node-core-integration-tests/suites/no-code/test.ts create mode 100644 dev-packages/node-core-integration-tests/suites/proxy/basic.js create mode 100644 dev-packages/node-core-integration-tests/suites/proxy/test.ts create mode 100644 dev-packages/node-core-integration-tests/suites/public-api/LocalVariables/deny-inspector.mjs create mode 100644 dev-packages/node-core-integration-tests/suites/public-api/LocalVariables/local-variables-caught.js create mode 100644 dev-packages/node-core-integration-tests/suites/public-api/LocalVariables/local-variables-caught.mjs create mode 100644 dev-packages/node-core-integration-tests/suites/public-api/LocalVariables/local-variables-instrument.js create mode 100644 dev-packages/node-core-integration-tests/suites/public-api/LocalVariables/local-variables-no-sentry.js create mode 100644 dev-packages/node-core-integration-tests/suites/public-api/LocalVariables/local-variables-rethrow.js create mode 100644 dev-packages/node-core-integration-tests/suites/public-api/LocalVariables/local-variables.js create mode 100644 dev-packages/node-core-integration-tests/suites/public-api/LocalVariables/no-local-variables.js create mode 100644 dev-packages/node-core-integration-tests/suites/public-api/LocalVariables/test.ts create mode 100644 dev-packages/node-core-integration-tests/suites/public-api/OnUncaughtException/additional-listener-test-script.js create mode 100644 dev-packages/node-core-integration-tests/suites/public-api/OnUncaughtException/log-entire-error-to-console.js create mode 100644 dev-packages/node-core-integration-tests/suites/public-api/OnUncaughtException/mimic-native-behaviour-additional-listener-test-script.js create mode 100644 dev-packages/node-core-integration-tests/suites/public-api/OnUncaughtException/mimic-native-behaviour-no-additional-listener-test-script.js create mode 100644 dev-packages/node-core-integration-tests/suites/public-api/OnUncaughtException/no-additional-listener-test-script.js create mode 100644 dev-packages/node-core-integration-tests/suites/public-api/OnUncaughtException/test.ts create mode 100644 dev-packages/node-core-integration-tests/suites/public-api/addBreadcrumb/empty-obj/scenario.ts create mode 100644 dev-packages/node-core-integration-tests/suites/public-api/addBreadcrumb/empty-obj/test.ts create mode 100644 dev-packages/node-core-integration-tests/suites/public-api/addBreadcrumb/multiple_breadcrumbs/scenario.ts create mode 100644 dev-packages/node-core-integration-tests/suites/public-api/addBreadcrumb/multiple_breadcrumbs/test.ts create mode 100644 dev-packages/node-core-integration-tests/suites/public-api/addBreadcrumb/simple_breadcrumb/scenario.ts create mode 100644 dev-packages/node-core-integration-tests/suites/public-api/addBreadcrumb/simple_breadcrumb/test.ts create mode 100644 dev-packages/node-core-integration-tests/suites/public-api/captureException/catched-error/scenario.ts create mode 100644 dev-packages/node-core-integration-tests/suites/public-api/captureException/catched-error/test.ts create mode 100644 dev-packages/node-core-integration-tests/suites/public-api/captureException/empty-obj/scenario.ts create mode 100644 dev-packages/node-core-integration-tests/suites/public-api/captureException/empty-obj/test.ts create mode 100644 dev-packages/node-core-integration-tests/suites/public-api/captureException/simple-error/scenario.ts create mode 100644 dev-packages/node-core-integration-tests/suites/public-api/captureException/simple-error/test.ts create mode 100644 dev-packages/node-core-integration-tests/suites/public-api/captureMessage/parameterized_message/scenario.ts create mode 100644 dev-packages/node-core-integration-tests/suites/public-api/captureMessage/parameterized_message/test.ts create mode 100644 dev-packages/node-core-integration-tests/suites/public-api/captureMessage/simple_message/scenario.ts create mode 100644 dev-packages/node-core-integration-tests/suites/public-api/captureMessage/simple_message/test.ts create mode 100644 dev-packages/node-core-integration-tests/suites/public-api/captureMessage/simple_message_attachStackTrace/scenario.ts create mode 100644 dev-packages/node-core-integration-tests/suites/public-api/captureMessage/simple_message_attachStackTrace/test.ts create mode 100644 dev-packages/node-core-integration-tests/suites/public-api/captureMessage/with_level/scenario.ts create mode 100644 dev-packages/node-core-integration-tests/suites/public-api/captureMessage/with_level/test.ts create mode 100644 dev-packages/node-core-integration-tests/suites/public-api/configureScope/clear_scope/scenario.ts create mode 100644 dev-packages/node-core-integration-tests/suites/public-api/configureScope/clear_scope/test.ts create mode 100644 dev-packages/node-core-integration-tests/suites/public-api/configureScope/set_properties/scenario.ts create mode 100644 dev-packages/node-core-integration-tests/suites/public-api/configureScope/set_properties/test.ts create mode 100644 dev-packages/node-core-integration-tests/suites/public-api/onUnhandledRejectionIntegration/mode-none.js create mode 100644 dev-packages/node-core-integration-tests/suites/public-api/onUnhandledRejectionIntegration/mode-strict.js create mode 100644 dev-packages/node-core-integration-tests/suites/public-api/onUnhandledRejectionIntegration/mode-warn-error.js create mode 100644 dev-packages/node-core-integration-tests/suites/public-api/onUnhandledRejectionIntegration/mode-warn-string.js create mode 100644 dev-packages/node-core-integration-tests/suites/public-api/onUnhandledRejectionIntegration/scenario-strict.ts create mode 100644 dev-packages/node-core-integration-tests/suites/public-api/onUnhandledRejectionIntegration/scenario-warn.ts create mode 100644 dev-packages/node-core-integration-tests/suites/public-api/onUnhandledRejectionIntegration/test.ts create mode 100644 dev-packages/node-core-integration-tests/suites/public-api/scopes/initialScopes/scenario.ts create mode 100644 dev-packages/node-core-integration-tests/suites/public-api/scopes/initialScopes/test.ts create mode 100644 dev-packages/node-core-integration-tests/suites/public-api/scopes/isolationScope/scenario.ts create mode 100644 dev-packages/node-core-integration-tests/suites/public-api/scopes/isolationScope/test.ts create mode 100644 dev-packages/node-core-integration-tests/suites/public-api/setContext/multiple-contexts/scenario.ts create mode 100644 dev-packages/node-core-integration-tests/suites/public-api/setContext/multiple-contexts/test.ts create mode 100644 dev-packages/node-core-integration-tests/suites/public-api/setContext/non-serializable-context/scenario.ts create mode 100644 dev-packages/node-core-integration-tests/suites/public-api/setContext/non-serializable-context/test.ts create mode 100644 dev-packages/node-core-integration-tests/suites/public-api/setContext/simple-context/scenario.ts create mode 100644 dev-packages/node-core-integration-tests/suites/public-api/setContext/simple-context/test.ts create mode 100644 dev-packages/node-core-integration-tests/suites/public-api/setExtra/multiple-extras/scenario.ts create mode 100644 dev-packages/node-core-integration-tests/suites/public-api/setExtra/multiple-extras/test.ts create mode 100644 dev-packages/node-core-integration-tests/suites/public-api/setExtra/non-serializable-extra/scenario.ts create mode 100644 dev-packages/node-core-integration-tests/suites/public-api/setExtra/non-serializable-extra/test.ts create mode 100644 dev-packages/node-core-integration-tests/suites/public-api/setExtra/simple-extra/scenario.ts create mode 100644 dev-packages/node-core-integration-tests/suites/public-api/setExtra/simple-extra/test.ts create mode 100644 dev-packages/node-core-integration-tests/suites/public-api/setExtras/consecutive-calls/scenario.ts create mode 100644 dev-packages/node-core-integration-tests/suites/public-api/setExtras/consecutive-calls/test.ts create mode 100644 dev-packages/node-core-integration-tests/suites/public-api/setExtras/multiple-extras/scenario.ts create mode 100644 dev-packages/node-core-integration-tests/suites/public-api/setExtras/multiple-extras/test.ts create mode 100644 dev-packages/node-core-integration-tests/suites/public-api/setMeasurement/scenario.ts create mode 100644 dev-packages/node-core-integration-tests/suites/public-api/setMeasurement/test.ts create mode 100644 dev-packages/node-core-integration-tests/suites/public-api/setTag/with-primitives/scenario.ts create mode 100644 dev-packages/node-core-integration-tests/suites/public-api/setTag/with-primitives/test.ts create mode 100644 dev-packages/node-core-integration-tests/suites/public-api/setTags/with-primitives/scenario.ts create mode 100644 dev-packages/node-core-integration-tests/suites/public-api/setTags/with-primitives/test.ts create mode 100644 dev-packages/node-core-integration-tests/suites/public-api/setUser/unset_user/scenario.ts create mode 100644 dev-packages/node-core-integration-tests/suites/public-api/setUser/unset_user/test.ts create mode 100644 dev-packages/node-core-integration-tests/suites/public-api/setUser/update_user/scenario.ts create mode 100644 dev-packages/node-core-integration-tests/suites/public-api/setUser/update_user/test.ts create mode 100644 dev-packages/node-core-integration-tests/suites/public-api/startSpan/basic-usage/scenario.ts create mode 100644 dev-packages/node-core-integration-tests/suites/public-api/startSpan/basic-usage/test.ts create mode 100644 dev-packages/node-core-integration-tests/suites/public-api/startSpan/parallel-root-spans/scenario.ts create mode 100644 dev-packages/node-core-integration-tests/suites/public-api/startSpan/parallel-root-spans/test.ts create mode 100644 dev-packages/node-core-integration-tests/suites/public-api/startSpan/parallel-spans-in-scope-with-parentSpanId/scenario.ts create mode 100644 dev-packages/node-core-integration-tests/suites/public-api/startSpan/parallel-spans-in-scope-with-parentSpanId/test.ts create mode 100644 dev-packages/node-core-integration-tests/suites/public-api/startSpan/parallel-spans-in-scope/scenario.ts create mode 100644 dev-packages/node-core-integration-tests/suites/public-api/startSpan/parallel-spans-in-scope/test.ts create mode 100644 dev-packages/node-core-integration-tests/suites/public-api/startSpan/updateName-method/scenario.ts create mode 100644 dev-packages/node-core-integration-tests/suites/public-api/startSpan/updateName-method/test.ts create mode 100644 dev-packages/node-core-integration-tests/suites/public-api/startSpan/updateSpanName-function/scenario.ts create mode 100644 dev-packages/node-core-integration-tests/suites/public-api/startSpan/updateSpanName-function/test.ts create mode 100644 dev-packages/node-core-integration-tests/suites/public-api/startSpan/with-nested-spans/scenario.ts create mode 100644 dev-packages/node-core-integration-tests/suites/public-api/startSpan/with-nested-spans/test.ts create mode 100644 dev-packages/node-core-integration-tests/suites/public-api/withScope/nested-scopes/scenario.ts create mode 100644 dev-packages/node-core-integration-tests/suites/public-api/withScope/nested-scopes/test.ts create mode 100644 dev-packages/node-core-integration-tests/suites/sessions/errored-session-aggregate/test.ts create mode 100644 dev-packages/node-core-integration-tests/suites/sessions/exited-session-aggregate/test.ts create mode 100644 dev-packages/node-core-integration-tests/suites/sessions/server.ts create mode 100644 dev-packages/node-core-integration-tests/suites/tracing/dsc-txn-name-update/scenario-events.ts create mode 100644 dev-packages/node-core-integration-tests/suites/tracing/dsc-txn-name-update/scenario-headers.ts create mode 100644 dev-packages/node-core-integration-tests/suites/tracing/dsc-txn-name-update/test.ts create mode 100644 dev-packages/node-core-integration-tests/suites/tracing/envelope-header/error-active-span-unsampled/scenario.ts create mode 100644 dev-packages/node-core-integration-tests/suites/tracing/envelope-header/error-active-span-unsampled/test.ts create mode 100644 dev-packages/node-core-integration-tests/suites/tracing/envelope-header/error-active-span/scenario.ts create mode 100644 dev-packages/node-core-integration-tests/suites/tracing/envelope-header/error-active-span/test.ts create mode 100644 dev-packages/node-core-integration-tests/suites/tracing/envelope-header/error/scenario.ts create mode 100644 dev-packages/node-core-integration-tests/suites/tracing/envelope-header/error/test.ts create mode 100644 dev-packages/node-core-integration-tests/suites/tracing/envelope-header/sampleRate-propagation/server.js create mode 100644 dev-packages/node-core-integration-tests/suites/tracing/envelope-header/sampleRate-propagation/test.ts create mode 100644 dev-packages/node-core-integration-tests/suites/tracing/envelope-header/transaction-route/scenario.ts create mode 100644 dev-packages/node-core-integration-tests/suites/tracing/envelope-header/transaction-route/test.ts create mode 100644 dev-packages/node-core-integration-tests/suites/tracing/envelope-header/transaction-url/scenario.ts create mode 100644 dev-packages/node-core-integration-tests/suites/tracing/envelope-header/transaction-url/test.ts create mode 100644 dev-packages/node-core-integration-tests/suites/tracing/envelope-header/transaction/scenario.ts create mode 100644 dev-packages/node-core-integration-tests/suites/tracing/envelope-header/transaction/test.ts create mode 100644 dev-packages/node-core-integration-tests/suites/tracing/linking/scenario-addLink-nested.ts create mode 100644 dev-packages/node-core-integration-tests/suites/tracing/linking/scenario-addLink.ts create mode 100644 dev-packages/node-core-integration-tests/suites/tracing/linking/scenario-addLinks-nested.ts create mode 100644 dev-packages/node-core-integration-tests/suites/tracing/linking/scenario-addLinks.ts create mode 100644 dev-packages/node-core-integration-tests/suites/tracing/linking/scenario-span-options.ts create mode 100644 dev-packages/node-core-integration-tests/suites/tracing/linking/test.ts create mode 100644 dev-packages/node-core-integration-tests/suites/tracing/maxSpans/scenario.ts create mode 100644 dev-packages/node-core-integration-tests/suites/tracing/maxSpans/test.ts create mode 100644 dev-packages/node-core-integration-tests/suites/tracing/meta-tags-twp-errors/no-server.js create mode 100644 dev-packages/node-core-integration-tests/suites/tracing/meta-tags-twp-errors/server.js create mode 100644 dev-packages/node-core-integration-tests/suites/tracing/meta-tags-twp-errors/test.ts create mode 100644 dev-packages/node-core-integration-tests/suites/tracing/meta-tags/server-sdk-disabled.js create mode 100644 dev-packages/node-core-integration-tests/suites/tracing/meta-tags/server-tracesSampleRate-zero.js create mode 100644 dev-packages/node-core-integration-tests/suites/tracing/meta-tags/server.js create mode 100644 dev-packages/node-core-integration-tests/suites/tracing/meta-tags/test.ts create mode 100644 dev-packages/node-core-integration-tests/suites/tracing/requests/fetch-breadcrumbs/instrument.mjs create mode 100644 dev-packages/node-core-integration-tests/suites/tracing/requests/fetch-breadcrumbs/scenario.mjs create mode 100644 dev-packages/node-core-integration-tests/suites/tracing/requests/fetch-breadcrumbs/test.ts create mode 100644 dev-packages/node-core-integration-tests/suites/tracing/requests/fetch-no-tracing-no-spans/instrument.mjs create mode 100644 dev-packages/node-core-integration-tests/suites/tracing/requests/fetch-no-tracing-no-spans/scenario.mjs create mode 100644 dev-packages/node-core-integration-tests/suites/tracing/requests/fetch-no-tracing-no-spans/test.ts create mode 100644 dev-packages/node-core-integration-tests/suites/tracing/requests/fetch-no-tracing/instrument.mjs create mode 100644 dev-packages/node-core-integration-tests/suites/tracing/requests/fetch-no-tracing/scenario.mjs create mode 100644 dev-packages/node-core-integration-tests/suites/tracing/requests/fetch-no-tracing/test.ts create mode 100644 dev-packages/node-core-integration-tests/suites/tracing/requests/fetch-sampled-no-active-span/instrument.mjs create mode 100644 dev-packages/node-core-integration-tests/suites/tracing/requests/fetch-sampled-no-active-span/scenario.mjs create mode 100644 dev-packages/node-core-integration-tests/suites/tracing/requests/fetch-sampled-no-active-span/test.ts create mode 100644 dev-packages/node-core-integration-tests/suites/tracing/requests/fetch-unsampled/instrument.mjs create mode 100644 dev-packages/node-core-integration-tests/suites/tracing/requests/fetch-unsampled/scenario.mjs create mode 100644 dev-packages/node-core-integration-tests/suites/tracing/requests/fetch-unsampled/test.ts create mode 100644 dev-packages/node-core-integration-tests/suites/tracing/requests/http-breadcrumbs/instrument.mjs create mode 100644 dev-packages/node-core-integration-tests/suites/tracing/requests/http-breadcrumbs/scenario.mjs create mode 100644 dev-packages/node-core-integration-tests/suites/tracing/requests/http-breadcrumbs/test.ts create mode 100644 dev-packages/node-core-integration-tests/suites/tracing/requests/http-no-tracing-no-spans/instrument.mjs create mode 100644 dev-packages/node-core-integration-tests/suites/tracing/requests/http-no-tracing-no-spans/scenario.mjs create mode 100644 dev-packages/node-core-integration-tests/suites/tracing/requests/http-no-tracing-no-spans/test.ts create mode 100644 dev-packages/node-core-integration-tests/suites/tracing/requests/http-no-tracing/instrument.mjs create mode 100644 dev-packages/node-core-integration-tests/suites/tracing/requests/http-no-tracing/scenario.mjs create mode 100644 dev-packages/node-core-integration-tests/suites/tracing/requests/http-no-tracing/test.ts create mode 100644 dev-packages/node-core-integration-tests/suites/tracing/requests/http-sampled-no-active-span/instrument.mjs create mode 100644 dev-packages/node-core-integration-tests/suites/tracing/requests/http-sampled-no-active-span/scenario.mjs create mode 100644 dev-packages/node-core-integration-tests/suites/tracing/requests/http-sampled-no-active-span/test.ts create mode 100644 dev-packages/node-core-integration-tests/suites/tracing/requests/http-sampled/instrument.mjs create mode 100644 dev-packages/node-core-integration-tests/suites/tracing/requests/http-sampled/scenario.mjs create mode 100644 dev-packages/node-core-integration-tests/suites/tracing/requests/http-sampled/test.ts create mode 100644 dev-packages/node-core-integration-tests/suites/tracing/requests/http-unsampled/instrument.mjs create mode 100644 dev-packages/node-core-integration-tests/suites/tracing/requests/http-unsampled/scenario.mjs create mode 100644 dev-packages/node-core-integration-tests/suites/tracing/requests/http-unsampled/test.ts create mode 100644 dev-packages/node-core-integration-tests/suites/tracing/sample-rand-propagation/server.js create mode 100644 dev-packages/node-core-integration-tests/suites/tracing/sample-rand-propagation/test.ts create mode 100644 dev-packages/node-core-integration-tests/suites/tracing/sample-rate-propagation/no-tracing-enabled/server.js create mode 100644 dev-packages/node-core-integration-tests/suites/tracing/sample-rate-propagation/no-tracing-enabled/test.ts create mode 100644 dev-packages/node-core-integration-tests/suites/tracing/sample-rate-propagation/tracesSampleRate-0/server.js create mode 100644 dev-packages/node-core-integration-tests/suites/tracing/sample-rate-propagation/tracesSampleRate-0/test.ts create mode 100644 dev-packages/node-core-integration-tests/suites/tracing/sample-rate-propagation/tracesSampleRate/server.js create mode 100644 dev-packages/node-core-integration-tests/suites/tracing/sample-rate-propagation/tracesSampleRate/test.ts create mode 100644 dev-packages/node-core-integration-tests/suites/tracing/sample-rate-propagation/tracesSampler-with-otel-http-instrumentation/server.js create mode 100644 dev-packages/node-core-integration-tests/suites/tracing/sample-rate-propagation/tracesSampler-with-otel-http-instrumentation/test.ts create mode 100644 dev-packages/node-core-integration-tests/suites/tracing/tracePropagationTargets/baggage-org-id/server-no-explicit-org-id.ts create mode 100644 dev-packages/node-core-integration-tests/suites/tracing/tracePropagationTargets/baggage-org-id/server-no-org-id.ts create mode 100644 dev-packages/node-core-integration-tests/suites/tracing/tracePropagationTargets/baggage-org-id/server.ts create mode 100644 dev-packages/node-core-integration-tests/suites/tracing/tracePropagationTargets/baggage-org-id/test.ts create mode 100644 dev-packages/node-core-integration-tests/suites/tracing/tracePropagationTargets/scenario.ts create mode 100644 dev-packages/node-core-integration-tests/suites/tracing/tracePropagationTargets/test.ts create mode 100644 dev-packages/node-core-integration-tests/suites/tsconfig.json create mode 100644 dev-packages/node-core-integration-tests/suites/winston/subject.ts create mode 100644 dev-packages/node-core-integration-tests/suites/winston/test.ts create mode 100644 dev-packages/node-core-integration-tests/test.txt create mode 100644 dev-packages/node-core-integration-tests/tsconfig.json create mode 100644 dev-packages/node-core-integration-tests/tsconfig.test.json create mode 100644 dev-packages/node-core-integration-tests/tsconfig.types.json create mode 100644 dev-packages/node-core-integration-tests/utils/assertions.ts create mode 100644 dev-packages/node-core-integration-tests/utils/index.ts create mode 100644 dev-packages/node-core-integration-tests/utils/runner.ts create mode 100644 dev-packages/node-core-integration-tests/utils/server.ts create mode 100644 dev-packages/node-core-integration-tests/utils/setup-tests.ts create mode 100644 dev-packages/node-core-integration-tests/utils/setupOtel.js create mode 100644 dev-packages/node-core-integration-tests/utils/setupOtel.ts create mode 100644 dev-packages/node-core-integration-tests/vite.config.ts diff --git a/dev-packages/node-core-integration-tests/.eslintrc.js b/dev-packages/node-core-integration-tests/.eslintrc.js new file mode 100644 index 000000000000..0598ba3f5ca1 --- /dev/null +++ b/dev-packages/node-core-integration-tests/.eslintrc.js @@ -0,0 +1,40 @@ +module.exports = { + env: { + node: true, + }, + extends: ['../../.eslintrc.js'], + overrides: [ + { + files: ['utils/**/*.ts', 'src/**/*.ts'], + parserOptions: { + project: ['tsconfig.json'], + sourceType: 'module', + }, + }, + { + files: ['suites/**/*.ts', 'suites/**/*.mjs'], + parserOptions: { + project: ['tsconfig.test.json'], + sourceType: 'module', + ecmaVersion: 'latest', + }, + globals: { + fetch: 'readonly', + }, + rules: { + '@typescript-eslint/typedef': 'off', + // Explicitly allow ts-ignore with description for Node integration tests + // Reason: We run these tests on TS3.8 which doesn't support `@ts-expect-error` + '@typescript-eslint/ban-ts-comment': [ + 'error', + { + 'ts-ignore': 'allow-with-description', + 'ts-expect-error': true, + }, + ], + // We rely on having imports after init() is called for OTEL + 'import/first': 'off', + }, + }, + ], +}; diff --git a/dev-packages/node-core-integration-tests/.gitignore b/dev-packages/node-core-integration-tests/.gitignore new file mode 100644 index 000000000000..365cb959a94c --- /dev/null +++ b/dev-packages/node-core-integration-tests/.gitignore @@ -0,0 +1 @@ +suites/**/tmp_* diff --git a/dev-packages/node-core-integration-tests/README.md b/dev-packages/node-core-integration-tests/README.md new file mode 100644 index 000000000000..2e49b2ee4a2e --- /dev/null +++ b/dev-packages/node-core-integration-tests/README.md @@ -0,0 +1,66 @@ +# Integration Tests for Sentry Node.JS Core SDK with OpenTelemetry v2 dependencies + +## Structure + +``` +suites/ +|---- public-api/ + |---- captureMessage/ + |---- test.ts [assertions] + |---- scenario.ts [Sentry initialization and test subject] + |---- customTest/ + |---- test.ts [assertions] + |---- scenario_1.ts [optional extra test scenario] + |---- scenario_2.ts [optional extra test scenario] + |---- server_with_mongo.ts [optional custom server] + |---- server_with_postgres.ts [optional custom server] +``` + +The tests are grouped by their scopes, such as `public-api` or `tracing`. In every group of tests, there are multiple +folders containing test scenarios and assertions. + +`scenario.ts` contains the initialization logic and the test subject. By default, `{TEST_DIR}/scenario.ts` is used, but +`runServer` also accepts an optional `scenarioPath` argument for non-standard usage. + +`test.ts` is required for each test case, and contains the server runner logic, request interceptors for Sentry +requests, and assertions. Test server, interceptors and assertions are all run on the same Vitest thread. + +### Utilities + +`utils/` contains helpers and Sentry-specific assertions that can be used in (`test.ts`). + +Nock interceptors are internally used to capture envelope requests by `getEnvelopeRequest` and +`getMultipleEnvelopeRequest` helpers. After capturing required requests, the interceptors are removed. Nock can manually +be used inside the test cases to intercept requests but should be removed before the test ends, as not to cause +flakiness. + +## Running Tests Locally + +Tests can be run locally with: + +`yarn test` + +To run tests with Vitest's watch mode: + +`yarn test:watch` + +To filter tests by their title: + +`yarn test -t "set different properties of a scope"` + +## Debugging Tests + +To enable verbose logging during test execution, set the `DEBUG` environment variable: + +`DEBUG=1 yarn test` + +When `DEBUG` is enabled, the test runner will output: + +- Test scenario startup information (path, flags, DSN) +- Docker Compose output when using `withDockerCompose` +- Child process stdout and stderr output +- HTTP requests made during tests +- Process errors and exceptions +- Line-by-line output from test scenarios + +This is particularly useful when debugging failing tests or understanding the test execution flow. diff --git a/dev-packages/node-core-integration-tests/package.json b/dev-packages/node-core-integration-tests/package.json new file mode 100644 index 000000000000..5e34fb9aacc3 --- /dev/null +++ b/dev-packages/node-core-integration-tests/package.json @@ -0,0 +1,67 @@ +{ + "name": "@sentry-internal/node-core-integration-tests", + "version": "9.34.0", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "private": true, + "main": "build/cjs/index.js", + "module": "build/esm/index.js", + "types": "build/types/src/index.d.ts", + "scripts": { + "build": "run-s build:transpile build:types", + "build:dev": "yarn build", + "build:transpile": "rollup -c rollup.npm.config.mjs", + "build:types": "tsc -p tsconfig.types.json", + "clean": "rimraf -g **/node_modules && run-p clean:script", + "clean:script": "node scripts/clean.js", + "lint": "eslint . --format stylish", + "fix": "eslint . --format stylish --fix", + "type-check": "tsc", + "test": "vitest run", + "test:watch": "yarn test --watch" + }, + "dependencies": { + "@nestjs/common": "11.0.16", + "@nestjs/core": "10.4.6", + "@nestjs/platform-express": "10.4.6", + "@opentelemetry/api": "^1.9.0", + "@opentelemetry/context-async-hooks": "^1.30.1", + "@opentelemetry/core": "^1.30.1", + "@opentelemetry/instrumentation": "^0.57.2", + "@opentelemetry/instrumentation-http": "0.57.2", + "@opentelemetry/resources": "^1.30.1", + "@opentelemetry/sdk-trace-base": "^1.30.1", + "@opentelemetry/semantic-conventions": "^1.34.0", + "@sentry/core": "9.34.0", + "@sentry/node-core": "9.34.0", + "body-parser": "^1.20.3", + "cors": "^2.8.5", + "cron": "^3.1.6", + "express": "^4.21.1", + "http-terminator": "^3.2.0", + "nock": "^13.5.5", + "node-cron": "^3.0.3", + "node-schedule": "^2.1.1", + "proxy": "^2.1.1", + "reflect-metadata": "0.2.1", + "rxjs": "^7.8.1", + "winston": "^3.17.0", + "yargs": "^16.2.0" + }, + "devDependencies": { + "@types/node-cron": "^3.0.11", + "@types/node-schedule": "^2.1.7", + "globby": "11" + }, + "config": { + "mongodbMemoryServer": { + "preferGlobalPath": true, + "runtimeDownload": false + } + }, + "volta": { + "extends": "../../package.json" + } +} diff --git a/dev-packages/node-core-integration-tests/rollup.npm.config.mjs b/dev-packages/node-core-integration-tests/rollup.npm.config.mjs new file mode 100644 index 000000000000..84a06f2fb64a --- /dev/null +++ b/dev-packages/node-core-integration-tests/rollup.npm.config.mjs @@ -0,0 +1,3 @@ +import { makeBaseNPMConfig, makeNPMConfigVariants } from '@sentry-internal/rollup-utils'; + +export default makeNPMConfigVariants(makeBaseNPMConfig()); diff --git a/dev-packages/node-core-integration-tests/scripts/clean.js b/dev-packages/node-core-integration-tests/scripts/clean.js new file mode 100644 index 000000000000..0610e39f92d4 --- /dev/null +++ b/dev-packages/node-core-integration-tests/scripts/clean.js @@ -0,0 +1,19 @@ +const { execSync } = require('child_process'); +const globby = require('globby'); +const { dirname, join } = require('path'); + +const cwd = join(__dirname, '..'); +const paths = globby.sync(['suites/**/docker-compose.yml'], { cwd }).map(path => join(cwd, dirname(path))); + +// eslint-disable-next-line no-console +console.log('Cleaning up docker containers and volumes...'); + +for (const path of paths) { + try { + // eslint-disable-next-line no-console + console.log(`docker compose down @ ${path}`); + execSync('docker compose down --volumes', { stdio: 'inherit', cwd: path }); + } catch (_) { + // + } +} diff --git a/dev-packages/node-core-integration-tests/scripts/use-ts-3_8.js b/dev-packages/node-core-integration-tests/scripts/use-ts-3_8.js new file mode 100644 index 000000000000..d759179f8e06 --- /dev/null +++ b/dev-packages/node-core-integration-tests/scripts/use-ts-3_8.js @@ -0,0 +1,39 @@ +/* eslint-disable no-console */ +const { execSync } = require('child_process'); +const { join } = require('path'); +const { readFileSync, writeFileSync } = require('fs'); + +const cwd = join(__dirname, '../../..'); + +// Newer versions of the Express types use syntax that isn't supported by TypeScript 3.8. +// We'll pin to the last version of those types that are compatible. +console.log('Pinning Express types to old versions...'); + +const packageJsonPath = join(cwd, 'package.json'); +const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf8')); + +if (!packageJson.resolutions) packageJson.resolutions = {}; +packageJson.resolutions['@types/express'] = '4.17.13'; +packageJson.resolutions['@types/express-serve-static-core'] = '4.17.30'; + +writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2)); + +const tsVersion = '3.8'; + +console.log(`Installing typescript@${tsVersion}, and @types/node@14...`); + +execSync(`yarn add --dev --ignore-workspace-root-check typescript@${tsVersion} @types/node@^14`, { + stdio: 'inherit', + cwd, +}); + +console.log('Removing unsupported tsconfig options...'); + +const baseTscConfigPath = join(cwd, 'packages/typescript/tsconfig.json'); + +const tsConfig = require(baseTscConfigPath); + +// TS 3.8 fails build when it encounters a config option it does not understand, so we remove it :( +delete tsConfig.compilerOptions.noUncheckedIndexedAccess; + +writeFileSync(baseTscConfigPath, JSON.stringify(tsConfig, null, 2)); diff --git a/dev-packages/node-core-integration-tests/src/index.ts b/dev-packages/node-core-integration-tests/src/index.ts new file mode 100644 index 000000000000..ed6a150bd8d6 --- /dev/null +++ b/dev-packages/node-core-integration-tests/src/index.ts @@ -0,0 +1,57 @@ +import type { BaseTransportOptions, Envelope, Transport, TransportMakeRequestResponse } from '@sentry/core'; +import type { Express } from 'express'; +import type { AddressInfo } from 'net'; + +/** + * Debug logging transport + */ +export function loggingTransport(_options: BaseTransportOptions): Transport { + return { + send(request: Envelope): Promise { + // eslint-disable-next-line no-console + console.log(JSON.stringify(request)); + return Promise.resolve({ statusCode: 200 }); + }, + flush(): PromiseLike { + return new Promise(resolve => setTimeout(() => resolve(true), 1000)); + }, + }; +} + +/** + * Starts an express server and sends the port to the runner + * @param app Express app + * @param port Port to start the app on. USE WITH CAUTION! By default a random port will be chosen. + * Setting this port to something specific is useful for local debugging but dangerous for + * CI/CD environments where port collisions can cause flakes! + */ +export function startExpressServerAndSendPortToRunner( + app: Pick, + port: number | undefined = undefined, +): void { + const server = app.listen(port || 0, () => { + const address = server.address() as AddressInfo; + + // @ts-expect-error If we write the port to the app we can read it within route handlers in tests + app.port = port || address.port; + + // eslint-disable-next-line no-console + console.log(`{"port":${port || address.port}}`); + }); +} + +/** + * Sends the port to the runner + */ +export function sendPortToRunner(port: number): void { + // eslint-disable-next-line no-console + console.log(`{"port":${port}}`); +} + +/** + * Can be used to get the port of a running app, so requests can be sent to a server from within the server. + */ +export function getPortAppIsRunningOn(app: Express): number | undefined { + // @ts-expect-error It's not defined in the types but we'd like to read it. + return app.port; +} diff --git a/dev-packages/node-core-integration-tests/suites/anr/app-path.mjs b/dev-packages/node-core-integration-tests/suites/anr/app-path.mjs new file mode 100644 index 000000000000..2cf1cff1ea32 --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/anr/app-path.mjs @@ -0,0 +1,37 @@ +import * as Sentry from '@sentry/node-core'; +import * as assert from 'assert'; +import * as crypto from 'crypto'; +import * as path from 'path'; +import * as url from 'url'; +import { setupOtel } from '../../utils/setupOtel.js'; + +global._sentryDebugIds = { [new Error().stack]: 'aaaaaaaa-aaaa-4aaa-aaaa-aaaaaaaaaa' }; + +const __dirname = path.dirname(url.fileURLToPath(import.meta.url)); + +setTimeout(() => { + process.exit(); +}, 10000); + +const client = Sentry.init({ + dsn: process.env.SENTRY_DSN, + release: '1.0', + integrations: [Sentry.anrIntegration({ captureStackTrace: true, anrThreshold: 100, appRootPath: __dirname })], +}); + +setupOtel(client); + +Sentry.setUser({ email: 'person@home.com' }); +Sentry.addBreadcrumb({ message: 'important message!' }); + +function longWork() { + for (let i = 0; i < 20; i++) { + const salt = crypto.randomBytes(128).toString('base64'); + const hash = crypto.pbkdf2Sync('myPassword', salt, 10000, 512, 'sha512'); + assert.ok(hash); + } +} + +setTimeout(() => { + longWork(); +}, 1000); diff --git a/dev-packages/node-core-integration-tests/suites/anr/basic-multiple.mjs b/dev-packages/node-core-integration-tests/suites/anr/basic-multiple.mjs new file mode 100644 index 000000000000..9c8b17b590bc --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/anr/basic-multiple.mjs @@ -0,0 +1,37 @@ +import * as Sentry from '@sentry/node-core'; +import * as assert from 'assert'; +import * as crypto from 'crypto'; +import { setupOtel } from '../../utils/setupOtel.js'; + +global._sentryDebugIds = { [new Error().stack]: 'aaaaaaaa-aaaa-4aaa-aaaa-aaaaaaaaaa' }; + +setTimeout(() => { + process.exit(); +}, 10000); + +const client = Sentry.init({ + dsn: process.env.SENTRY_DSN, + release: '1.0', + integrations: [Sentry.anrIntegration({ captureStackTrace: true, anrThreshold: 100, maxAnrEvents: 2 })], +}); + +setupOtel(client); + +Sentry.setUser({ email: 'person@home.com' }); +Sentry.addBreadcrumb({ message: 'important message!' }); + +function longWork() { + for (let i = 0; i < 20; i++) { + const salt = crypto.randomBytes(128).toString('base64'); + const hash = crypto.pbkdf2Sync('myPassword', salt, 10000, 512, 'sha512'); + assert.ok(hash); + } +} + +setTimeout(() => { + longWork(); +}, 1000); + +setTimeout(() => { + longWork(); +}, 4000); diff --git a/dev-packages/node-core-integration-tests/suites/anr/basic-session.js b/dev-packages/node-core-integration-tests/suites/anr/basic-session.js new file mode 100644 index 000000000000..541c5ee25e36 --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/anr/basic-session.js @@ -0,0 +1,32 @@ +const crypto = require('crypto'); +const assert = require('assert'); + +const Sentry = require('@sentry/node-core'); +const { setupOtel } = require('../../utils/setupOtel.js'); + +setTimeout(() => { + process.exit(); +}, 10000); + +const client = Sentry.init({ + dsn: process.env.SENTRY_DSN, + release: '1.0.0', + integrations: [Sentry.anrIntegration({ captureStackTrace: true, anrThreshold: 100 })], +}); + +setupOtel(client); + +Sentry.setUser({ email: 'person@home.com' }); +Sentry.addBreadcrumb({ message: 'important message!' }); + +function longWork() { + for (let i = 0; i < 20; i++) { + const salt = crypto.randomBytes(128).toString('base64'); + const hash = crypto.pbkdf2Sync('myPassword', salt, 10000, 512, 'sha512'); + assert.ok(hash); + } +} + +setTimeout(() => { + longWork(); +}, 1000); diff --git a/dev-packages/node-core-integration-tests/suites/anr/basic.js b/dev-packages/node-core-integration-tests/suites/anr/basic.js new file mode 100644 index 000000000000..738810f2fa2f --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/anr/basic.js @@ -0,0 +1,34 @@ +const crypto = require('crypto'); +const assert = require('assert'); + +const Sentry = require('@sentry/node-core'); +const { setupOtel } = require('../../utils/setupOtel.js'); + +global._sentryDebugIds = { [new Error().stack]: 'aaaaaaaa-aaaa-4aaa-aaaa-aaaaaaaaaa' }; + +setTimeout(() => { + process.exit(); +}, 10000); + +const client = Sentry.init({ + dsn: process.env.SENTRY_DSN, + release: '1.0', + integrations: [Sentry.anrIntegration({ captureStackTrace: true, anrThreshold: 100 })], +}); + +setupOtel(client); + +Sentry.setUser({ email: 'person@home.com' }); +Sentry.addBreadcrumb({ message: 'important message!' }); + +function longWork() { + for (let i = 0; i < 20; i++) { + const salt = crypto.randomBytes(128).toString('base64'); + const hash = crypto.pbkdf2Sync('myPassword', salt, 10000, 512, 'sha512'); + assert.ok(hash); + } +} + +setTimeout(() => { + longWork(); +}, 1000); diff --git a/dev-packages/node-core-integration-tests/suites/anr/basic.mjs b/dev-packages/node-core-integration-tests/suites/anr/basic.mjs new file mode 100644 index 000000000000..5902394e8109 --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/anr/basic.mjs @@ -0,0 +1,38 @@ +import * as Sentry from '@sentry/node-core'; +import * as assert from 'assert'; +import * as crypto from 'crypto'; +import { setupOtel } from '../../utils/setupOtel.js'; + +global._sentryDebugIds = { [new Error().stack]: 'aaaaaaaa-aaaa-4aaa-aaaa-aaaaaaaaaa' }; + +setTimeout(() => { + process.exit(); +}, 10000); + +const client = Sentry.init({ + dsn: process.env.SENTRY_DSN, + release: '1.0', + integrations: [Sentry.anrIntegration({ captureStackTrace: true, anrThreshold: 100 })], +}); + +setupOtel(client); + +Sentry.setUser({ email: 'person@home.com' }); +Sentry.addBreadcrumb({ message: 'important message!' }); + +function longWork() { + for (let i = 0; i < 20; i++) { + const salt = crypto.randomBytes(128).toString('base64'); + const hash = crypto.pbkdf2Sync('myPassword', salt, 10000, 512, 'sha512'); + assert.ok(hash); + } +} + +setTimeout(() => { + longWork(); +}, 1000); + +// Ensure we only send one event even with multiple blocking events +setTimeout(() => { + longWork(); +}, 4000); diff --git a/dev-packages/node-core-integration-tests/suites/anr/forked.js b/dev-packages/node-core-integration-tests/suites/anr/forked.js new file mode 100644 index 000000000000..be4848abee5c --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/anr/forked.js @@ -0,0 +1,33 @@ +const crypto = require('crypto'); +const assert = require('assert'); + +const Sentry = require('@sentry/node-core'); +const { setupOtel } = require('../../utils/setupOtel.js'); + +setTimeout(() => { + process.exit(); +}, 10000); + +const client = Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', + debug: true, + integrations: [Sentry.anrIntegration({ captureStackTrace: true, anrThreshold: 100 })], +}); + +setupOtel(client); + +Sentry.setUser({ email: 'person@home.com' }); +Sentry.addBreadcrumb({ message: 'important message!' }); + +function longWork() { + for (let i = 0; i < 20; i++) { + const salt = crypto.randomBytes(128).toString('base64'); + const hash = crypto.pbkdf2Sync('myPassword', salt, 10000, 512, 'sha512'); + assert.ok(hash); + } +} + +setTimeout(() => { + longWork(); +}, 1000); diff --git a/dev-packages/node-core-integration-tests/suites/anr/forker.js b/dev-packages/node-core-integration-tests/suites/anr/forker.js new file mode 100644 index 000000000000..c1ac5e1ccd1c --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/anr/forker.js @@ -0,0 +1,7 @@ +const { fork } = require('child_process'); +const { join } = require('path'); + +const child = fork(join(__dirname, 'forked.js'), { stdio: 'inherit' }); +child.on('exit', () => { + process.exit(); +}); diff --git a/dev-packages/node-core-integration-tests/suites/anr/indefinite.mjs b/dev-packages/node-core-integration-tests/suites/anr/indefinite.mjs new file mode 100644 index 000000000000..db9217ec34b2 --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/anr/indefinite.mjs @@ -0,0 +1,32 @@ +import * as Sentry from '@sentry/node-core'; +import * as assert from 'assert'; +import * as crypto from 'crypto'; +import { setupOtel } from '../../utils/setupOtel.js'; + +setTimeout(() => { + process.exit(); +}, 10000); + +const client = Sentry.init({ + dsn: process.env.SENTRY_DSN, + release: '1.0', + integrations: [Sentry.anrIntegration({ captureStackTrace: true, anrThreshold: 100 })], +}); + +setupOtel(client); + +Sentry.setUser({ email: 'person@home.com' }); +Sentry.addBreadcrumb({ message: 'important message!' }); + +function longWork() { + // This loop will run almost indefinitely + for (let i = 0; i < 2000000000; i++) { + const salt = crypto.randomBytes(128).toString('base64'); + const hash = crypto.pbkdf2Sync('myPassword', salt, 10000, 512, 'sha512'); + assert.ok(hash); + } +} + +setTimeout(() => { + longWork(); +}, 1000); diff --git a/dev-packages/node-core-integration-tests/suites/anr/isolated.mjs b/dev-packages/node-core-integration-tests/suites/anr/isolated.mjs new file mode 100644 index 000000000000..37e804d01b71 --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/anr/isolated.mjs @@ -0,0 +1,53 @@ +import * as Sentry from '@sentry/node-core'; +import * as assert from 'assert'; +import * as crypto from 'crypto'; +import { setupOtel } from '../../utils/setupOtel.js'; + +setTimeout(() => { + process.exit(); +}, 10000); + +const client = Sentry.init({ + dsn: process.env.SENTRY_DSN, + release: '1.0', + integrations: [Sentry.anrIntegration({ captureStackTrace: true, anrThreshold: 100 })], +}); + +setupOtel(client); + +async function longWork() { + await new Promise(resolve => setTimeout(resolve, 1000)); + + for (let i = 0; i < 20; i++) { + const salt = crypto.randomBytes(128).toString('base64'); + const hash = crypto.pbkdf2Sync('myPassword', salt, 10000, 512, 'sha512'); + assert.ok(hash); + } +} + +function neverResolve() { + return new Promise(() => { + // + }); +} + +const fns = [ + neverResolve, + neverResolve, + neverResolve, + neverResolve, + neverResolve, + longWork, // [5] + neverResolve, + neverResolve, + neverResolve, + neverResolve, +]; + +for (let id = 0; id < 10; id++) { + Sentry.withIsolationScope(async () => { + Sentry.setUser({ id }); + + await fns[id](); + }); +} diff --git a/dev-packages/node-core-integration-tests/suites/anr/should-exit-forced.js b/dev-packages/node-core-integration-tests/suites/anr/should-exit-forced.js new file mode 100644 index 000000000000..523d7d7fce6b --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/anr/should-exit-forced.js @@ -0,0 +1,20 @@ +const Sentry = require('@sentry/node-core'); +const { setupOtel } = require('../../utils/setupOtel.js'); + +function configureSentry() { + const client = Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', + debug: true, + integrations: [Sentry.anrIntegration({ captureStackTrace: true })], + }); + setupOtel(client); +} + +async function main() { + configureSentry(); + await new Promise(resolve => setTimeout(resolve, 1000)); + process.exit(0); +} + +main(); diff --git a/dev-packages/node-core-integration-tests/suites/anr/should-exit.js b/dev-packages/node-core-integration-tests/suites/anr/should-exit.js new file mode 100644 index 000000000000..ba8d24c347d5 --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/anr/should-exit.js @@ -0,0 +1,19 @@ +const Sentry = require('@sentry/node-core'); +const { setupOtel } = require('../../utils/setupOtel.js'); + +function configureSentry() { + const client = Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', + debug: true, + integrations: [Sentry.anrIntegration({ captureStackTrace: true })], + }); + setupOtel(client); +} + +async function main() { + configureSentry(); + await new Promise(resolve => setTimeout(resolve, 1000)); +} + +main(); diff --git a/dev-packages/node-core-integration-tests/suites/anr/stop-and-start.js b/dev-packages/node-core-integration-tests/suites/anr/stop-and-start.js new file mode 100644 index 000000000000..c377c8716814 --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/anr/stop-and-start.js @@ -0,0 +1,55 @@ +const crypto = require('crypto'); +const assert = require('assert'); + +const Sentry = require('@sentry/node-core'); +const { setupOtel } = require('../../utils/setupOtel.js'); + +setTimeout(() => { + process.exit(); +}, 20000); + +const anr = Sentry.anrIntegration({ captureStackTrace: true, anrThreshold: 100 }); + +const client = Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', + debug: true, + integrations: [anr], +}); + +setupOtel(client); + +Sentry.setUser({ email: 'person@home.com' }); +Sentry.addBreadcrumb({ message: 'important message!' }); + +function longWorkIgnored() { + for (let i = 0; i < 20; i++) { + const salt = crypto.randomBytes(128).toString('base64'); + const hash = crypto.pbkdf2Sync('myPassword', salt, 10000, 512, 'sha512'); + assert.ok(hash); + } +} + +function longWork() { + for (let i = 0; i < 20; i++) { + const salt = crypto.randomBytes(128).toString('base64'); + const hash = crypto.pbkdf2Sync('myPassword', salt, 10000, 512, 'sha512'); + assert.ok(hash); + } +} + +setTimeout(() => { + anr.stopWorker(); + + setTimeout(() => { + longWorkIgnored(); + + setTimeout(() => { + anr.startWorker(); + + setTimeout(() => { + longWork(); + }); + }, 2000); + }, 2000); +}, 2000); diff --git a/dev-packages/node-core-integration-tests/suites/anr/test.ts b/dev-packages/node-core-integration-tests/suites/anr/test.ts new file mode 100644 index 000000000000..08b6a6571e17 --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/anr/test.ts @@ -0,0 +1,253 @@ +import type { Event } from '@sentry/core'; +import { afterAll, describe, expect, test } from 'vitest'; +import { cleanupChildProcesses, createRunner } from '../../utils/runner'; + +const ANR_EVENT = { + // Ensure we have context + contexts: { + trace: { + span_id: expect.stringMatching(/[a-f0-9]{16}/), + trace_id: expect.stringMatching(/[a-f0-9]{32}/), + }, + device: { + arch: expect.any(String), + }, + app: { + app_start_time: expect.any(String), + }, + os: { + name: expect.any(String), + }, + culture: { + timezone: expect.any(String), + }, + }, + // and an exception that is our ANR + exception: { + values: [ + { + type: 'ApplicationNotResponding', + value: 'Application Not Responding for at least 100 ms', + mechanism: { type: 'ANR' }, + stacktrace: { + frames: expect.arrayContaining([ + expect.objectContaining({ + colno: expect.any(Number), + lineno: expect.any(Number), + filename: expect.any(String), + function: '?', + in_app: true, + }), + expect.objectContaining({ + colno: expect.any(Number), + lineno: expect.any(Number), + filename: expect.any(String), + function: 'longWork', + in_app: true, + }), + ]), + }, + }, + ], + }, +}; + +const ANR_EVENT_WITHOUT_STACKTRACE = { + // Ensure we have context + contexts: { + device: { + arch: expect.any(String), + }, + app: { + app_start_time: expect.any(String), + }, + os: { + name: expect.any(String), + }, + culture: { + timezone: expect.any(String), + }, + }, + // and an exception that is our ANR + exception: { + values: [ + { + type: 'ApplicationNotResponding', + value: 'Application Not Responding for at least 100 ms', + mechanism: { type: 'ANR' }, + stacktrace: {}, + }, + ], + }, +}; + +const ANR_EVENT_WITH_SCOPE = { + ...ANR_EVENT, + user: { + email: 'person@home.com', + }, + breadcrumbs: expect.arrayContaining([ + { + timestamp: expect.any(Number), + message: 'important message!', + }, + ]), +}; + +const ANR_EVENT_WITH_DEBUG_META: Event = { + ...ANR_EVENT_WITH_SCOPE, + debug_meta: { + images: [ + { + type: 'sourcemap', + debug_id: 'aaaaaaaa-aaaa-4aaa-aaaa-aaaaaaaaaa', + code_file: expect.stringContaining('basic'), + }, + ], + }, +}; + +describe('should report ANR when event loop blocked', { timeout: 90_000 }, () => { + afterAll(() => { + cleanupChildProcesses(); + }); + + test('CJS', async () => { + await createRunner(__dirname, 'basic.js') + .withMockSentryServer() + .expect({ event: ANR_EVENT_WITH_DEBUG_META }) + .start() + .completed(); + }); + + test('ESM', async () => { + await createRunner(__dirname, 'basic.mjs') + .withMockSentryServer() + .expect({ event: ANR_EVENT_WITH_DEBUG_META }) + .start() + .completed(); + }); + + test('Custom appRootPath', async () => { + const ANR_EVENT_WITH_SPECIFIC_DEBUG_META: Event = { + ...ANR_EVENT_WITH_SCOPE, + debug_meta: { + images: [ + { + type: 'sourcemap', + debug_id: 'aaaaaaaa-aaaa-4aaa-aaaa-aaaaaaaaaa', + code_file: 'app:///app-path.mjs', + }, + ], + }, + }; + + await createRunner(__dirname, 'app-path.mjs') + .withMockSentryServer() + .expect({ event: ANR_EVENT_WITH_SPECIFIC_DEBUG_META }) + .start() + .completed(); + }); + + test('multiple events via maxAnrEvents', async () => { + await createRunner(__dirname, 'basic-multiple.mjs') + .withMockSentryServer() + .expect({ event: ANR_EVENT_WITH_DEBUG_META }) + .expect({ event: ANR_EVENT_WITH_DEBUG_META }) + .start() + .completed(); + }); + + test('blocked indefinitely', async () => { + await createRunner(__dirname, 'indefinite.mjs') + .withMockSentryServer() + .expect({ event: ANR_EVENT }) + .start() + .completed(); + }); + + test("With --inspect the debugger isn't used", async () => { + await createRunner(__dirname, 'basic.mjs') + .withMockSentryServer() + .withFlags('--inspect') + .expect({ event: ANR_EVENT_WITHOUT_STACKTRACE }) + .start() + .completed(); + }); + + test('should exit', async () => { + const runner = createRunner(__dirname, 'should-exit.js').start(); + + await new Promise(resolve => setTimeout(resolve, 5_000)); + + expect(runner.childHasExited()).toBe(true); + }); + + test('should exit forced', async () => { + const runner = createRunner(__dirname, 'should-exit-forced.js').start(); + + await new Promise(resolve => setTimeout(resolve, 5_000)); + + expect(runner.childHasExited()).toBe(true); + }); + + test('With session', async () => { + await createRunner(__dirname, 'basic-session.js') + .withMockSentryServer() + .unignore('session') + .expect({ + session: { + status: 'abnormal', + abnormal_mechanism: 'anr_foreground', + attrs: { + release: '1.0.0', + }, + }, + }) + .expect({ event: ANR_EVENT_WITH_SCOPE }) + .start() + .completed(); + }); + + test('from forked process', async () => { + await createRunner(__dirname, 'forker.js').expect({ event: ANR_EVENT_WITH_SCOPE }).start().completed(); + }); + + test('worker can be stopped and restarted', async () => { + await createRunner(__dirname, 'stop-and-start.js').expect({ event: ANR_EVENT_WITH_SCOPE }).start().completed(); + }); + + const EXPECTED_ISOLATED_EVENT = { + user: { + id: 5, + }, + exception: { + values: [ + { + type: 'ApplicationNotResponding', + value: 'Application Not Responding for at least 100 ms', + mechanism: { type: 'ANR' }, + stacktrace: { + frames: expect.arrayContaining([ + { + colno: expect.any(Number), + lineno: expect.any(Number), + filename: expect.stringMatching(/isolated.mjs$/), + function: 'longWork', + in_app: true, + }, + ]), + }, + }, + ], + }, + }; + + test('fetches correct isolated scope', async () => { + await createRunner(__dirname, 'isolated.mjs') + .withMockSentryServer() + .expect({ event: EXPECTED_ISOLATED_EVENT }) + .start() + .completed(); + }); +}); diff --git a/dev-packages/node-core-integration-tests/suites/breadcrumbs/process-thread/app.mjs b/dev-packages/node-core-integration-tests/suites/breadcrumbs/process-thread/app.mjs new file mode 100644 index 000000000000..7169b4824532 --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/breadcrumbs/process-thread/app.mjs @@ -0,0 +1,33 @@ +import * as Sentry from '@sentry/node-core'; +import { loggingTransport } from '@sentry-internal/node-integration-tests'; +import { spawn } from 'child_process'; +import { join } from 'path'; +import { Worker } from 'worker_threads'; +import { setupOtel } from '../../../utils/setupOtel.js'; + +const __dirname = new URL('.', import.meta.url).pathname; + +const client = Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', + integrations: [Sentry.childProcessIntegration({ captureWorkerErrors: false })], + transport: loggingTransport, +}); + +setupOtel(client); + +(async () => { + await new Promise(resolve => { + const child = spawn('sleep', ['a']); + child.on('error', resolve); + child.on('exit', resolve); + }); + + await new Promise(resolve => { + const worker = new Worker(join(__dirname, 'worker.mjs')); + worker.on('error', resolve); + worker.on('exit', resolve); + }); + + throw new Error('This is a test error'); +})(); diff --git a/dev-packages/node-core-integration-tests/suites/breadcrumbs/process-thread/test.ts b/dev-packages/node-core-integration-tests/suites/breadcrumbs/process-thread/test.ts new file mode 100644 index 000000000000..a3ae49da4808 --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/breadcrumbs/process-thread/test.ts @@ -0,0 +1,50 @@ +import type { Event } from '@sentry/core'; +import { afterAll, expect, test } from 'vitest'; +import { conditionalTest } from '../../../utils'; +import { cleanupChildProcesses, createRunner } from '../../../utils/runner'; + +const EVENT = { + // and an exception that is our ANR + exception: { + values: [ + { + type: 'Error', + value: 'This is a test error', + }, + ], + }, + breadcrumbs: [ + { + timestamp: expect.any(Number), + category: 'child_process', + message: "Child process exited with code '1'", + level: 'warning', + data: { + spawnfile: 'sleep', + }, + }, + { + timestamp: expect.any(Number), + category: 'worker_thread', + message: "Worker thread errored with 'Worker error'", + level: 'error', + data: { + threadId: expect.any(Number), + }, + }, + ], +}; + +conditionalTest({ min: 20 })('should capture process and thread breadcrumbs', () => { + afterAll(() => { + cleanupChildProcesses(); + }); + + test('ESM', async () => { + await createRunner(__dirname, 'app.mjs') + .withMockSentryServer() + .expect({ event: EVENT as Event }) + .start() + .completed(); + }); +}); diff --git a/dev-packages/node-core-integration-tests/suites/breadcrumbs/process-thread/worker.mjs b/dev-packages/node-core-integration-tests/suites/breadcrumbs/process-thread/worker.mjs new file mode 100644 index 000000000000..049063bd26b4 --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/breadcrumbs/process-thread/worker.mjs @@ -0,0 +1 @@ +throw new Error('Worker error'); diff --git a/dev-packages/node-core-integration-tests/suites/child-process/child.js b/dev-packages/node-core-integration-tests/suites/child-process/child.js new file mode 100644 index 000000000000..cb1937007297 --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/child-process/child.js @@ -0,0 +1,3 @@ +setTimeout(() => { + throw new Error('Test error'); +}, 1000); diff --git a/dev-packages/node-core-integration-tests/suites/child-process/child.mjs b/dev-packages/node-core-integration-tests/suites/child-process/child.mjs new file mode 100644 index 000000000000..cb1937007297 --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/child-process/child.mjs @@ -0,0 +1,3 @@ +setTimeout(() => { + throw new Error('Test error'); +}, 1000); diff --git a/dev-packages/node-core-integration-tests/suites/child-process/fork.js b/dev-packages/node-core-integration-tests/suites/child-process/fork.js new file mode 100644 index 000000000000..0cadad736d87 --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/child-process/fork.js @@ -0,0 +1,20 @@ +const Sentry = require('@sentry/node-core'); +const { setupOtel } = require('../../utils/setupOtel.js'); +const { loggingTransport } = require('@sentry-internal/node-integration-tests'); +const path = require('path'); +const { fork } = require('child_process'); + +const client = Sentry.init({ + debug: true, + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', + transport: loggingTransport, +}); + +setupOtel(client); + +fork(path.join(__dirname, 'child.mjs')); + +setTimeout(() => { + throw new Error('Exiting main process'); +}, 3000); diff --git a/dev-packages/node-core-integration-tests/suites/child-process/fork.mjs b/dev-packages/node-core-integration-tests/suites/child-process/fork.mjs new file mode 100644 index 000000000000..7aab2c78e6b6 --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/child-process/fork.mjs @@ -0,0 +1,22 @@ +import * as Sentry from '@sentry/node-core'; +import { loggingTransport } from '@sentry-internal/node-integration-tests'; +import { fork } from 'child_process'; +import * as path from 'path'; +import { setupOtel } from '../../utils/setupOtel.js'; + +const __dirname = new URL('.', import.meta.url).pathname; + +const client = Sentry.init({ + debug: true, + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', + transport: loggingTransport, +}); + +setupOtel(client); + +fork(path.join(__dirname, 'child.mjs')); + +setTimeout(() => { + throw new Error('Exiting main process'); +}, 3000); diff --git a/dev-packages/node-core-integration-tests/suites/child-process/test.ts b/dev-packages/node-core-integration-tests/suites/child-process/test.ts new file mode 100644 index 000000000000..798cd48d86d0 --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/child-process/test.ts @@ -0,0 +1,66 @@ +import type { Event } from '@sentry/core'; +import { afterAll, describe, expect, test } from 'vitest'; +import { conditionalTest } from '../../utils'; +import { cleanupChildProcesses, createRunner } from '../../utils/runner'; + +const WORKER_EVENT: Event = { + exception: { + values: [ + { + type: 'Error', + value: 'Test error', + mechanism: { + type: 'instrument', + handled: false, + data: { + threadId: expect.any(String), + }, + }, + }, + ], + }, +}; + +const CHILD_EVENT: Event = { + exception: { + values: [ + { + type: 'Error', + value: 'Exiting main process', + }, + ], + }, + breadcrumbs: [ + { + category: 'child_process', + message: "Child process exited with code '1'", + level: 'warning', + }, + ], +}; + +describe('should capture child process events', () => { + afterAll(() => { + cleanupChildProcesses(); + }); + + conditionalTest({ min: 20 })('worker', () => { + test('ESM', async () => { + await createRunner(__dirname, 'worker.mjs').expect({ event: WORKER_EVENT }).start().completed(); + }); + + test('CJS', async () => { + await createRunner(__dirname, 'worker.js').expect({ event: WORKER_EVENT }).start().completed(); + }); + }); + + conditionalTest({ min: 20 })('fork', () => { + test('ESM', async () => { + await createRunner(__dirname, 'fork.mjs').expect({ event: CHILD_EVENT }).start().completed(); + }); + + test('CJS', async () => { + await createRunner(__dirname, 'fork.js').expect({ event: CHILD_EVENT }).start().completed(); + }); + }); +}); diff --git a/dev-packages/node-core-integration-tests/suites/child-process/worker.js b/dev-packages/node-core-integration-tests/suites/child-process/worker.js new file mode 100644 index 000000000000..34818297cff9 --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/child-process/worker.js @@ -0,0 +1,20 @@ +const Sentry = require('@sentry/node-core'); +const { setupOtel } = require('../../utils/setupOtel.js'); +const { loggingTransport } = require('@sentry-internal/node-integration-tests'); +const path = require('path'); +const { Worker } = require('worker_threads'); + +const client = Sentry.init({ + debug: true, + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', + transport: loggingTransport, +}); + +setupOtel(client); + +new Worker(path.join(__dirname, 'child.js')); + +setTimeout(() => { + process.exit(); +}, 3000); diff --git a/dev-packages/node-core-integration-tests/suites/child-process/worker.mjs b/dev-packages/node-core-integration-tests/suites/child-process/worker.mjs new file mode 100644 index 000000000000..1c2037ba79e0 --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/child-process/worker.mjs @@ -0,0 +1,22 @@ +import * as Sentry from '@sentry/node-core'; +import { loggingTransport } from '@sentry-internal/node-integration-tests'; +import * as path from 'path'; +import { Worker } from 'worker_threads'; +import { setupOtel } from '../../utils/setupOtel.js'; + +const __dirname = new URL('.', import.meta.url).pathname; + +const client = Sentry.init({ + debug: true, + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', + transport: loggingTransport, +}); + +setupOtel(client); + +new Worker(path.join(__dirname, 'child.mjs')); + +setTimeout(() => { + process.exit(); +}, 3000); diff --git a/dev-packages/node-core-integration-tests/suites/client-reports/drop-reasons/before-send/scenario.ts b/dev-packages/node-core-integration-tests/suites/client-reports/drop-reasons/before-send/scenario.ts new file mode 100644 index 000000000000..fab6c8f0afdf --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/client-reports/drop-reasons/before-send/scenario.ts @@ -0,0 +1,26 @@ +import * as Sentry from '@sentry/node-core'; +import { loggingTransport } from '@sentry-internal/node-integration-tests'; +import { setupOtel } from '../../../../utils/setupOtel'; + +// eslint-disable-next-line @typescript-eslint/no-floating-promises +(async () => { + const client = Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + transport: loggingTransport, + beforeSend(event) { + return !event.type ? null : event; + }, + }); + + setupOtel(client); + + Sentry.captureException(new Error('this should get dropped by the event processor')); + + await Sentry.flush(); + + Sentry.captureException(new Error('this should get dropped by the event processor')); + Sentry.captureException(new Error('this should get dropped by the event processor')); + + // eslint-disable-next-line @typescript-eslint/no-floating-promises + Sentry.flush(); +})(); diff --git a/dev-packages/node-core-integration-tests/suites/client-reports/drop-reasons/before-send/test.ts b/dev-packages/node-core-integration-tests/suites/client-reports/drop-reasons/before-send/test.ts new file mode 100644 index 000000000000..73a40fd88d17 --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/client-reports/drop-reasons/before-send/test.ts @@ -0,0 +1,35 @@ +import { afterAll, test } from 'vitest'; +import { cleanupChildProcesses, createRunner } from '../../../../utils/runner'; + +afterAll(() => { + cleanupChildProcesses(); +}); + +test('should record client report for beforeSend', async () => { + await createRunner(__dirname, 'scenario.ts') + .unignore('client_report') + .expect({ + client_report: { + discarded_events: [ + { + category: 'error', + quantity: 1, + reason: 'before_send', + }, + ], + }, + }) + .expect({ + client_report: { + discarded_events: [ + { + category: 'error', + quantity: 2, + reason: 'before_send', + }, + ], + }, + }) + .start() + .completed(); +}); diff --git a/dev-packages/node-core-integration-tests/suites/client-reports/drop-reasons/event-processors/scenario.ts b/dev-packages/node-core-integration-tests/suites/client-reports/drop-reasons/event-processors/scenario.ts new file mode 100644 index 000000000000..3e50b33f0626 --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/client-reports/drop-reasons/event-processors/scenario.ts @@ -0,0 +1,27 @@ +import * as Sentry from '@sentry/node-core'; +import { loggingTransport } from '@sentry-internal/node-integration-tests'; +import { setupOtel } from '../../../../utils/setupOtel'; + +// eslint-disable-next-line @typescript-eslint/no-floating-promises +(async () => { + const client = Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + transport: loggingTransport, + }); + + setupOtel(client); + + Sentry.addEventProcessor(event => { + return !event.type ? null : event; + }); + + Sentry.captureException(new Error('this should get dropped by the event processor')); + + await Sentry.flush(); + + Sentry.captureException(new Error('this should get dropped by the event processor')); + Sentry.captureException(new Error('this should get dropped by the event processor')); + + // eslint-disable-next-line @typescript-eslint/no-floating-promises + Sentry.flush(); +})(); diff --git a/dev-packages/node-core-integration-tests/suites/client-reports/drop-reasons/event-processors/test.ts b/dev-packages/node-core-integration-tests/suites/client-reports/drop-reasons/event-processors/test.ts new file mode 100644 index 000000000000..4e236e375c40 --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/client-reports/drop-reasons/event-processors/test.ts @@ -0,0 +1,35 @@ +import { afterAll, test } from 'vitest'; +import { cleanupChildProcesses, createRunner } from '../../../../utils/runner'; + +afterAll(() => { + cleanupChildProcesses(); +}); + +test('should record client report for event processors', async () => { + await createRunner(__dirname, 'scenario.ts') + .unignore('client_report') + .expect({ + client_report: { + discarded_events: [ + { + category: 'error', + quantity: 1, + reason: 'event_processor', + }, + ], + }, + }) + .expect({ + client_report: { + discarded_events: [ + { + category: 'error', + quantity: 2, + reason: 'event_processor', + }, + ], + }, + }) + .start() + .completed(); +}); diff --git a/dev-packages/node-core-integration-tests/suites/client-reports/periodic-send/scenario.ts b/dev-packages/node-core-integration-tests/suites/client-reports/periodic-send/scenario.ts new file mode 100644 index 000000000000..3a7a1dd32181 --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/client-reports/periodic-send/scenario.ts @@ -0,0 +1,16 @@ +import * as Sentry from '@sentry/node-core'; +import { loggingTransport } from '@sentry-internal/node-integration-tests'; +import { setupOtel } from '../../../utils/setupOtel'; + +const client = Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + transport: loggingTransport, + clientReportFlushInterval: 5000, + beforeSend(event) { + return !event.type ? null : event; + }, +}); + +setupOtel(client); + +Sentry.captureException(new Error('this should get dropped by before send')); diff --git a/dev-packages/node-core-integration-tests/suites/client-reports/periodic-send/test.ts b/dev-packages/node-core-integration-tests/suites/client-reports/periodic-send/test.ts new file mode 100644 index 000000000000..69775219b784 --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/client-reports/periodic-send/test.ts @@ -0,0 +1,24 @@ +import { afterAll, test } from 'vitest'; +import { cleanupChildProcesses, createRunner } from '../../../utils/runner'; + +afterAll(() => { + cleanupChildProcesses(); +}); + +test('should flush client reports automatically after the timeout interval', async () => { + await createRunner(__dirname, 'scenario.ts') + .unignore('client_report') + .expect({ + client_report: { + discarded_events: [ + { + category: 'error', + quantity: 1, + reason: 'before_send', + }, + ], + }, + }) + .start() + .completed(); +}); diff --git a/dev-packages/node-core-integration-tests/suites/contextLines/filename-with-spaces/instrument.mjs b/dev-packages/node-core-integration-tests/suites/contextLines/filename-with-spaces/instrument.mjs new file mode 100644 index 000000000000..0aade82dbf23 --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/contextLines/filename-with-spaces/instrument.mjs @@ -0,0 +1,11 @@ +import * as Sentry from '@sentry/node-core'; +import { loggingTransport } from '@sentry-internal/node-integration-tests'; +import { setupOtel } from '../../../utils/setupOtel.js'; + +const client = Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', + transport: loggingTransport, +}); + +setupOtel(client); diff --git a/dev-packages/node-core-integration-tests/suites/contextLines/filename-with-spaces/scenario with space.cjs b/dev-packages/node-core-integration-tests/suites/contextLines/filename-with-spaces/scenario with space.cjs new file mode 100644 index 000000000000..41618eb3fee5 --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/contextLines/filename-with-spaces/scenario with space.cjs @@ -0,0 +1,12 @@ +const Sentry = require('@sentry/node'); +const { loggingTransport } = require('@sentry-internal/node-integration-tests'); + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', + transport: loggingTransport, +}); + +Sentry.captureException(new Error('Test Error')); + +// some more post context diff --git a/dev-packages/node-core-integration-tests/suites/contextLines/filename-with-spaces/scenario with space.mjs b/dev-packages/node-core-integration-tests/suites/contextLines/filename-with-spaces/scenario with space.mjs new file mode 100644 index 000000000000..e3139401e5e2 --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/contextLines/filename-with-spaces/scenario with space.mjs @@ -0,0 +1,5 @@ +import * as Sentry from '@sentry/node-core'; + +Sentry.captureException(new Error('Test Error')); + +// some more post context diff --git a/dev-packages/node-core-integration-tests/suites/contextLines/filename-with-spaces/test.ts b/dev-packages/node-core-integration-tests/suites/contextLines/filename-with-spaces/test.ts new file mode 100644 index 000000000000..c765e4b541f2 --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/contextLines/filename-with-spaces/test.ts @@ -0,0 +1,85 @@ +import { join } from 'path'; +import { describe, expect, test } from 'vitest'; +import { createRunner } from '../../../utils/runner'; + +describe('ContextLines integration in ESM', () => { + test('reads encoded context lines from filenames with spaces', async () => { + expect.assertions(1); + const instrumentPath = join(__dirname, 'instrument.mjs'); + + await createRunner(__dirname, 'scenario with space.mjs') + .withInstrument(instrumentPath) + .expect({ + event: { + exception: { + values: [ + { + value: 'Test Error', + stacktrace: { + frames: expect.arrayContaining([ + { + filename: expect.stringMatching(/\/scenario with space.mjs$/), + context_line: "Sentry.captureException(new Error('Test Error'));", + pre_context: ["import * as Sentry from '@sentry/node-core';", ''], + post_context: ['', '// some more post context'], + colno: 25, + lineno: 3, + function: '?', + in_app: true, + module: 'scenario with space', + }, + ]), + }, + }, + ], + }, + }, + }) + .start() + .completed(); + }); +}); + +describe('ContextLines integration in CJS', () => { + test('reads context lines from filenames with spaces', async () => { + expect.assertions(1); + + await createRunner(__dirname, 'scenario with space.cjs') + .expect({ + event: { + exception: { + values: [ + { + value: 'Test Error', + stacktrace: { + frames: expect.arrayContaining([ + { + filename: expect.stringMatching(/\/scenario with space.cjs$/), + context_line: "Sentry.captureException(new Error('Test Error'));", + pre_context: [ + '', + 'Sentry.init({', + " dsn: 'https://public@dsn.ingest.sentry.io/1337',", + " release: '1.0',", + ' transport: loggingTransport,', + '});', + '', + ], + post_context: ['', '// some more post context'], + colno: 25, + lineno: 10, + function: 'Object.?', + in_app: true, + module: 'scenario with space', + }, + ]), + }, + }, + ], + }, + }, + }) + .start() + .completed(); + }); +}); diff --git a/dev-packages/node-core-integration-tests/suites/contextLines/memory-leak/nested-file.ts b/dev-packages/node-core-integration-tests/suites/contextLines/memory-leak/nested-file.ts new file mode 100644 index 000000000000..bd76720a6285 --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/contextLines/memory-leak/nested-file.ts @@ -0,0 +1,5 @@ +import * as Sentry from '@sentry/node-core'; + +export function captureException(i: number): void { + Sentry.captureException(new Error(`error in loop ${i}`)); +} diff --git a/dev-packages/node-core-integration-tests/suites/contextLines/memory-leak/other-file.ts b/dev-packages/node-core-integration-tests/suites/contextLines/memory-leak/other-file.ts new file mode 100644 index 000000000000..c48fae3e2e2e --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/contextLines/memory-leak/other-file.ts @@ -0,0 +1,7 @@ +import { captureException } from './nested-file'; + +export function runSentry(): void { + for (let i = 0; i < 10; i++) { + captureException(i); + } +} diff --git a/dev-packages/node-core-integration-tests/suites/contextLines/memory-leak/scenario.ts b/dev-packages/node-core-integration-tests/suites/contextLines/memory-leak/scenario.ts new file mode 100644 index 000000000000..cf36c8a2f613 --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/contextLines/memory-leak/scenario.ts @@ -0,0 +1,32 @@ +import { execSync } from 'node:child_process'; +import * as path from 'node:path'; +import * as Sentry from '@sentry/node-core'; +import { loggingTransport } from '@sentry-internal/node-integration-tests'; +import { setupOtel } from '../../../utils/setupOtel'; + +const client = Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', + transport: loggingTransport, +}); + +setupOtel(client); + +import { runSentry } from './other-file'; + +runSentry(); + +const lsofOutput = execSync(`lsof -p ${process.pid}`, { encoding: 'utf8' }); +const lsofTable = lsofOutput.split('\n'); +const mainPath = __dirname.replace(`${path.sep}suites${path.sep}contextLines${path.sep}memory-leak`, ''); +const numberOfLsofEntriesWithMainPath = lsofTable.filter(entry => entry.includes(mainPath)); + +// There should only be a single entry with the main path, otherwise we are leaking file handles from the +// context lines integration. +if (numberOfLsofEntriesWithMainPath.length > 1) { + // eslint-disable-next-line no-console + console.error('Leaked file handles detected'); + // eslint-disable-next-line no-console + console.error(lsofTable); + process.exit(1); +} diff --git a/dev-packages/node-core-integration-tests/suites/contextLines/memory-leak/test.ts b/dev-packages/node-core-integration-tests/suites/contextLines/memory-leak/test.ts new file mode 100644 index 000000000000..1a5170c05fe7 --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/contextLines/memory-leak/test.ts @@ -0,0 +1,18 @@ +import { afterAll, describe, test } from 'vitest'; +import { cleanupChildProcesses, createRunner } from '../../../utils/runner'; + +describe('ContextLines integration in CJS', () => { + afterAll(() => { + cleanupChildProcesses(); + }); + + // Regression test for: https://github.com/getsentry/sentry-javascript/issues/14892 + test('does not leak open file handles', async () => { + await createRunner(__dirname, 'scenario.ts') + .expectN(10, { + event: {}, + }) + .start() + .completed(); + }); +}); diff --git a/dev-packages/node-core-integration-tests/suites/cron/cron/scenario.ts b/dev-packages/node-core-integration-tests/suites/cron/cron/scenario.ts new file mode 100644 index 000000000000..4ddd9f115189 --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/cron/cron/scenario.ts @@ -0,0 +1,33 @@ +import * as Sentry from '@sentry/node-core'; +import { loggingTransport } from '@sentry-internal/node-integration-tests'; +import { CronJob } from 'cron'; +import { setupOtel } from '../../../utils/setupOtel'; + +const client = Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', + transport: loggingTransport, +}); + +setupOtel(client); + +const CronJobWithCheckIn = Sentry.cron.instrumentCron(CronJob, 'my-cron-job'); + +let closeNext = false; + +const cron = new CronJobWithCheckIn('* * * * * *', () => { + if (closeNext) { + cron.stop(); + throw new Error('Error in cron job'); + } + + // eslint-disable-next-line no-console + console.log('You will see this message every second'); + closeNext = true; +}); + +cron.start(); + +setTimeout(() => { + process.exit(); +}, 5000); diff --git a/dev-packages/node-core-integration-tests/suites/cron/cron/test.ts b/dev-packages/node-core-integration-tests/suites/cron/cron/test.ts new file mode 100644 index 000000000000..8b9fdfd5c593 --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/cron/cron/test.ts @@ -0,0 +1,77 @@ +import { afterAll, expect, test } from 'vitest'; +import { cleanupChildProcesses, createRunner } from '../../../utils/runner'; + +afterAll(() => { + cleanupChildProcesses(); +}); + +test('cron instrumentation', async () => { + await createRunner(__dirname, 'scenario.ts') + .expect({ + check_in: { + check_in_id: expect.any(String), + monitor_slug: 'my-cron-job', + status: 'in_progress', + release: '1.0', + monitor_config: { schedule: { type: 'crontab', value: '* * * * * *' } }, + contexts: { + trace: { + trace_id: expect.stringMatching(/[a-f0-9]{32}/), + span_id: expect.stringMatching(/[a-f0-9]{16}/), + }, + }, + }, + }) + .expect({ + check_in: { + check_in_id: expect.any(String), + monitor_slug: 'my-cron-job', + status: 'ok', + release: '1.0', + duration: expect.any(Number), + contexts: { + trace: { + trace_id: expect.stringMatching(/[a-f0-9]{32}/), + span_id: expect.stringMatching(/[a-f0-9]{16}/), + }, + }, + }, + }) + .expect({ + check_in: { + check_in_id: expect.any(String), + monitor_slug: 'my-cron-job', + status: 'in_progress', + release: '1.0', + monitor_config: { schedule: { type: 'crontab', value: '* * * * * *' } }, + contexts: { + trace: { + trace_id: expect.stringMatching(/[a-f0-9]{32}/), + span_id: expect.stringMatching(/[a-f0-9]{16}/), + }, + }, + }, + }) + .expect({ + check_in: { + check_in_id: expect.any(String), + monitor_slug: 'my-cron-job', + status: 'error', + release: '1.0', + duration: expect.any(Number), + contexts: { + trace: { + trace_id: expect.stringMatching(/[a-f0-9]{32}/), + span_id: expect.stringMatching(/[a-f0-9]{16}/), + }, + }, + }, + }) + .expect({ + event: { + exception: { values: [{ type: 'Error', value: 'Error in cron job' }] }, + }, + }) + .start() + .completed(); +}); diff --git a/dev-packages/node-core-integration-tests/suites/cron/node-cron/scenario.ts b/dev-packages/node-core-integration-tests/suites/cron/node-cron/scenario.ts new file mode 100644 index 000000000000..818bf7b63871 --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/cron/node-cron/scenario.ts @@ -0,0 +1,39 @@ +import * as Sentry from '@sentry/node-core'; +import { loggingTransport } from '@sentry-internal/node-integration-tests'; +import * as cron from 'node-cron'; +import { setupOtel } from '../../../utils/setupOtel'; + +const client = Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', + transport: loggingTransport, +}); + +setupOtel(client); + +const cronWithCheckIn = Sentry.cron.instrumentNodeCron(cron); + +let closeNext = false; + +const task = cronWithCheckIn.schedule( + '* * * * * *', + () => { + if (closeNext) { + // https://github.com/node-cron/node-cron/issues/317 + setImmediate(() => { + task.stop(); + }); + + throw new Error('Error in cron job'); + } + + // eslint-disable-next-line no-console + console.log('You will see this message every second'); + closeNext = true; + }, + { name: 'my-cron-job' }, +); + +setTimeout(() => { + process.exit(); +}, 5000); diff --git a/dev-packages/node-core-integration-tests/suites/cron/node-cron/test.ts b/dev-packages/node-core-integration-tests/suites/cron/node-cron/test.ts new file mode 100644 index 000000000000..1c5fa515e208 --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/cron/node-cron/test.ts @@ -0,0 +1,77 @@ +import { afterAll, expect, test } from 'vitest'; +import { cleanupChildProcesses, createRunner } from '../../../utils/runner'; + +afterAll(() => { + cleanupChildProcesses(); +}); + +test('node-cron instrumentation', async () => { + await createRunner(__dirname, 'scenario.ts') + .expect({ + check_in: { + check_in_id: expect.any(String), + monitor_slug: 'my-cron-job', + status: 'in_progress', + release: '1.0', + monitor_config: { schedule: { type: 'crontab', value: '* * * * * *' } }, + contexts: { + trace: { + trace_id: expect.stringMatching(/[a-f0-9]{32}/), + span_id: expect.stringMatching(/[a-f0-9]{16}/), + }, + }, + }, + }) + .expect({ + check_in: { + check_in_id: expect.any(String), + monitor_slug: 'my-cron-job', + status: 'ok', + release: '1.0', + duration: expect.any(Number), + contexts: { + trace: { + trace_id: expect.stringMatching(/[a-f0-9]{32}/), + span_id: expect.stringMatching(/[a-f0-9]{16}/), + }, + }, + }, + }) + .expect({ + check_in: { + check_in_id: expect.any(String), + monitor_slug: 'my-cron-job', + status: 'in_progress', + release: '1.0', + monitor_config: { schedule: { type: 'crontab', value: '* * * * * *' } }, + contexts: { + trace: { + trace_id: expect.stringMatching(/[a-f0-9]{32}/), + span_id: expect.stringMatching(/[a-f0-9]{16}/), + }, + }, + }, + }) + .expect({ + check_in: { + check_in_id: expect.any(String), + monitor_slug: 'my-cron-job', + status: 'error', + release: '1.0', + duration: expect.any(Number), + contexts: { + trace: { + trace_id: expect.stringMatching(/[a-f0-9]{32}/), + span_id: expect.stringMatching(/[a-f0-9]{16}/), + }, + }, + }, + }) + .expect({ + event: { + exception: { values: [{ type: 'Error', value: 'Error in cron job' }] }, + }, + }) + .start() + .completed(); +}); diff --git a/dev-packages/node-core-integration-tests/suites/cron/node-schedule/scenario.ts b/dev-packages/node-core-integration-tests/suites/cron/node-schedule/scenario.ts new file mode 100644 index 000000000000..65f4fc9ab49a --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/cron/node-schedule/scenario.ts @@ -0,0 +1,31 @@ +import * as Sentry from '@sentry/node-core'; +import { loggingTransport } from '@sentry-internal/node-integration-tests'; +import * as schedule from 'node-schedule'; +import { setupOtel } from '../../../utils/setupOtel'; + +const client = Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', + transport: loggingTransport, +}); + +setupOtel(client); + +const scheduleWithCheckIn = Sentry.cron.instrumentNodeSchedule(schedule); + +let closeNext = false; + +const job = scheduleWithCheckIn.scheduleJob('my-cron-job', '* * * * * *', () => { + if (closeNext) { + job.cancel(); + throw new Error('Error in cron job'); + } + + // eslint-disable-next-line no-console + console.log('You will see this message every second'); + closeNext = true; +}); + +setTimeout(() => { + process.exit(); +}, 5000); diff --git a/dev-packages/node-core-integration-tests/suites/cron/node-schedule/test.ts b/dev-packages/node-core-integration-tests/suites/cron/node-schedule/test.ts new file mode 100644 index 000000000000..a2019253203f --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/cron/node-schedule/test.ts @@ -0,0 +1,77 @@ +import { afterAll, expect, test } from 'vitest'; +import { cleanupChildProcesses, createRunner } from '../../../utils/runner'; + +afterAll(() => { + cleanupChildProcesses(); +}); + +test('node-schedule instrumentation', async () => { + await createRunner(__dirname, 'scenario.ts') + .expect({ + check_in: { + check_in_id: expect.any(String), + monitor_slug: 'my-cron-job', + status: 'in_progress', + release: '1.0', + monitor_config: { schedule: { type: 'crontab', value: '* * * * * *' } }, + contexts: { + trace: { + trace_id: expect.stringMatching(/[a-f0-9]{32}/), + span_id: expect.stringMatching(/[a-f0-9]{16}/), + }, + }, + }, + }) + .expect({ + check_in: { + check_in_id: expect.any(String), + monitor_slug: 'my-cron-job', + status: 'ok', + release: '1.0', + duration: expect.any(Number), + contexts: { + trace: { + trace_id: expect.stringMatching(/[a-f0-9]{32}/), + span_id: expect.stringMatching(/[a-f0-9]{16}/), + }, + }, + }, + }) + .expect({ + check_in: { + check_in_id: expect.any(String), + monitor_slug: 'my-cron-job', + status: 'in_progress', + release: '1.0', + monitor_config: { schedule: { type: 'crontab', value: '* * * * * *' } }, + contexts: { + trace: { + trace_id: expect.stringMatching(/[a-f0-9]{32}/), + span_id: expect.stringMatching(/[a-f0-9]{16}/), + }, + }, + }, + }) + .expect({ + check_in: { + check_in_id: expect.any(String), + monitor_slug: 'my-cron-job', + status: 'error', + release: '1.0', + duration: expect.any(Number), + contexts: { + trace: { + trace_id: expect.stringMatching(/[a-f0-9]{32}/), + span_id: expect.stringMatching(/[a-f0-9]{16}/), + }, + }, + }, + }) + .expect({ + event: { + exception: { values: [{ type: 'Error', value: 'Error in cron job' }] }, + }, + }) + .start() + .completed(); +}); diff --git a/dev-packages/node-core-integration-tests/suites/esm/import-in-the-middle/app.mjs b/dev-packages/node-core-integration-tests/suites/esm/import-in-the-middle/app.mjs new file mode 100644 index 000000000000..180eedbab9a5 --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/esm/import-in-the-middle/app.mjs @@ -0,0 +1,24 @@ +import * as Sentry from '@sentry/node-core'; +import { loggingTransport } from '@sentry-internal/node-integration-tests'; +import * as iitm from 'import-in-the-middle'; +import { setupOtel } from '../../../utils/setupOtel.js'; + +new iitm.Hook((_, name) => { + if (name !== 'http') { + throw new Error(`'http' should be the only hooked modules but we just hooked '${name}'`); + } +}); + +const client = Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', + transport: loggingTransport, +}); + +setupOtel(client); + +(async () => { + await import('./sub-module.mjs'); + await import('http'); + await import('os'); +})(); diff --git a/dev-packages/node-core-integration-tests/suites/esm/import-in-the-middle/sub-module.mjs b/dev-packages/node-core-integration-tests/suites/esm/import-in-the-middle/sub-module.mjs new file mode 100644 index 000000000000..9940c57857eb --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/esm/import-in-the-middle/sub-module.mjs @@ -0,0 +1,2 @@ +// eslint-disable-next-line no-console +console.assert(true); diff --git a/dev-packages/node-core-integration-tests/suites/esm/import-in-the-middle/test.ts b/dev-packages/node-core-integration-tests/suites/esm/import-in-the-middle/test.ts new file mode 100644 index 000000000000..99dea0e9193a --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/esm/import-in-the-middle/test.ts @@ -0,0 +1,15 @@ +import { spawnSync } from 'child_process'; +import { join } from 'path'; +import { afterAll, describe, expect, test } from 'vitest'; +import { cleanupChildProcesses } from '../../../utils/runner'; + +afterAll(() => { + cleanupChildProcesses(); +}); + +describe('import-in-the-middle', () => { + test('should only instrument modules that we have instrumentation for', () => { + const result = spawnSync('node', [join(__dirname, 'app.mjs')], { encoding: 'utf-8' }); + expect(result.stderr).not.toMatch('should be the only hooked modules but we just hooked'); + }); +}); diff --git a/dev-packages/node-core-integration-tests/suites/esm/modules-integration/app.mjs b/dev-packages/node-core-integration-tests/suites/esm/modules-integration/app.mjs new file mode 100644 index 000000000000..ab1566c7b139 --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/esm/modules-integration/app.mjs @@ -0,0 +1,12 @@ +import * as Sentry from '@sentry/node-core'; +import { loggingTransport } from '@sentry-internal/node-integration-tests'; +import { setupOtel } from '../../../utils/setupOtel.js'; + +const client = Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', + integrations: [Sentry.modulesIntegration()], + transport: loggingTransport, +}); + +setupOtel(client); diff --git a/dev-packages/node-core-integration-tests/suites/esm/modules-integration/test.ts b/dev-packages/node-core-integration-tests/suites/esm/modules-integration/test.ts new file mode 100644 index 000000000000..94995aedb91f --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/esm/modules-integration/test.ts @@ -0,0 +1,12 @@ +import { afterAll, describe, test } from 'vitest'; +import { cleanupChildProcesses, createRunner } from '../../../utils/runner'; + +afterAll(() => { + cleanupChildProcesses(); +}); + +describe('modulesIntegration', () => { + test('does not crash ESM setups', async () => { + await createRunner(__dirname, 'app.mjs').ensureNoErrorOutput().start().completed(); + }); +}); diff --git a/dev-packages/node-core-integration-tests/suites/esm/warn-esm/server.js b/dev-packages/node-core-integration-tests/suites/esm/warn-esm/server.js new file mode 100644 index 000000000000..fc6c1aaa75f4 --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/esm/warn-esm/server.js @@ -0,0 +1,22 @@ +const { loggingTransport } = require('@sentry-internal/node-integration-tests'); +const Sentry = require('@sentry/node-core'); +const { setupOtel } = require('../../../utils/setupOtel.js'); + +const client = Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', + transport: loggingTransport, +}); + +setupOtel(client); + +const { startExpressServerAndSendPortToRunner } = require('@sentry-internal/node-integration-tests'); +const express = require('express'); + +const app = express(); + +app.get('/test/success', (req, res) => { + res.send({ response: 'response 3' }); +}); + +startExpressServerAndSendPortToRunner(app); diff --git a/dev-packages/node-core-integration-tests/suites/esm/warn-esm/server.mjs b/dev-packages/node-core-integration-tests/suites/esm/warn-esm/server.mjs new file mode 100644 index 000000000000..b02456a34f4e --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/esm/warn-esm/server.mjs @@ -0,0 +1,20 @@ +import * as Sentry from '@sentry/node-core'; +import { loggingTransport, startExpressServerAndSendPortToRunner } from '@sentry-internal/node-integration-tests'; +import express from 'express'; +import { setupOtel } from '../../../utils/setupOtel.js'; + +const client = Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', + transport: loggingTransport, +}); + +setupOtel(client); + +const app = express(); + +app.get('/test/success', (req, res) => { + res.send({ response: 'response 3' }); +}); + +startExpressServerAndSendPortToRunner(app); diff --git a/dev-packages/node-core-integration-tests/suites/esm/warn-esm/test.ts b/dev-packages/node-core-integration-tests/suites/esm/warn-esm/test.ts new file mode 100644 index 000000000000..18eebdab6e85 --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/esm/warn-esm/test.ts @@ -0,0 +1,42 @@ +import { afterAll, expect, test } from 'vitest'; +import { cleanupChildProcesses, createRunner } from '../../../utils/runner'; + +afterAll(() => { + cleanupChildProcesses(); +}); + +const esmWarning = `[Sentry] You are using Node.js v${process.versions.node} in ESM mode ("import syntax"). The Sentry Node.js SDK is not compatible with ESM in Node.js versions before 18.19.0 or before 20.6.0. Please either build your application with CommonJS ("require() syntax"), or upgrade your Node.js version.`; + +test("warns if using ESM on Node.js versions that don't support `register()`", async () => { + const nodeMajorVersion = Number(process.versions.node.split('.')[0]); + if (nodeMajorVersion >= 18) { + return; + } + + const runner = createRunner(__dirname, 'server.mjs').ignore('event').start(); + + await runner.makeRequest('get', '/test/success'); + + expect(runner.getLogs()).toContain(esmWarning); +}); + +test('does not warn if using ESM on Node.js versions that support `register()`', async () => { + const nodeMajorVersion = Number(process.versions.node.split('.')[0]); + if (nodeMajorVersion < 18) { + return; + } + + const runner = createRunner(__dirname, 'server.mjs').ignore('event').start(); + + await runner.makeRequest('get', '/test/success'); + + expect(runner.getLogs()).not.toContain(esmWarning); +}); + +test('does not warn if using CJS', async () => { + const runner = createRunner(__dirname, 'server.js').ignore('event').start(); + + await runner.makeRequest('get', '/test/success'); + + expect(runner.getLogs()).not.toContain(esmWarning); +}); diff --git a/dev-packages/node-core-integration-tests/suites/featureFlags/featureFlagsIntegration/onError/basic/scenario.ts b/dev-packages/node-core-integration-tests/suites/featureFlags/featureFlagsIntegration/onError/basic/scenario.ts new file mode 100644 index 000000000000..5a89f59e17b4 --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/featureFlags/featureFlagsIntegration/onError/basic/scenario.ts @@ -0,0 +1,22 @@ +import { _INTERNAL_FLAG_BUFFER_SIZE as FLAG_BUFFER_SIZE } from '@sentry/core'; +import * as Sentry from '@sentry/node-core'; +import { loggingTransport } from '@sentry-internal/node-integration-tests'; +import { setupOtel } from '../../../../../utils/setupOtel'; + +const client = Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + sampleRate: 1.0, + transport: loggingTransport, + integrations: [Sentry.featureFlagsIntegration()], +}); + +setupOtel(client); + +const flagsIntegration = Sentry.getClient()?.getIntegrationByName('FeatureFlags'); +for (let i = 1; i <= FLAG_BUFFER_SIZE; i++) { + flagsIntegration?.addFeatureFlag(`feat${i}`, false); +} +flagsIntegration?.addFeatureFlag(`feat${FLAG_BUFFER_SIZE + 1}`, true); // eviction +flagsIntegration?.addFeatureFlag('feat3', true); // update + +throw new Error('Test error'); diff --git a/dev-packages/node-core-integration-tests/suites/featureFlags/featureFlagsIntegration/onError/basic/test.ts b/dev-packages/node-core-integration-tests/suites/featureFlags/featureFlagsIntegration/onError/basic/test.ts new file mode 100644 index 000000000000..74ff1c125b45 --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/featureFlags/featureFlagsIntegration/onError/basic/test.ts @@ -0,0 +1,31 @@ +import { _INTERNAL_FLAG_BUFFER_SIZE as FLAG_BUFFER_SIZE } from '@sentry/core'; +import { afterAll, test } from 'vitest'; +import { cleanupChildProcesses, createRunner } from '../../../../../utils/runner'; + +afterAll(() => { + cleanupChildProcesses(); +}); + +test('Flags captured on error with eviction, update, and no async tasks', async () => { + // Based on scenario.ts. + const expectedFlags = [{ flag: 'feat2', result: false }]; + for (let i = 4; i <= FLAG_BUFFER_SIZE; i++) { + expectedFlags.push({ flag: `feat${i}`, result: false }); + } + expectedFlags.push({ flag: `feat${FLAG_BUFFER_SIZE + 1}`, result: true }); + expectedFlags.push({ flag: 'feat3', result: true }); + + await createRunner(__dirname, 'scenario.ts') + .expect({ + event: { + exception: { values: [{ type: 'Error', value: 'Test error' }] }, + contexts: { + flags: { + values: expectedFlags, + }, + }, + }, + }) + .start() + .completed(); +}); diff --git a/dev-packages/node-core-integration-tests/suites/featureFlags/featureFlagsIntegration/onError/withScope/scenario.ts b/dev-packages/node-core-integration-tests/suites/featureFlags/featureFlagsIntegration/onError/withScope/scenario.ts new file mode 100644 index 000000000000..f45c089cdca2 --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/featureFlags/featureFlagsIntegration/onError/withScope/scenario.ts @@ -0,0 +1,33 @@ +import * as Sentry from '@sentry/node-core'; +import { loggingTransport } from '@sentry-internal/node-integration-tests'; +import { setupOtel } from '../../../../../utils/setupOtel'; + +const flagsIntegration = Sentry.featureFlagsIntegration(); + +const client = Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + sampleRate: 1.0, + transport: loggingTransport, + integrations: [flagsIntegration], +}); + +setupOtel(client); + +async function run(): Promise { + flagsIntegration.addFeatureFlag('shared', true); + + Sentry.withScope(() => { + flagsIntegration.addFeatureFlag('forked', true); + flagsIntegration.addFeatureFlag('shared', false); + Sentry.captureException(new Error('Error in forked scope')); + }); + + await Sentry.flush(); + + flagsIntegration.addFeatureFlag('main', true); + + throw new Error('Error in main scope'); +} + +// eslint-disable-next-line @typescript-eslint/no-floating-promises +run(); diff --git a/dev-packages/node-core-integration-tests/suites/featureFlags/featureFlagsIntegration/onError/withScope/test.ts b/dev-packages/node-core-integration-tests/suites/featureFlags/featureFlagsIntegration/onError/withScope/test.ts new file mode 100644 index 000000000000..947b299923e7 --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/featureFlags/featureFlagsIntegration/onError/withScope/test.ts @@ -0,0 +1,38 @@ +import { afterAll, test } from 'vitest'; +import { cleanupChildProcesses, createRunner } from '../../../../../utils/runner'; + +afterAll(() => { + cleanupChildProcesses(); +}); + +test('Flags captured on error are isolated by current scope', async () => { + await createRunner(__dirname, 'scenario.ts') + .expect({ + event: { + exception: { values: [{ type: 'Error', value: 'Error in forked scope' }] }, + contexts: { + flags: { + values: [ + { flag: 'forked', result: true }, + { flag: 'shared', result: false }, + ], + }, + }, + }, + }) + .expect({ + event: { + exception: { values: [{ type: 'Error', value: 'Error in main scope' }] }, + contexts: { + flags: { + values: [ + { flag: 'shared', result: true }, + { flag: 'main', result: true }, + ], + }, + }, + }, + }) + .start() + .completed(); +}); diff --git a/dev-packages/node-core-integration-tests/suites/featureFlags/featureFlagsIntegration/onSpan/scenario.ts b/dev-packages/node-core-integration-tests/suites/featureFlags/featureFlagsIntegration/onSpan/scenario.ts new file mode 100644 index 000000000000..77b97396ab5a --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/featureFlags/featureFlagsIntegration/onSpan/scenario.ts @@ -0,0 +1,28 @@ +import { _INTERNAL_MAX_FLAGS_PER_SPAN as MAX_FLAGS_PER_SPAN } from '@sentry/core'; +import * as Sentry from '@sentry/node-core'; +import { loggingTransport } from '@sentry-internal/node-integration-tests'; +import { setupOtel } from '../../../../utils/setupOtel'; + +const client = Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + sampleRate: 1.0, + tracesSampleRate: 1.0, + transport: loggingTransport, + integrations: [Sentry.featureFlagsIntegration()], +}); + +setupOtel(client); + +const flagsIntegration = Sentry.getClient()?.getIntegrationByName('FeatureFlags'); + +Sentry.startSpan({ name: 'test-root-span' }, () => { + Sentry.startSpan({ name: 'test-span' }, () => { + Sentry.startSpan({ name: 'test-nested-span' }, () => { + for (let i = 1; i <= MAX_FLAGS_PER_SPAN; i++) { + flagsIntegration?.addFeatureFlag(`feat${i}`, false); + } + flagsIntegration?.addFeatureFlag(`feat${MAX_FLAGS_PER_SPAN + 1}`, true); // dropped flag + flagsIntegration?.addFeatureFlag('feat3', true); // update + }); + }); +}); diff --git a/dev-packages/node-core-integration-tests/suites/featureFlags/featureFlagsIntegration/onSpan/test.ts b/dev-packages/node-core-integration-tests/suites/featureFlags/featureFlagsIntegration/onSpan/test.ts new file mode 100644 index 000000000000..4a417a3c3959 --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/featureFlags/featureFlagsIntegration/onSpan/test.ts @@ -0,0 +1,33 @@ +import { _INTERNAL_MAX_FLAGS_PER_SPAN as MAX_FLAGS_PER_SPAN } from '@sentry/core'; +import { afterAll, expect, test } from 'vitest'; +import { cleanupChildProcesses, createRunner } from '../../../../utils/runner'; + +afterAll(() => { + cleanupChildProcesses(); +}); + +test('Flags captured on span attributes with max limit', async () => { + // Based on scenario.ts. + const expectedFlags: Record = {}; + for (let i = 1; i <= MAX_FLAGS_PER_SPAN; i++) { + expectedFlags[`flag.evaluation.feat${i}`] = i === 3; + } + + await createRunner(__dirname, 'scenario.ts') + .expect({ + transaction: { + spans: [ + expect.objectContaining({ + description: 'test-span', + data: expect.objectContaining({}), + }), + expect.objectContaining({ + description: 'test-nested-span', + data: expect.objectContaining(expectedFlags), + }), + ], + }, + }) + .start() + .completed(); +}); diff --git a/dev-packages/node-core-integration-tests/suites/no-code/app.js b/dev-packages/node-core-integration-tests/suites/no-code/app.js new file mode 100644 index 000000000000..cb1937007297 --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/no-code/app.js @@ -0,0 +1,3 @@ +setTimeout(() => { + throw new Error('Test error'); +}, 1000); diff --git a/dev-packages/node-core-integration-tests/suites/no-code/app.mjs b/dev-packages/node-core-integration-tests/suites/no-code/app.mjs new file mode 100644 index 000000000000..cb1937007297 --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/no-code/app.mjs @@ -0,0 +1,3 @@ +setTimeout(() => { + throw new Error('Test error'); +}, 1000); diff --git a/dev-packages/node-core-integration-tests/suites/no-code/test.ts b/dev-packages/node-core-integration-tests/suites/no-code/test.ts new file mode 100644 index 000000000000..11f37fbf4d6c --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/no-code/test.ts @@ -0,0 +1,39 @@ +import { afterAll, describe, test } from 'vitest'; +import { cleanupChildProcesses, createRunner } from '../../utils/runner'; + +const EVENT = { + exception: { + values: [ + { + type: 'Error', + value: 'Test error', + }, + ], + }, +}; + +describe('no-code init', () => { + afterAll(() => { + cleanupChildProcesses(); + }); + + test('CJS', async () => { + await createRunner(__dirname, 'app.js') + .withFlags('--require=@sentry/node-core/init') + .withMockSentryServer() + .expect({ event: EVENT }) + .start() + .completed(); + }); + + describe('--import', () => { + test('ESM', async () => { + await createRunner(__dirname, 'app.mjs') + .withFlags('--import=@sentry/node-core/init') + .withMockSentryServer() + .expect({ event: EVENT }) + .start() + .completed(); + }); + }); +}); diff --git a/dev-packages/node-core-integration-tests/suites/proxy/basic.js b/dev-packages/node-core-integration-tests/suites/proxy/basic.js new file mode 100644 index 000000000000..00709a26de91 --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/proxy/basic.js @@ -0,0 +1,20 @@ +const http = require('http'); +const Sentry = require('@sentry/node-core'); +const { createProxy } = require('proxy'); +const { setupOtel } = require('../../utils/setupOtel.js'); + +const proxy = createProxy(http.createServer()); +proxy.listen(0, () => { + const proxyPort = proxy.address().port; + + const client = Sentry.init({ + dsn: process.env.SENTRY_DSN, + transportOptions: { + proxy: `http://localhost:${proxyPort}`, + }, + }); + + setupOtel(client); + + Sentry.captureMessage('Hello, via proxy!'); +}); diff --git a/dev-packages/node-core-integration-tests/suites/proxy/test.ts b/dev-packages/node-core-integration-tests/suites/proxy/test.ts new file mode 100644 index 000000000000..805b913d4814 --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/proxy/test.ts @@ -0,0 +1,18 @@ +import { afterAll, test } from 'vitest'; +import { cleanupChildProcesses, createRunner } from '../../utils/runner'; + +afterAll(() => { + cleanupChildProcesses(); +}); + +test('proxies sentry requests', async () => { + await createRunner(__dirname, 'basic.js') + .withMockSentryServer() + .expect({ + event: { + message: 'Hello, via proxy!', + }, + }) + .start() + .completed(); +}); diff --git a/dev-packages/node-core-integration-tests/suites/public-api/LocalVariables/deny-inspector.mjs b/dev-packages/node-core-integration-tests/suites/public-api/LocalVariables/deny-inspector.mjs new file mode 100644 index 000000000000..08db0bf96acb --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/public-api/LocalVariables/deny-inspector.mjs @@ -0,0 +1,22 @@ +import { register } from 'node:module'; + +register( + new URL(`data:application/javascript, +export async function resolve(specifier, context, nextResolve) { + if (specifier === 'node:inspector' || specifier === 'inspector') { + throw new Error('Should not use node:inspector module'); + } + + return nextResolve(specifier); +}`), + import.meta.url, +); + +(async () => { + const Sentry = await import('@sentry/node-core'); + const { setupOtel } = await import('../../../utils/setupOtel.js'); + + const client = Sentry.init({}); + + setupOtel(client); +})(); diff --git a/dev-packages/node-core-integration-tests/suites/public-api/LocalVariables/local-variables-caught.js b/dev-packages/node-core-integration-tests/suites/public-api/LocalVariables/local-variables-caught.js new file mode 100644 index 000000000000..17211aea77bd --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/public-api/LocalVariables/local-variables-caught.js @@ -0,0 +1,43 @@ +/* eslint-disable no-unused-vars */ +const Sentry = require('@sentry/node-core'); +const { loggingTransport } = require('@sentry-internal/node-core-integration-tests'); +const { setupOtel } = require('../../../utils/setupOtel.js'); + +const client = Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + includeLocalVariables: true, + transport: loggingTransport, +}); + +setupOtel(client); + +class Some { + two(name) { + throw new Error('Enough!'); + } +} + +function one(name) { + const arr = [1, '2', null]; + const obj = { + name, + num: 5, + }; + const bool = false; + const num = 0; + const str = ''; + const something = undefined; + const somethingElse = null; + + const ty = new Some(); + + ty.two(name); +} + +setTimeout(() => { + try { + one('some name'); + } catch (e) { + Sentry.captureException(e); + } +}, 1000); diff --git a/dev-packages/node-core-integration-tests/suites/public-api/LocalVariables/local-variables-caught.mjs b/dev-packages/node-core-integration-tests/suites/public-api/LocalVariables/local-variables-caught.mjs new file mode 100644 index 000000000000..3df12c70382b --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/public-api/LocalVariables/local-variables-caught.mjs @@ -0,0 +1,47 @@ +/* eslint-disable no-unused-vars */ +import * as Sentry from '@sentry/node-core'; +import { loggingTransport } from '@sentry-internal/node-integration-tests'; +import { setupOtel } from '../../../utils/setupOtel.js'; + +const client = Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + includeLocalVariables: true, + transport: loggingTransport, +}); + +setupOtel(client); + +class Some { + async two(name) { + return new Promise((_, reject) => { + reject(new Error('Enough!')); + }); + } +} + +async function one(name) { + const arr = [1, '2', null]; + const obj = { + name, + num: 5, + functionsShouldNotBeIncluded: () => {}, + functionsShouldNotBeIncluded2() {}, + }; + const bool = false; + const num = 0; + const str = ''; + const something = undefined; + const somethingElse = null; + + const ty = new Some(); + + await ty.two(name); +} + +setTimeout(async () => { + try { + await one('some name'); + } catch (e) { + Sentry.captureException(e); + } +}, 1000); diff --git a/dev-packages/node-core-integration-tests/suites/public-api/LocalVariables/local-variables-instrument.js b/dev-packages/node-core-integration-tests/suites/public-api/LocalVariables/local-variables-instrument.js new file mode 100644 index 000000000000..71b6c22cf75e --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/public-api/LocalVariables/local-variables-instrument.js @@ -0,0 +1,11 @@ +const Sentry = require('@sentry/node-core'); +const { loggingTransport } = require('@sentry-internal/node-core-integration-tests'); +const { setupOtel } = require('../../../utils/setupOtel.js'); + +const client = Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + includeLocalVariables: true, + transport: loggingTransport, +}); + +setupOtel(client); diff --git a/dev-packages/node-core-integration-tests/suites/public-api/LocalVariables/local-variables-no-sentry.js b/dev-packages/node-core-integration-tests/suites/public-api/LocalVariables/local-variables-no-sentry.js new file mode 100644 index 000000000000..08636175fa7b --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/public-api/LocalVariables/local-variables-no-sentry.js @@ -0,0 +1,31 @@ +/* eslint-disable no-unused-vars */ +process.on('uncaughtException', () => { + // do nothing - this will prevent the Error below from closing this process +}); + +class Some { + two(name) { + throw new Error('Enough!'); + } +} + +function one(name) { + const arr = [1, '2', null]; + const obj = { + name, + num: 5, + }; + const bool = false; + const num = 0; + const str = ''; + const something = undefined; + const somethingElse = null; + + const ty = new Some(); + + ty.two(name); +} + +setTimeout(() => { + one('some name'); +}, 1000); diff --git a/dev-packages/node-core-integration-tests/suites/public-api/LocalVariables/local-variables-rethrow.js b/dev-packages/node-core-integration-tests/suites/public-api/LocalVariables/local-variables-rethrow.js new file mode 100644 index 000000000000..5a533ac16867 --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/public-api/LocalVariables/local-variables-rethrow.js @@ -0,0 +1,48 @@ +/* eslint-disable no-unused-vars */ +const Sentry = require('@sentry/node-core'); +const { loggingTransport } = require('@sentry-internal/node-core-integration-tests'); +const { setupOtel } = require('../../../utils/setupOtel.js'); + +const client = Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + includeLocalVariables: true, + transport: loggingTransport, +}); + +setupOtel(client); + +class Some { + two(name) { + throw new Error('Enough!'); + } +} + +function one(name) { + const arr = [1, '2', null]; + const obj = { + name, + num: 5, + }; + const bool = false; + const num = 0; + const str = ''; + const something = undefined; + const somethingElse = null; + + const ty = new Some(); + + ty.two(name); +} + +setTimeout(() => { + try { + try { + one('some name'); + } catch (e) { + const more = 'here'; + throw e; + } + } catch (e) { + Sentry.captureException(e); + } +}, 1000); diff --git a/dev-packages/node-core-integration-tests/suites/public-api/LocalVariables/local-variables.js b/dev-packages/node-core-integration-tests/suites/public-api/LocalVariables/local-variables.js new file mode 100644 index 000000000000..ecdd5f219316 --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/public-api/LocalVariables/local-variables.js @@ -0,0 +1,43 @@ +/* eslint-disable no-unused-vars */ +const Sentry = require('@sentry/node-core'); +const { loggingTransport } = require('@sentry-internal/node-core-integration-tests'); +const { setupOtel } = require('../../../utils/setupOtel.js'); + +const client = Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + includeLocalVariables: true, + transport: loggingTransport, +}); + +setupOtel(client); + +process.on('uncaughtException', () => { + // do nothing - this will prevent the Error below from closing this process +}); + +class Some { + two(name) { + throw new Error('Enough!'); + } +} + +function one(name) { + const arr = [1, '2', null]; + const obj = { + name, + num: 5, + }; + const bool = false; + const num = 0; + const str = ''; + const something = undefined; + const somethingElse = null; + + const ty = new Some(); + + ty.two(name); +} + +setTimeout(() => { + one('some name'); +}, 1000); diff --git a/dev-packages/node-core-integration-tests/suites/public-api/LocalVariables/no-local-variables.js b/dev-packages/node-core-integration-tests/suites/public-api/LocalVariables/no-local-variables.js new file mode 100644 index 000000000000..1532abc2797a --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/public-api/LocalVariables/no-local-variables.js @@ -0,0 +1,42 @@ +/* eslint-disable no-unused-vars */ +const Sentry = require('@sentry/node-core'); +const { loggingTransport } = require('@sentry-internal/node-core-integration-tests'); +const { setupOtel } = require('../../../utils/setupOtel.js'); + +const client = Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + transport: loggingTransport, +}); + +setupOtel(client); + +process.on('uncaughtException', () => { + // do nothing - this will prevent the Error below from closing this process +}); + +class Some { + two(name) { + throw new Error('Enough!'); + } +} + +function one(name) { + const arr = [1, '2', null]; + const obj = { + name, + num: 5, + }; + const bool = false; + const num = 0; + const str = ''; + const something = undefined; + const somethingElse = null; + + const ty = new Some(); + + ty.two(name); +} + +setTimeout(() => { + one('some name'); +}, 1000); diff --git a/dev-packages/node-core-integration-tests/suites/public-api/LocalVariables/test.ts b/dev-packages/node-core-integration-tests/suites/public-api/LocalVariables/test.ts new file mode 100644 index 000000000000..e95e5a9e3767 --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/public-api/LocalVariables/test.ts @@ -0,0 +1,104 @@ +import * as path from 'path'; +import { afterAll, describe, expect, test } from 'vitest'; +import { conditionalTest } from '../../../utils'; +import { cleanupChildProcesses, createRunner } from '../../../utils/runner'; + +// This test takes some time because it connects the debugger etc. +// So we increase the timeout here +// vi.setTimeout(45_000); + +const EXPECTED_LOCAL_VARIABLES_EVENT = { + exception: { + values: [ + { + stacktrace: { + frames: expect.arrayContaining([ + expect.objectContaining({ + function: 'one', + vars: { + name: 'some name', + arr: [1, '2', null], + obj: { name: 'some name', num: 5 }, + ty: '', + bool: false, + num: 0, + str: '', + something: '', + somethingElse: '', + }, + }), + expect.objectContaining({ + function: 'Some.two', + vars: { name: 'some name' }, + }), + ]), + }, + }, + ], + }, +}; + +describe('LocalVariables integration', () => { + afterAll(() => { + cleanupChildProcesses(); + }); + + test('Should not include local variables by default', async () => { + await createRunner(__dirname, 'no-local-variables.js') + .expect({ + event: event => { + for (const frame of event.exception?.values?.[0]?.stacktrace?.frames || []) { + expect(frame.vars).toBeUndefined(); + } + }, + }) + .start() + .completed(); + }); + + test('Should include local variables when enabled', async () => { + await createRunner(__dirname, 'local-variables.js') + .expect({ event: EXPECTED_LOCAL_VARIABLES_EVENT }) + .start() + .completed(); + }); + + test('Should include local variables when instrumenting via --require', async () => { + const requirePath = path.resolve(__dirname, 'local-variables-instrument.js'); + + await createRunner(__dirname, 'local-variables-no-sentry.js') + .withFlags(`--require=${requirePath}`) + .expect({ event: EXPECTED_LOCAL_VARIABLES_EVENT }) + .start() + .completed(); + }); + + test('Should include local variables with ESM', async () => { + await createRunner(__dirname, 'local-variables-caught.mjs') + .expect({ event: EXPECTED_LOCAL_VARIABLES_EVENT }) + .start() + .completed(); + }); + + conditionalTest({ min: 19 })('Node v19+', () => { + test('Should not import inspector when not in use', async () => { + await createRunner(__dirname, 'deny-inspector.mjs').ensureNoErrorOutput().start().completed(); + }); + }); + + conditionalTest({ min: 20 })('Node v20+', () => { + test('Should retain original local variables when error is re-thrown', async () => { + await createRunner(__dirname, 'local-variables-rethrow.js') + .expect({ event: EXPECTED_LOCAL_VARIABLES_EVENT }) + .start() + .completed(); + }); + }); + + test('Includes local variables for caught exceptions when enabled', async () => { + await createRunner(__dirname, 'local-variables-caught.js') + .expect({ event: EXPECTED_LOCAL_VARIABLES_EVENT }) + .start() + .completed(); + }); +}); diff --git a/dev-packages/node-core-integration-tests/suites/public-api/OnUncaughtException/additional-listener-test-script.js b/dev-packages/node-core-integration-tests/suites/public-api/OnUncaughtException/additional-listener-test-script.js new file mode 100644 index 000000000000..745b744da467 --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/public-api/OnUncaughtException/additional-listener-test-script.js @@ -0,0 +1,19 @@ +const Sentry = require('@sentry/node-core'); +const { setupOtel } = require('../../../utils/setupOtel.js'); + +const client = Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', +}); + +setupOtel(client); + +process.on('uncaughtException', () => { + // do nothing - this will prevent the Error below from closing this process before the timeout resolves +}); + +setTimeout(() => { + process.stdout.write("I'm alive!"); + process.exit(0); +}, 500); + +throw new Error(); diff --git a/dev-packages/node-core-integration-tests/suites/public-api/OnUncaughtException/log-entire-error-to-console.js b/dev-packages/node-core-integration-tests/suites/public-api/OnUncaughtException/log-entire-error-to-console.js new file mode 100644 index 000000000000..467eb127f7d1 --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/public-api/OnUncaughtException/log-entire-error-to-console.js @@ -0,0 +1,10 @@ +const Sentry = require('@sentry/node-core'); +const { setupOtel } = require('../../../utils/setupOtel.js'); + +const client = Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', +}); + +setupOtel(client); + +throw new Error('foo', { cause: 'bar' }); diff --git a/dev-packages/node-core-integration-tests/suites/public-api/OnUncaughtException/mimic-native-behaviour-additional-listener-test-script.js b/dev-packages/node-core-integration-tests/suites/public-api/OnUncaughtException/mimic-native-behaviour-additional-listener-test-script.js new file mode 100644 index 000000000000..d5eb8383c188 --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/public-api/OnUncaughtException/mimic-native-behaviour-additional-listener-test-script.js @@ -0,0 +1,24 @@ +const Sentry = require('@sentry/node-core'); +const { setupOtel } = require('../../../utils/setupOtel.js'); + +const client = Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + integrations: [ + Sentry.onUncaughtExceptionIntegration({ + exitEvenIfOtherHandlersAreRegistered: false, + }), + ], +}); + +setupOtel(client); + +process.on('uncaughtException', () => { + // do nothing - this will prevent the Error below from closing this process before the timeout resolves +}); + +setTimeout(() => { + process.stdout.write("I'm alive!"); + process.exit(0); +}, 500); + +throw new Error(); diff --git a/dev-packages/node-core-integration-tests/suites/public-api/OnUncaughtException/mimic-native-behaviour-no-additional-listener-test-script.js b/dev-packages/node-core-integration-tests/suites/public-api/OnUncaughtException/mimic-native-behaviour-no-additional-listener-test-script.js new file mode 100644 index 000000000000..bc8fc1f8c898 --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/public-api/OnUncaughtException/mimic-native-behaviour-no-additional-listener-test-script.js @@ -0,0 +1,21 @@ +const Sentry = require('@sentry/node-core'); +const { setupOtel } = require('../../../utils/setupOtel.js'); + +const client = Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + integrations: [ + Sentry.onUncaughtExceptionIntegration({ + exitEvenIfOtherHandlersAreRegistered: false, + }), + ], +}); + +setupOtel(client); + +setTimeout(() => { + // This should not be called because the script throws before this resolves + process.stdout.write("I'm alive!"); + process.exit(0); +}, 500); + +throw new Error(); diff --git a/dev-packages/node-core-integration-tests/suites/public-api/OnUncaughtException/no-additional-listener-test-script.js b/dev-packages/node-core-integration-tests/suites/public-api/OnUncaughtException/no-additional-listener-test-script.js new file mode 100644 index 000000000000..513eb3abc7cb --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/public-api/OnUncaughtException/no-additional-listener-test-script.js @@ -0,0 +1,16 @@ +const Sentry = require('@sentry/node-core'); +const { setupOtel } = require('../../../utils/setupOtel.js'); + +const client = Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', +}); + +setupOtel(client); + +setTimeout(() => { + // This should not be called because the script throws before this resolves + process.stdout.write("I'm alive!"); + process.exit(0); +}, 500); + +throw new Error(); diff --git a/dev-packages/node-core-integration-tests/suites/public-api/OnUncaughtException/test.ts b/dev-packages/node-core-integration-tests/suites/public-api/OnUncaughtException/test.ts new file mode 100644 index 000000000000..d27b08f152be --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/public-api/OnUncaughtException/test.ts @@ -0,0 +1,77 @@ +import * as childProcess from 'child_process'; +import * as path from 'path'; +import { describe, expect, test } from 'vitest'; + +describe('OnUncaughtException integration', () => { + test('should close process on uncaught error with no additional listeners registered', () => + new Promise(done => { + expect.assertions(3); + + const testScriptPath = path.resolve(__dirname, 'no-additional-listener-test-script.js'); + + childProcess.exec(`node ${testScriptPath}`, { encoding: 'utf8' }, (err, stdout) => { + expect(err).not.toBeNull(); + expect(err?.code).toBe(1); + expect(stdout).not.toBe("I'm alive!"); + done(); + }); + })); + + test('should not close process on uncaught error when additional listeners are registered', () => + new Promise(done => { + expect.assertions(2); + + const testScriptPath = path.resolve(__dirname, 'additional-listener-test-script.js'); + + childProcess.exec(`node ${testScriptPath}`, { encoding: 'utf8' }, (err, stdout) => { + expect(err).toBeNull(); + expect(stdout).toBe("I'm alive!"); + done(); + }); + })); + + test('should log entire error object to console stderr', () => + new Promise(done => { + expect.assertions(2); + + const testScriptPath = path.resolve(__dirname, 'log-entire-error-to-console.js'); + + childProcess.exec(`node ${testScriptPath}`, { encoding: 'utf8' }, (err, stderr) => { + expect(err).not.toBeNull(); + const errString = err?.toString() || ''; + + expect(errString).toContain(stderr); + + done(); + }); + })); + + describe('with `exitEvenIfOtherHandlersAreRegistered` set to false', () => { + test('should close process on uncaught error with no additional listeners registered', () => + new Promise(done => { + expect.assertions(3); + + const testScriptPath = path.resolve(__dirname, 'mimic-native-behaviour-no-additional-listener-test-script.js'); + + childProcess.exec(`node ${testScriptPath}`, { encoding: 'utf8' }, (err, stdout) => { + expect(err).not.toBeNull(); + expect(err?.code).toBe(1); + expect(stdout).not.toBe("I'm alive!"); + done(); + }); + })); + + test('should not close process on uncaught error when additional listeners are registered', () => + new Promise(done => { + expect.assertions(2); + + const testScriptPath = path.resolve(__dirname, 'mimic-native-behaviour-additional-listener-test-script.js'); + + childProcess.exec(`node ${testScriptPath}`, { encoding: 'utf8' }, (err, stdout) => { + expect(err).toBeNull(); + expect(stdout).toBe("I'm alive!"); + done(); + }); + })); + }); +}); diff --git a/dev-packages/node-core-integration-tests/suites/public-api/addBreadcrumb/empty-obj/scenario.ts b/dev-packages/node-core-integration-tests/suites/public-api/addBreadcrumb/empty-obj/scenario.ts new file mode 100644 index 000000000000..45aa96d3e8f7 --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/public-api/addBreadcrumb/empty-obj/scenario.ts @@ -0,0 +1,14 @@ +import * as Sentry from '@sentry/node-core'; +import { loggingTransport } from '@sentry-internal/node-integration-tests'; +import { setupOtel } from '../../../../utils/setupOtel'; + +const client = Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', + transport: loggingTransport, +}); + +setupOtel(client); + +Sentry.addBreadcrumb({}); +Sentry.captureMessage('test-empty-obj'); diff --git a/dev-packages/node-core-integration-tests/suites/public-api/addBreadcrumb/empty-obj/test.ts b/dev-packages/node-core-integration-tests/suites/public-api/addBreadcrumb/empty-obj/test.ts new file mode 100644 index 000000000000..c4f5145a8bbf --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/public-api/addBreadcrumb/empty-obj/test.ts @@ -0,0 +1,17 @@ +import { afterAll, test } from 'vitest'; +import { cleanupChildProcesses, createRunner } from '../../../../utils/runner'; + +afterAll(() => { + cleanupChildProcesses(); +}); + +test('should add an empty breadcrumb, when an empty object is given', async () => { + await createRunner(__dirname, 'scenario.ts') + .expect({ + event: { + message: 'test-empty-obj', + }, + }) + .start() + .completed(); +}); diff --git a/dev-packages/node-core-integration-tests/suites/public-api/addBreadcrumb/multiple_breadcrumbs/scenario.ts b/dev-packages/node-core-integration-tests/suites/public-api/addBreadcrumb/multiple_breadcrumbs/scenario.ts new file mode 100644 index 000000000000..91e7670f4dfe --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/public-api/addBreadcrumb/multiple_breadcrumbs/scenario.ts @@ -0,0 +1,23 @@ +import * as Sentry from '@sentry/node-core'; +import { loggingTransport } from '@sentry-internal/node-integration-tests'; +import { setupOtel } from '../../../../utils/setupOtel'; + +const client = Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', + transport: loggingTransport, +}); + +setupOtel(client); + +Sentry.addBreadcrumb({ + category: 'foo', + message: 'bar', + level: 'fatal', +}); + +Sentry.addBreadcrumb({ + category: 'qux', +}); + +Sentry.captureMessage('test_multi_breadcrumbs'); diff --git a/dev-packages/node-core-integration-tests/suites/public-api/addBreadcrumb/multiple_breadcrumbs/test.ts b/dev-packages/node-core-integration-tests/suites/public-api/addBreadcrumb/multiple_breadcrumbs/test.ts new file mode 100644 index 000000000000..13dba000a823 --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/public-api/addBreadcrumb/multiple_breadcrumbs/test.ts @@ -0,0 +1,27 @@ +import { afterAll, test } from 'vitest'; +import { cleanupChildProcesses, createRunner } from '../../../../utils/runner'; + +afterAll(() => { + cleanupChildProcesses(); +}); + +test('should add multiple breadcrumbs', async () => { + await createRunner(__dirname, 'scenario.ts') + .expect({ + event: { + message: 'test_multi_breadcrumbs', + breadcrumbs: [ + { + category: 'foo', + message: 'bar', + level: 'fatal', + }, + { + category: 'qux', + }, + ], + }, + }) + .start() + .completed(); +}); diff --git a/dev-packages/node-core-integration-tests/suites/public-api/addBreadcrumb/simple_breadcrumb/scenario.ts b/dev-packages/node-core-integration-tests/suites/public-api/addBreadcrumb/simple_breadcrumb/scenario.ts new file mode 100644 index 000000000000..27cbadbd9c22 --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/public-api/addBreadcrumb/simple_breadcrumb/scenario.ts @@ -0,0 +1,19 @@ +import * as Sentry from '@sentry/node-core'; +import { loggingTransport } from '@sentry-internal/node-integration-tests'; +import { setupOtel } from '../../../../utils/setupOtel'; + +const client = Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', + transport: loggingTransport, +}); + +setupOtel(client); + +Sentry.addBreadcrumb({ + category: 'foo', + message: 'bar', + level: 'fatal', +}); + +Sentry.captureMessage('test_simple'); diff --git a/dev-packages/node-core-integration-tests/suites/public-api/addBreadcrumb/simple_breadcrumb/test.ts b/dev-packages/node-core-integration-tests/suites/public-api/addBreadcrumb/simple_breadcrumb/test.ts new file mode 100644 index 000000000000..9708e00201ae --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/public-api/addBreadcrumb/simple_breadcrumb/test.ts @@ -0,0 +1,20 @@ +import { test } from 'vitest'; +import { createRunner } from '../../../../utils/runner'; + +test('should add a simple breadcrumb', async () => { + await createRunner(__dirname, 'scenario.ts') + .expect({ + event: { + message: 'test_simple', + breadcrumbs: [ + { + category: 'foo', + message: 'bar', + level: 'fatal', + }, + ], + }, + }) + .start() + .completed(); +}); diff --git a/dev-packages/node-core-integration-tests/suites/public-api/captureException/catched-error/scenario.ts b/dev-packages/node-core-integration-tests/suites/public-api/captureException/catched-error/scenario.ts new file mode 100644 index 000000000000..f09a9b971e6f --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/public-api/captureException/catched-error/scenario.ts @@ -0,0 +1,17 @@ +import * as Sentry from '@sentry/node-core'; +import { loggingTransport } from '@sentry-internal/node-core-integration-tests'; +import { setupOtel } from '../../../../utils/setupOtel'; + +const client = Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', + transport: loggingTransport, +}); + +setupOtel(client); + +try { + throw new Error('catched_error'); +} catch (err) { + Sentry.captureException(err); +} diff --git a/dev-packages/node-core-integration-tests/suites/public-api/captureException/catched-error/test.ts b/dev-packages/node-core-integration-tests/suites/public-api/captureException/catched-error/test.ts new file mode 100644 index 000000000000..31a5bf3d6b2e --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/public-api/captureException/catched-error/test.ts @@ -0,0 +1,45 @@ +import { afterAll, expect, test } from 'vitest'; +import { cleanupChildProcesses, createRunner } from '../../../../utils/runner'; + +afterAll(() => { + cleanupChildProcesses(); +}); + +test('should work inside catch block', async () => { + await createRunner(__dirname, 'scenario.ts') + .expect({ + event: { + exception: { + values: [ + { + type: 'Error', + value: 'catched_error', + mechanism: { + type: 'generic', + handled: true, + }, + stacktrace: { + frames: expect.arrayContaining([ + expect.objectContaining({ + context_line: " throw new Error('catched_error');", + pre_context: [ + " release: '1.0',", + ' transport: loggingTransport,', + '});', + '', + 'setupOtel(client);', + '', + 'try {', + ], + post_context: ['} catch (err) {', ' Sentry.captureException(err);', '}'], + }), + ]), + }, + }, + ], + }, + }, + }) + .start() + .completed(); +}); diff --git a/dev-packages/node-core-integration-tests/suites/public-api/captureException/empty-obj/scenario.ts b/dev-packages/node-core-integration-tests/suites/public-api/captureException/empty-obj/scenario.ts new file mode 100644 index 000000000000..50e651ff3f71 --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/public-api/captureException/empty-obj/scenario.ts @@ -0,0 +1,13 @@ +import * as Sentry from '@sentry/node-core'; +import { loggingTransport } from '@sentry-internal/node-core-integration-tests'; +import { setupOtel } from '../../../../utils/setupOtel'; + +const client = Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', + transport: loggingTransport, +}); + +Sentry.captureException({}); + +setupOtel(client); diff --git a/dev-packages/node-core-integration-tests/suites/public-api/captureException/empty-obj/test.ts b/dev-packages/node-core-integration-tests/suites/public-api/captureException/empty-obj/test.ts new file mode 100644 index 000000000000..b8a6fe4f85e2 --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/public-api/captureException/empty-obj/test.ts @@ -0,0 +1,28 @@ +import { afterAll, test } from 'vitest'; +import { cleanupChildProcesses, createRunner } from '../../../../utils/runner'; + +afterAll(() => { + cleanupChildProcesses(); +}); + +test('should capture an empty object', async () => { + await createRunner(__dirname, 'scenario.ts') + .expect({ + event: { + exception: { + values: [ + { + type: 'Error', + value: 'Object captured as exception with keys: [object has no keys]', + mechanism: { + type: 'generic', + handled: true, + }, + }, + ], + }, + }, + }) + .start() + .completed(); +}); diff --git a/dev-packages/node-core-integration-tests/suites/public-api/captureException/simple-error/scenario.ts b/dev-packages/node-core-integration-tests/suites/public-api/captureException/simple-error/scenario.ts new file mode 100644 index 000000000000..8fd8955e6df4 --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/public-api/captureException/simple-error/scenario.ts @@ -0,0 +1,13 @@ +import * as Sentry from '@sentry/node-core'; +import { loggingTransport } from '@sentry-internal/node-core-integration-tests'; +import { setupOtel } from '../../../../utils/setupOtel'; + +const client = Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', + transport: loggingTransport, +}); + +setupOtel(client); + +Sentry.captureException(new Error('test_simple_error')); diff --git a/dev-packages/node-core-integration-tests/suites/public-api/captureException/simple-error/test.ts b/dev-packages/node-core-integration-tests/suites/public-api/captureException/simple-error/test.ts new file mode 100644 index 000000000000..3afe450398e3 --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/public-api/captureException/simple-error/test.ts @@ -0,0 +1,31 @@ +import { afterAll, expect, test } from 'vitest'; +import { cleanupChildProcesses, createRunner } from '../../../../utils/runner'; + +afterAll(() => { + cleanupChildProcesses(); +}); + +test('should capture a simple error with message', async () => { + await createRunner(__dirname, 'scenario.ts') + .expect({ + event: { + exception: { + values: [ + { + type: 'Error', + value: 'test_simple_error', + mechanism: { + type: 'generic', + handled: true, + }, + stacktrace: { + frames: expect.any(Array), + }, + }, + ], + }, + }, + }) + .start() + .completed(); +}); diff --git a/dev-packages/node-core-integration-tests/suites/public-api/captureMessage/parameterized_message/scenario.ts b/dev-packages/node-core-integration-tests/suites/public-api/captureMessage/parameterized_message/scenario.ts new file mode 100644 index 000000000000..013ba37320c6 --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/public-api/captureMessage/parameterized_message/scenario.ts @@ -0,0 +1,16 @@ +import * as Sentry from '@sentry/node-core'; +import { loggingTransport } from '@sentry-internal/node-integration-tests'; +import { setupOtel } from '../../../../utils/setupOtel'; + +const client = Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', + transport: loggingTransport, +}); + +setupOtel(client); + +const x = 'first'; +const y = 'second'; + +Sentry.captureMessage(Sentry.parameterize`This is a log statement with ${x} and ${y} params`); diff --git a/dev-packages/node-core-integration-tests/suites/public-api/captureMessage/parameterized_message/test.ts b/dev-packages/node-core-integration-tests/suites/public-api/captureMessage/parameterized_message/test.ts new file mode 100644 index 000000000000..15e6e76306fe --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/public-api/captureMessage/parameterized_message/test.ts @@ -0,0 +1,20 @@ +import { afterAll, test } from 'vitest'; +import { cleanupChildProcesses, createRunner } from '../../../../utils/runner'; + +afterAll(() => { + cleanupChildProcesses(); +}); + +test('should capture a parameterized representation of the message', async () => { + await createRunner(__dirname, 'scenario.ts') + .expect({ + event: { + logentry: { + message: 'This is a log statement with %s and %s params', + params: ['first', 'second'], + }, + }, + }) + .start() + .completed(); +}); diff --git a/dev-packages/node-core-integration-tests/suites/public-api/captureMessage/simple_message/scenario.ts b/dev-packages/node-core-integration-tests/suites/public-api/captureMessage/simple_message/scenario.ts new file mode 100644 index 000000000000..ac6d3b60b18a --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/public-api/captureMessage/simple_message/scenario.ts @@ -0,0 +1,13 @@ +import * as Sentry from '@sentry/node-core'; +import { loggingTransport } from '@sentry-internal/node-integration-tests'; +import { setupOtel } from '../../../../utils/setupOtel'; + +const client = Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', + transport: loggingTransport, +}); + +setupOtel(client); + +Sentry.captureMessage('Message'); diff --git a/dev-packages/node-core-integration-tests/suites/public-api/captureMessage/simple_message/test.ts b/dev-packages/node-core-integration-tests/suites/public-api/captureMessage/simple_message/test.ts new file mode 100644 index 000000000000..e32081747f28 --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/public-api/captureMessage/simple_message/test.ts @@ -0,0 +1,18 @@ +import { afterAll, test } from 'vitest'; +import { cleanupChildProcesses, createRunner } from '../../../../utils/runner'; + +afterAll(() => { + cleanupChildProcesses(); +}); + +test('should capture a simple message string', async () => { + await createRunner(__dirname, 'scenario.ts') + .expect({ + event: { + message: 'Message', + level: 'info', + }, + }) + .start() + .completed(); +}); diff --git a/dev-packages/node-core-integration-tests/suites/public-api/captureMessage/simple_message_attachStackTrace/scenario.ts b/dev-packages/node-core-integration-tests/suites/public-api/captureMessage/simple_message_attachStackTrace/scenario.ts new file mode 100644 index 000000000000..d829d8fe100d --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/public-api/captureMessage/simple_message_attachStackTrace/scenario.ts @@ -0,0 +1,14 @@ +import * as Sentry from '@sentry/node-core'; +import { loggingTransport } from '@sentry-internal/node-integration-tests'; +import { setupOtel } from '../../../../utils/setupOtel'; + +const client = Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', + transport: loggingTransport, + attachStacktrace: true, +}); + +setupOtel(client); + +Sentry.captureMessage('Message'); diff --git a/dev-packages/node-core-integration-tests/suites/public-api/captureMessage/simple_message_attachStackTrace/test.ts b/dev-packages/node-core-integration-tests/suites/public-api/captureMessage/simple_message_attachStackTrace/test.ts new file mode 100644 index 000000000000..8c79687b2bc4 --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/public-api/captureMessage/simple_message_attachStackTrace/test.ts @@ -0,0 +1,27 @@ +import { afterAll, expect, test } from 'vitest'; +import { cleanupChildProcesses, createRunner } from '../../../../utils/runner'; + +afterAll(() => { + cleanupChildProcesses(); +}); + +test('capture a simple message string with a stack trace if `attachStackTrace` is `true`', async () => { + await createRunner(__dirname, 'scenario.ts') + .expect({ + event: { + message: 'Message', + level: 'info', + exception: { + values: [ + { + mechanism: { synthetic: true, type: 'generic', handled: true }, + value: 'Message', + stacktrace: { frames: expect.any(Array) }, + }, + ], + }, + }, + }) + .start() + .completed(); +}); diff --git a/dev-packages/node-core-integration-tests/suites/public-api/captureMessage/with_level/scenario.ts b/dev-packages/node-core-integration-tests/suites/public-api/captureMessage/with_level/scenario.ts new file mode 100644 index 000000000000..0156dd6339c1 --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/public-api/captureMessage/with_level/scenario.ts @@ -0,0 +1,18 @@ +import * as Sentry from '@sentry/node-core'; +import { loggingTransport } from '@sentry-internal/node-integration-tests'; +import { setupOtel } from '../../../../utils/setupOtel'; + +const client = Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', + transport: loggingTransport, +}); + +setupOtel(client); + +Sentry.captureMessage('debug_message', 'debug'); +Sentry.captureMessage('info_message', 'info'); +Sentry.captureMessage('warning_message', 'warning'); +Sentry.captureMessage('error_message', 'error'); +Sentry.captureMessage('fatal_message', 'fatal'); +Sentry.captureMessage('log_message', 'log'); diff --git a/dev-packages/node-core-integration-tests/suites/public-api/captureMessage/with_level/test.ts b/dev-packages/node-core-integration-tests/suites/public-api/captureMessage/with_level/test.ts new file mode 100644 index 000000000000..a44af6931d1f --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/public-api/captureMessage/with_level/test.ts @@ -0,0 +1,18 @@ +import { afterAll, test } from 'vitest'; +import { cleanupChildProcesses, createRunner } from '../../../../utils/runner'; + +afterAll(() => { + cleanupChildProcesses(); +}); + +test('should capture with different severity levels', async () => { + await createRunner(__dirname, 'scenario.ts') + .expect({ event: { message: 'debug_message', level: 'debug' } }) + .expect({ event: { message: 'info_message', level: 'info' } }) + .expect({ event: { message: 'warning_message', level: 'warning' } }) + .expect({ event: { message: 'error_message', level: 'error' } }) + .expect({ event: { message: 'fatal_message', level: 'fatal' } }) + .expect({ event: { message: 'log_message', level: 'log' } }) + .start() + .completed(); +}); diff --git a/dev-packages/node-core-integration-tests/suites/public-api/configureScope/clear_scope/scenario.ts b/dev-packages/node-core-integration-tests/suites/public-api/configureScope/clear_scope/scenario.ts new file mode 100644 index 000000000000..fa0b7016626c --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/public-api/configureScope/clear_scope/scenario.ts @@ -0,0 +1,19 @@ +import * as Sentry from '@sentry/node-core'; +import { loggingTransport } from '@sentry-internal/node-integration-tests'; +import { setupOtel } from '../../../../utils/setupOtel'; + +const client = Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', + transport: loggingTransport, +}); + +setupOtel(client); + +const scope = Sentry.getCurrentScope(); +scope.setTag('foo', 'bar'); +scope.setUser({ id: 'baz' }); +scope.setExtra('qux', 'quux'); +scope.clear(); + +Sentry.captureMessage('cleared_scope'); diff --git a/dev-packages/node-core-integration-tests/suites/public-api/configureScope/clear_scope/test.ts b/dev-packages/node-core-integration-tests/suites/public-api/configureScope/clear_scope/test.ts new file mode 100644 index 000000000000..19f16417bb50 --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/public-api/configureScope/clear_scope/test.ts @@ -0,0 +1,17 @@ +import { afterAll, test } from 'vitest'; +import { cleanupChildProcesses, createRunner } from '../../../../utils/runner'; + +afterAll(() => { + cleanupChildProcesses(); +}); + +test('should clear previously set properties of a scope', async () => { + await createRunner(__dirname, 'scenario.ts') + .expect({ + event: { + message: 'cleared_scope', + }, + }) + .start() + .completed(); +}); diff --git a/dev-packages/node-core-integration-tests/suites/public-api/configureScope/set_properties/scenario.ts b/dev-packages/node-core-integration-tests/suites/public-api/configureScope/set_properties/scenario.ts new file mode 100644 index 000000000000..3ec9b740fd23 --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/public-api/configureScope/set_properties/scenario.ts @@ -0,0 +1,18 @@ +import * as Sentry from '@sentry/node-core'; +import { loggingTransport } from '@sentry-internal/node-integration-tests'; +import { setupOtel } from '../../../../utils/setupOtel'; + +const client = Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', + transport: loggingTransport, +}); + +setupOtel(client); + +const scope = Sentry.getCurrentScope(); +scope.setTag('foo', 'bar'); +scope.setUser({ id: 'baz' }); +scope.setExtra('qux', 'quux'); + +Sentry.captureMessage('configured_scope'); diff --git a/dev-packages/node-core-integration-tests/suites/public-api/configureScope/set_properties/test.ts b/dev-packages/node-core-integration-tests/suites/public-api/configureScope/set_properties/test.ts new file mode 100644 index 000000000000..ecfb83c3a4a3 --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/public-api/configureScope/set_properties/test.ts @@ -0,0 +1,26 @@ +import { afterAll, test } from 'vitest'; +import { cleanupChildProcesses, createRunner } from '../../../../utils/runner'; + +afterAll(() => { + cleanupChildProcesses(); +}); + +test('should set different properties of a scope', async () => { + await createRunner(__dirname, 'scenario.ts') + .expect({ + event: { + message: 'configured_scope', + tags: { + foo: 'bar', + }, + extra: { + qux: 'quux', + }, + user: { + id: 'baz', + }, + }, + }) + .start() + .completed(); +}); diff --git a/dev-packages/node-core-integration-tests/suites/public-api/onUnhandledRejectionIntegration/mode-none.js b/dev-packages/node-core-integration-tests/suites/public-api/onUnhandledRejectionIntegration/mode-none.js new file mode 100644 index 000000000000..7974601a0ddd --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/public-api/onUnhandledRejectionIntegration/mode-none.js @@ -0,0 +1,16 @@ +const Sentry = require('@sentry/node-core'); +const { setupOtel } = require('../../../utils/setupOtel.js'); + +const client = Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + integrations: [Sentry.onUnhandledRejectionIntegration({ mode: 'none' })], +}); + +setupOtel(client); + +setTimeout(() => { + process.stdout.write("I'm alive!"); + process.exit(0); +}, 500); + +Promise.reject('test rejection'); diff --git a/dev-packages/node-core-integration-tests/suites/public-api/onUnhandledRejectionIntegration/mode-strict.js b/dev-packages/node-core-integration-tests/suites/public-api/onUnhandledRejectionIntegration/mode-strict.js new file mode 100644 index 000000000000..5a0919adabce --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/public-api/onUnhandledRejectionIntegration/mode-strict.js @@ -0,0 +1,17 @@ +const Sentry = require('@sentry/node-core'); +const { setupOtel } = require('../../../utils/setupOtel.js'); + +const client = Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + integrations: [Sentry.onUnhandledRejectionIntegration({ mode: 'strict' })], +}); + +setupOtel(client); + +setTimeout(() => { + // should not be called + process.stdout.write("I'm alive!"); + process.exit(0); +}, 500); + +Promise.reject('test rejection'); diff --git a/dev-packages/node-core-integration-tests/suites/public-api/onUnhandledRejectionIntegration/mode-warn-error.js b/dev-packages/node-core-integration-tests/suites/public-api/onUnhandledRejectionIntegration/mode-warn-error.js new file mode 100644 index 000000000000..1e4e209a610a --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/public-api/onUnhandledRejectionIntegration/mode-warn-error.js @@ -0,0 +1,15 @@ +const Sentry = require('@sentry/node-core'); +const { setupOtel } = require('../../../utils/setupOtel.js'); + +const client = Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', +}); + +setupOtel(client); + +setTimeout(() => { + process.stdout.write("I'm alive!"); + process.exit(0); +}, 500); + +Promise.reject(new Error('test rejection')); diff --git a/dev-packages/node-core-integration-tests/suites/public-api/onUnhandledRejectionIntegration/mode-warn-string.js b/dev-packages/node-core-integration-tests/suites/public-api/onUnhandledRejectionIntegration/mode-warn-string.js new file mode 100644 index 000000000000..a80cada0d039 --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/public-api/onUnhandledRejectionIntegration/mode-warn-string.js @@ -0,0 +1,15 @@ +const Sentry = require('@sentry/node-core'); +const { setupOtel } = require('../../../utils/setupOtel.js'); + +const client = Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', +}); + +setupOtel(client); + +setTimeout(() => { + process.stdout.write("I'm alive!"); + process.exit(0); +}, 500); + +Promise.reject('test rejection'); diff --git a/dev-packages/node-core-integration-tests/suites/public-api/onUnhandledRejectionIntegration/scenario-strict.ts b/dev-packages/node-core-integration-tests/suites/public-api/onUnhandledRejectionIntegration/scenario-strict.ts new file mode 100644 index 000000000000..fa5f165582e7 --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/public-api/onUnhandledRejectionIntegration/scenario-strict.ts @@ -0,0 +1,15 @@ +import * as Sentry from '@sentry/node-core'; +import { loggingTransport } from '@sentry-internal/node-integration-tests'; +import { setupOtel } from '../../../utils/setupOtel'; + +const client = Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', + transport: loggingTransport, + integrations: [Sentry.onUnhandledRejectionIntegration({ mode: 'strict' })], +}); + +setupOtel(client); + +// eslint-disable-next-line @typescript-eslint/no-floating-promises +Promise.reject('test rejection'); diff --git a/dev-packages/node-core-integration-tests/suites/public-api/onUnhandledRejectionIntegration/scenario-warn.ts b/dev-packages/node-core-integration-tests/suites/public-api/onUnhandledRejectionIntegration/scenario-warn.ts new file mode 100644 index 000000000000..b2babced8554 --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/public-api/onUnhandledRejectionIntegration/scenario-warn.ts @@ -0,0 +1,14 @@ +import * as Sentry from '@sentry/node-core'; +import { loggingTransport } from '@sentry-internal/node-integration-tests'; +import { setupOtel } from '../../../utils/setupOtel'; + +const client = Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', + transport: loggingTransport, +}); + +setupOtel(client); + +// eslint-disable-next-line @typescript-eslint/no-floating-promises +Promise.reject('test rejection'); diff --git a/dev-packages/node-core-integration-tests/suites/public-api/onUnhandledRejectionIntegration/test.ts b/dev-packages/node-core-integration-tests/suites/public-api/onUnhandledRejectionIntegration/test.ts new file mode 100644 index 000000000000..2f4a22c835a4 --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/public-api/onUnhandledRejectionIntegration/test.ts @@ -0,0 +1,126 @@ +import * as childProcess from 'child_process'; +import * as path from 'path'; +import { afterAll, describe, expect, test } from 'vitest'; +import { cleanupChildProcesses, createRunner } from '../../../utils/runner'; + +describe('onUnhandledRejectionIntegration', () => { + afterAll(() => { + cleanupChildProcesses(); + }); + + test('should show string-type promise rejection warnings by default', () => + new Promise(done => { + expect.assertions(3); + + const testScriptPath = path.resolve(__dirname, 'mode-warn-string.js'); + + childProcess.execFile('node', [testScriptPath], { encoding: 'utf8' }, (err, stdout, stderr) => { + expect(err).toBeNull(); + expect(stdout).toBe("I'm alive!"); + expect(stderr.trim()) + .toBe(`This error originated either by throwing inside of an async function without a catch block, or by rejecting a promise which was not handled with .catch(). The promise rejected with the reason: +test rejection`); + done(); + }); + })); + + test('should show error-type promise rejection warnings by default', () => + new Promise(done => { + expect.assertions(3); + + const testScriptPath = path.resolve(__dirname, 'mode-warn-error.js'); + + childProcess.execFile('node', [testScriptPath], { encoding: 'utf8' }, (err, stdout, stderr) => { + expect(err).toBeNull(); + expect(stdout).toBe("I'm alive!"); + expect(stderr) + .toContain(`This error originated either by throwing inside of an async function without a catch block, or by rejecting a promise which was not handled with .catch(). The promise rejected with the reason: +Error: test rejection + at Object.`); + done(); + }); + })); + + test('should not close process on unhandled rejection in strict mode', () => + new Promise(done => { + expect.assertions(4); + + const testScriptPath = path.resolve(__dirname, 'mode-strict.js'); + + childProcess.execFile('node', [testScriptPath], { encoding: 'utf8' }, (err, stdout, stderr) => { + expect(err).not.toBeNull(); + expect(err?.code).toBe(1); + expect(stdout).not.toBe("I'm alive!"); + expect(stderr) + .toContain(`This error originated either by throwing inside of an async function without a catch block, or by rejecting a promise which was not handled with .catch(). The promise rejected with the reason: +test rejection`); + done(); + }); + })); + + test('should not close process or warn on unhandled rejection in none mode', () => + new Promise(done => { + expect.assertions(3); + + const testScriptPath = path.resolve(__dirname, 'mode-none.js'); + + childProcess.execFile('node', [testScriptPath], { encoding: 'utf8' }, (err, stdout, stderr) => { + expect(err).toBeNull(); + expect(stdout).toBe("I'm alive!"); + expect(stderr).toBe(''); + done(); + }); + })); + + test('captures exceptions for unhandled rejections', async () => { + await createRunner(__dirname, 'scenario-warn.ts') + .expect({ + event: { + level: 'error', + exception: { + values: [ + { + type: 'Error', + value: 'test rejection', + mechanism: { + type: 'onunhandledrejection', + handled: false, + }, + stacktrace: { + frames: expect.any(Array), + }, + }, + ], + }, + }, + }) + .start() + .completed(); + }); + + test('captures exceptions for unhandled rejections in strict mode', async () => { + await createRunner(__dirname, 'scenario-strict.ts') + .expect({ + event: { + level: 'fatal', + exception: { + values: [ + { + type: 'Error', + value: 'test rejection', + mechanism: { + type: 'onunhandledrejection', + handled: false, + }, + stacktrace: { + frames: expect.any(Array), + }, + }, + ], + }, + }, + }) + .start() + .completed(); + }); +}); diff --git a/dev-packages/node-core-integration-tests/suites/public-api/scopes/initialScopes/scenario.ts b/dev-packages/node-core-integration-tests/suites/public-api/scopes/initialScopes/scenario.ts new file mode 100644 index 000000000000..b2d85041bae7 --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/public-api/scopes/initialScopes/scenario.ts @@ -0,0 +1,28 @@ +import * as Sentry from '@sentry/node-core'; +import { loggingTransport } from '@sentry-internal/node-integration-tests'; +import { setupOtel } from '../../../../utils/setupOtel'; + +const client = Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', + transport: loggingTransport, +}); + +setupOtel(client); + +const globalScope = Sentry.getGlobalScope(); +const isolationScope = Sentry.getIsolationScope(); +const currentScope = Sentry.getCurrentScope(); + +globalScope.setExtra('aa', 'aa'); +isolationScope.setExtra('bb', 'bb'); +currentScope.setExtra('cc', 'cc'); + +Sentry.captureMessage('outer_before'); + +Sentry.withScope(scope => { + scope.setExtra('dd', 'dd'); + Sentry.captureMessage('inner'); +}); + +Sentry.captureMessage('outer_after'); diff --git a/dev-packages/node-core-integration-tests/suites/public-api/scopes/initialScopes/test.ts b/dev-packages/node-core-integration-tests/suites/public-api/scopes/initialScopes/test.ts new file mode 100644 index 000000000000..8f16958cc1c9 --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/public-api/scopes/initialScopes/test.ts @@ -0,0 +1,40 @@ +import { afterAll, test } from 'vitest'; +import { cleanupChildProcesses, createRunner } from '../../../../utils/runner'; + +afterAll(() => { + cleanupChildProcesses(); +}); + +test('should apply scopes correctly', async () => { + await createRunner(__dirname, 'scenario.ts') + .expect({ + event: { + message: 'outer_before', + extra: { + aa: 'aa', + bb: 'bb', + }, + }, + }) + .expect({ + event: { + message: 'inner', + extra: { + aa: 'aa', + bb: 'bb', + cc: 'cc', + }, + }, + }) + .expect({ + event: { + message: 'outer_after', + extra: { + aa: 'aa', + bb: 'bb', + }, + }, + }) + .start() + .completed(); +}); diff --git a/dev-packages/node-core-integration-tests/suites/public-api/scopes/isolationScope/scenario.ts b/dev-packages/node-core-integration-tests/suites/public-api/scopes/isolationScope/scenario.ts new file mode 100644 index 000000000000..4bf13819f0f5 --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/public-api/scopes/isolationScope/scenario.ts @@ -0,0 +1,35 @@ +import * as Sentry from '@sentry/node-core'; +import { loggingTransport } from '@sentry-internal/node-integration-tests'; +import { setupOtel } from '../../../../utils/setupOtel'; + +const client = Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', + transport: loggingTransport, +}); + +setupOtel(client); + +const globalScope = Sentry.getGlobalScope(); +const isolationScope = Sentry.getIsolationScope(); +const currentScope = Sentry.getCurrentScope(); + +globalScope.setExtra('aa', 'aa'); +isolationScope.setExtra('bb', 'bb'); +currentScope.setExtra('cc', 'cc'); + +Sentry.captureMessage('outer_before'); + +Sentry.withScope(scope => { + Sentry.getIsolationScope().setExtra('dd', 'dd'); + scope.setExtra('ee', 'ee'); + Sentry.captureMessage('inner'); +}); + +Sentry.withIsolationScope(() => { + Sentry.getIsolationScope().setExtra('ff', 'ff'); + Sentry.getCurrentScope().setExtra('gg', 'gg'); + Sentry.captureMessage('inner_async_context'); +}); + +Sentry.captureMessage('outer_after'); diff --git a/dev-packages/node-core-integration-tests/suites/public-api/scopes/isolationScope/test.ts b/dev-packages/node-core-integration-tests/suites/public-api/scopes/isolationScope/test.ts new file mode 100644 index 000000000000..eb926423ef58 --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/public-api/scopes/isolationScope/test.ts @@ -0,0 +1,57 @@ +import { afterAll, test } from 'vitest'; +import { cleanupChildProcesses, createRunner } from '../../../../utils/runner'; + +afterAll(() => { + cleanupChildProcesses(); +}); + +test('should apply scopes correctly', async () => { + await createRunner(__dirname, 'scenario.ts') + .expect({ + event: { + message: 'outer_before', + extra: { + aa: 'aa', + bb: 'bb', + }, + }, + }) + .expect({ + event: { + message: 'inner', + extra: { + aa: 'aa', + bb: 'bb', + cc: 'cc', + dd: 'dd', + ee: 'ee', + }, + }, + }) + .expect({ + event: { + message: 'inner_async_context', + extra: { + aa: 'aa', + bb: 'bb', + cc: 'cc', + dd: 'dd', + ff: 'ff', + gg: 'gg', + }, + }, + }) + .expect({ + event: { + message: 'outer_after', + extra: { + aa: 'aa', + bb: 'bb', + cc: 'cc', + dd: 'dd', + }, + }, + }) + .start() + .completed(); +}); diff --git a/dev-packages/node-core-integration-tests/suites/public-api/setContext/multiple-contexts/scenario.ts b/dev-packages/node-core-integration-tests/suites/public-api/setContext/multiple-contexts/scenario.ts new file mode 100644 index 000000000000..3a67c4ec8f78 --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/public-api/setContext/multiple-contexts/scenario.ts @@ -0,0 +1,27 @@ +import * as Sentry from '@sentry/node-core'; +import { loggingTransport } from '@sentry-internal/node-integration-tests'; +import { setupOtel } from '../../../../utils/setupOtel'; + +const client = Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', + transport: loggingTransport, +}); + +setupOtel(client); + +Sentry.setContext('context_1', { + foo: 'bar', + baz: { + qux: 'quux', + }, +}); + +Sentry.setContext('context_2', { + 1: 'foo', + bar: false, +}); + +Sentry.setContext('context_3', null); + +Sentry.captureMessage('multiple_contexts'); diff --git a/dev-packages/node-core-integration-tests/suites/public-api/setContext/multiple-contexts/test.ts b/dev-packages/node-core-integration-tests/suites/public-api/setContext/multiple-contexts/test.ts new file mode 100644 index 000000000000..1cf8342e2f29 --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/public-api/setContext/multiple-contexts/test.ts @@ -0,0 +1,24 @@ +import { afterAll, test } from 'vitest'; +import { cleanupChildProcesses, createRunner } from '../../../../utils/runner'; + +afterAll(() => { + cleanupChildProcesses(); +}); + +test('should record multiple contexts', async () => { + await createRunner(__dirname, 'scenario.ts') + .expect({ + event: { + message: 'multiple_contexts', + contexts: { + context_1: { + foo: 'bar', + baz: { qux: 'quux' }, + }, + context_2: { 1: 'foo', bar: false }, + }, + }, + }) + .start() + .completed(); +}); diff --git a/dev-packages/node-core-integration-tests/suites/public-api/setContext/non-serializable-context/scenario.ts b/dev-packages/node-core-integration-tests/suites/public-api/setContext/non-serializable-context/scenario.ts new file mode 100644 index 000000000000..fdeb4cd4a121 --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/public-api/setContext/non-serializable-context/scenario.ts @@ -0,0 +1,22 @@ +import * as Sentry from '@sentry/node-core'; +import { loggingTransport } from '@sentry-internal/node-integration-tests'; +import { setupOtel } from '../../../../utils/setupOtel'; + +const client = Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', + transport: loggingTransport, +}); + +setupOtel(client); + +type Circular = { + self?: Circular; +}; + +const objCircular: Circular = {}; +objCircular.self = objCircular; + +Sentry.setContext('non_serializable', objCircular); + +Sentry.captureMessage('non_serializable'); diff --git a/dev-packages/node-core-integration-tests/suites/public-api/setContext/non-serializable-context/test.ts b/dev-packages/node-core-integration-tests/suites/public-api/setContext/non-serializable-context/test.ts new file mode 100644 index 000000000000..34c962e5e216 --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/public-api/setContext/non-serializable-context/test.ts @@ -0,0 +1,13 @@ +import { afterAll, test } from 'vitest'; +import { cleanupChildProcesses, createRunner } from '../../../../utils/runner'; + +afterAll(() => { + cleanupChildProcesses(); +}); + +test('should normalize non-serializable context', async () => { + await createRunner(__dirname, 'scenario.ts') + .expect({ event: { message: 'non_serializable', contexts: {} } }) + .start() + .completed(); +}); diff --git a/dev-packages/node-core-integration-tests/suites/public-api/setContext/simple-context/scenario.ts b/dev-packages/node-core-integration-tests/suites/public-api/setContext/simple-context/scenario.ts new file mode 100644 index 000000000000..4fa05a4ae2ba --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/public-api/setContext/simple-context/scenario.ts @@ -0,0 +1,14 @@ +import * as Sentry from '@sentry/node-core'; +import { loggingTransport } from '@sentry-internal/node-integration-tests'; +import { setupOtel } from '../../../../utils/setupOtel'; + +const client = Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', + transport: loggingTransport, +}); + +setupOtel(client); + +Sentry.setContext('foo', { bar: 'baz' }); +Sentry.captureMessage('simple_context_object'); diff --git a/dev-packages/node-core-integration-tests/suites/public-api/setContext/simple-context/test.ts b/dev-packages/node-core-integration-tests/suites/public-api/setContext/simple-context/test.ts new file mode 100644 index 000000000000..3c28a109130b --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/public-api/setContext/simple-context/test.ts @@ -0,0 +1,22 @@ +import { afterAll, test } from 'vitest'; +import { cleanupChildProcesses, createRunner } from '../../../../utils/runner'; + +afterAll(() => { + cleanupChildProcesses(); +}); + +test('should set a simple context', async () => { + await createRunner(__dirname, 'scenario.ts') + .expect({ + event: { + message: 'simple_context_object', + contexts: { + foo: { + bar: 'baz', + }, + }, + }, + }) + .start() + .completed(); +}); diff --git a/dev-packages/node-core-integration-tests/suites/public-api/setExtra/multiple-extras/scenario.ts b/dev-packages/node-core-integration-tests/suites/public-api/setExtra/multiple-extras/scenario.ts new file mode 100644 index 000000000000..f8275d63986e --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/public-api/setExtra/multiple-extras/scenario.ts @@ -0,0 +1,22 @@ +import * as Sentry from '@sentry/node-core'; +import { loggingTransport } from '@sentry-internal/node-integration-tests'; +import { setupOtel } from '../../../../utils/setupOtel'; + +const client = Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', + transport: loggingTransport, +}); + +setupOtel(client); + +Sentry.setExtra('extra_1', { + foo: 'bar', + baz: { + qux: 'quux', + }, +}); + +Sentry.setExtra('extra_2', false); + +Sentry.captureMessage('multiple_extras'); diff --git a/dev-packages/node-core-integration-tests/suites/public-api/setExtra/multiple-extras/test.ts b/dev-packages/node-core-integration-tests/suites/public-api/setExtra/multiple-extras/test.ts new file mode 100644 index 000000000000..f40d56af6579 --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/public-api/setExtra/multiple-extras/test.ts @@ -0,0 +1,21 @@ +import { afterAll, test } from 'vitest'; +import { cleanupChildProcesses, createRunner } from '../../../../utils/runner'; + +afterAll(() => { + cleanupChildProcesses(); +}); + +test('should record multiple extras of different types', async () => { + await createRunner(__dirname, 'scenario.ts') + .expect({ + event: { + message: 'multiple_extras', + extra: { + extra_1: { foo: 'bar', baz: { qux: 'quux' } }, + extra_2: false, + }, + }, + }) + .start() + .completed(); +}); diff --git a/dev-packages/node-core-integration-tests/suites/public-api/setExtra/non-serializable-extra/scenario.ts b/dev-packages/node-core-integration-tests/suites/public-api/setExtra/non-serializable-extra/scenario.ts new file mode 100644 index 000000000000..1d4eedf9ccc9 --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/public-api/setExtra/non-serializable-extra/scenario.ts @@ -0,0 +1,22 @@ +import * as Sentry from '@sentry/node-core'; +import { loggingTransport } from '@sentry-internal/node-integration-tests'; +import { setupOtel } from '../../../../utils/setupOtel'; + +const client = Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', + transport: loggingTransport, +}); + +setupOtel(client); + +type Circular = { + self?: Circular; +}; + +const objCircular: Circular = {}; +objCircular.self = objCircular; + +Sentry.setExtra('non_serializable', objCircular); + +Sentry.captureMessage('non_serializable'); diff --git a/dev-packages/node-core-integration-tests/suites/public-api/setExtra/non-serializable-extra/test.ts b/dev-packages/node-core-integration-tests/suites/public-api/setExtra/non-serializable-extra/test.ts new file mode 100644 index 000000000000..113c99883f32 --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/public-api/setExtra/non-serializable-extra/test.ts @@ -0,0 +1,18 @@ +import { afterAll, test } from 'vitest'; +import { cleanupChildProcesses, createRunner } from '../../../../utils/runner'; + +afterAll(() => { + cleanupChildProcesses(); +}); + +test('should normalize non-serializable extra', async () => { + await createRunner(__dirname, 'scenario.ts') + .expect({ + event: { + message: 'non_serializable', + extra: {}, + }, + }) + .start() + .completed(); +}); diff --git a/dev-packages/node-core-integration-tests/suites/public-api/setExtra/simple-extra/scenario.ts b/dev-packages/node-core-integration-tests/suites/public-api/setExtra/simple-extra/scenario.ts new file mode 100644 index 000000000000..87b1314979d1 --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/public-api/setExtra/simple-extra/scenario.ts @@ -0,0 +1,19 @@ +import * as Sentry from '@sentry/node-core'; +import { loggingTransport } from '@sentry-internal/node-integration-tests'; +import { setupOtel } from '../../../../utils/setupOtel'; + +const client = Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', + transport: loggingTransport, +}); + +setupOtel(client); + +Sentry.setExtra('foo', { + foo: 'bar', + baz: { + qux: 'quux', + }, +}); +Sentry.captureMessage('simple_extra'); diff --git a/dev-packages/node-core-integration-tests/suites/public-api/setExtra/simple-extra/test.ts b/dev-packages/node-core-integration-tests/suites/public-api/setExtra/simple-extra/test.ts new file mode 100644 index 000000000000..115d4ca064a4 --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/public-api/setExtra/simple-extra/test.ts @@ -0,0 +1,25 @@ +import { afterAll, test } from 'vitest'; +import { cleanupChildProcesses, createRunner } from '../../../../utils/runner'; + +afterAll(() => { + cleanupChildProcesses(); +}); + +test('should set a simple extra', async () => { + await createRunner(__dirname, 'scenario.ts') + .expect({ + event: { + message: 'simple_extra', + extra: { + foo: { + foo: 'bar', + baz: { + qux: 'quux', + }, + }, + }, + }, + }) + .start() + .completed(); +}); diff --git a/dev-packages/node-core-integration-tests/suites/public-api/setExtras/consecutive-calls/scenario.ts b/dev-packages/node-core-integration-tests/suites/public-api/setExtras/consecutive-calls/scenario.ts new file mode 100644 index 000000000000..5e60d1092008 --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/public-api/setExtras/consecutive-calls/scenario.ts @@ -0,0 +1,22 @@ +import * as Sentry from '@sentry/node-core'; +import { loggingTransport } from '@sentry-internal/node-integration-tests'; +import { setupOtel } from '../../../../utils/setupOtel'; + +const client = Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', + transport: loggingTransport, +}); + +setupOtel(client); + +Sentry.setExtras({ extra: [] }); +Sentry.setExtras({ null: 0 }); +Sentry.setExtras({ + obj: { + foo: ['bar', 'baz', 1], + }, +}); +Sentry.setExtras({ [Infinity]: 2 }); + +Sentry.captureMessage('consecutive_calls'); diff --git a/dev-packages/node-core-integration-tests/suites/public-api/setExtras/consecutive-calls/test.ts b/dev-packages/node-core-integration-tests/suites/public-api/setExtras/consecutive-calls/test.ts new file mode 100644 index 000000000000..da5dc31e9fea --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/public-api/setExtras/consecutive-calls/test.ts @@ -0,0 +1,18 @@ +import { afterAll, test } from 'vitest'; +import { cleanupChildProcesses, createRunner } from '../../../../utils/runner'; + +afterAll(() => { + cleanupChildProcesses(); +}); + +test('should set extras from multiple consecutive calls', async () => { + await createRunner(__dirname, 'scenario.ts') + .expect({ + event: { + message: 'consecutive_calls', + extra: { extra: [], Infinity: 2, null: 0, obj: { foo: ['bar', 'baz', 1] } }, + }, + }) + .start() + .completed(); +}); diff --git a/dev-packages/node-core-integration-tests/suites/public-api/setExtras/multiple-extras/scenario.ts b/dev-packages/node-core-integration-tests/suites/public-api/setExtras/multiple-extras/scenario.ts new file mode 100644 index 000000000000..36d1d9b1de92 --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/public-api/setExtras/multiple-extras/scenario.ts @@ -0,0 +1,24 @@ +import * as Sentry from '@sentry/node-core'; +import { loggingTransport } from '@sentry-internal/node-integration-tests'; +import { setupOtel } from '../../../../utils/setupOtel'; + +const client = Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', + transport: loggingTransport, +}); + +setupOtel(client); + +Sentry.setExtras({ + extra_1: [1, ['foo'], 'bar'], + extra_2: 'baz', + extra_3: Math.PI, + extra_4: { + qux: { + quux: false, + }, + }, +}); + +Sentry.captureMessage('multiple_extras'); diff --git a/dev-packages/node-core-integration-tests/suites/public-api/setExtras/multiple-extras/test.ts b/dev-packages/node-core-integration-tests/suites/public-api/setExtras/multiple-extras/test.ts new file mode 100644 index 000000000000..614a157fed14 --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/public-api/setExtras/multiple-extras/test.ts @@ -0,0 +1,23 @@ +import { afterAll, test } from 'vitest'; +import { cleanupChildProcesses, createRunner } from '../../../../utils/runner'; + +afterAll(() => { + cleanupChildProcesses(); +}); + +test('should record an extras object', async () => { + await createRunner(__dirname, 'scenario.ts') + .expect({ + event: { + message: 'multiple_extras', + extra: { + extra_1: [1, ['foo'], 'bar'], + extra_2: 'baz', + extra_3: 3.141592653589793, + extra_4: { qux: { quux: false } }, + }, + }, + }) + .start() + .completed(); +}); diff --git a/dev-packages/node-core-integration-tests/suites/public-api/setMeasurement/scenario.ts b/dev-packages/node-core-integration-tests/suites/public-api/setMeasurement/scenario.ts new file mode 100644 index 000000000000..5f6788d0f8b8 --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/public-api/setMeasurement/scenario.ts @@ -0,0 +1,19 @@ +import * as Sentry from '@sentry/node-core'; +import { loggingTransport } from '@sentry-internal/node-integration-tests'; +import { setupOtel } from '../../../utils/setupOtel'; + +const client = Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', + tracesSampleRate: 1, + transport: loggingTransport, +}); + +setupOtel(client); + +Sentry.startSpan({ name: 'some_transaction' }, () => { + Sentry.setMeasurement('metric.foo', 42, 'ms'); + Sentry.setMeasurement('metric.bar', 1337, 'nanoseconds'); + Sentry.setMeasurement('metric.baz', 99, 's'); + Sentry.setMeasurement('metric.baz', 1, ''); +}); diff --git a/dev-packages/node-core-integration-tests/suites/public-api/setMeasurement/test.ts b/dev-packages/node-core-integration-tests/suites/public-api/setMeasurement/test.ts new file mode 100644 index 000000000000..829e6a7ed3da --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/public-api/setMeasurement/test.ts @@ -0,0 +1,22 @@ +import { afterAll, test } from 'vitest'; +import { cleanupChildProcesses, createRunner } from '../../../utils/runner'; + +afterAll(() => { + cleanupChildProcesses(); +}); + +test('should attach measurement to transaction', async () => { + await createRunner(__dirname, 'scenario.ts') + .expect({ + transaction: { + transaction: 'some_transaction', + measurements: { + 'metric.foo': { value: 42, unit: 'ms' }, + 'metric.bar': { value: 1337, unit: 'nanoseconds' }, + 'metric.baz': { value: 1, unit: '' }, + }, + }, + }) + .start() + .completed(); +}); diff --git a/dev-packages/node-core-integration-tests/suites/public-api/setTag/with-primitives/scenario.ts b/dev-packages/node-core-integration-tests/suites/public-api/setTag/with-primitives/scenario.ts new file mode 100644 index 000000000000..5717d98929f4 --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/public-api/setTag/with-primitives/scenario.ts @@ -0,0 +1,20 @@ +import * as Sentry from '@sentry/node-core'; +import { loggingTransport } from '@sentry-internal/node-integration-tests'; +import { setupOtel } from '../../../../utils/setupOtel'; + +const client = Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', + transport: loggingTransport, +}); + +setupOtel(client); + +Sentry.setTag('tag_1', 'foo'); +Sentry.setTag('tag_2', Math.PI); +Sentry.setTag('tag_3', false); +Sentry.setTag('tag_4', null); +Sentry.setTag('tag_5', undefined); +Sentry.setTag('tag_6', -1); + +Sentry.captureMessage('primitive_tags'); diff --git a/dev-packages/node-core-integration-tests/suites/public-api/setTag/with-primitives/test.ts b/dev-packages/node-core-integration-tests/suites/public-api/setTag/with-primitives/test.ts new file mode 100644 index 000000000000..23e22402c666 --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/public-api/setTag/with-primitives/test.ts @@ -0,0 +1,24 @@ +import { afterAll, test } from 'vitest'; +import { cleanupChildProcesses, createRunner } from '../../../../utils/runner'; + +afterAll(() => { + cleanupChildProcesses(); +}); + +test('should set primitive tags', async () => { + await createRunner(__dirname, 'scenario.ts') + .expect({ + event: { + message: 'primitive_tags', + tags: { + tag_1: 'foo', + tag_2: 3.141592653589793, + tag_3: false, + tag_4: null, + tag_6: -1, + }, + }, + }) + .start() + .completed(); +}); diff --git a/dev-packages/node-core-integration-tests/suites/public-api/setTags/with-primitives/scenario.ts b/dev-packages/node-core-integration-tests/suites/public-api/setTags/with-primitives/scenario.ts new file mode 100644 index 000000000000..5717d98929f4 --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/public-api/setTags/with-primitives/scenario.ts @@ -0,0 +1,20 @@ +import * as Sentry from '@sentry/node-core'; +import { loggingTransport } from '@sentry-internal/node-integration-tests'; +import { setupOtel } from '../../../../utils/setupOtel'; + +const client = Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', + transport: loggingTransport, +}); + +setupOtel(client); + +Sentry.setTag('tag_1', 'foo'); +Sentry.setTag('tag_2', Math.PI); +Sentry.setTag('tag_3', false); +Sentry.setTag('tag_4', null); +Sentry.setTag('tag_5', undefined); +Sentry.setTag('tag_6', -1); + +Sentry.captureMessage('primitive_tags'); diff --git a/dev-packages/node-core-integration-tests/suites/public-api/setTags/with-primitives/test.ts b/dev-packages/node-core-integration-tests/suites/public-api/setTags/with-primitives/test.ts new file mode 100644 index 000000000000..23e22402c666 --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/public-api/setTags/with-primitives/test.ts @@ -0,0 +1,24 @@ +import { afterAll, test } from 'vitest'; +import { cleanupChildProcesses, createRunner } from '../../../../utils/runner'; + +afterAll(() => { + cleanupChildProcesses(); +}); + +test('should set primitive tags', async () => { + await createRunner(__dirname, 'scenario.ts') + .expect({ + event: { + message: 'primitive_tags', + tags: { + tag_1: 'foo', + tag_2: 3.141592653589793, + tag_3: false, + tag_4: null, + tag_6: -1, + }, + }, + }) + .start() + .completed(); +}); diff --git a/dev-packages/node-core-integration-tests/suites/public-api/setUser/unset_user/scenario.ts b/dev-packages/node-core-integration-tests/suites/public-api/setUser/unset_user/scenario.ts new file mode 100644 index 000000000000..c935f334275c --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/public-api/setUser/unset_user/scenario.ts @@ -0,0 +1,25 @@ +import * as Sentry from '@sentry/node-core'; +import { loggingTransport } from '@sentry-internal/node-integration-tests'; +import { setupOtel } from '../../../../utils/setupOtel'; + +const client = Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', + transport: loggingTransport, +}); + +setupOtel(client); + +Sentry.captureMessage('no_user'); + +Sentry.setUser({ + id: 'foo', + ip_address: 'bar', + other_key: 'baz', +}); + +Sentry.captureMessage('user'); + +Sentry.setUser(null); + +Sentry.captureMessage('unset_user'); diff --git a/dev-packages/node-core-integration-tests/suites/public-api/setUser/unset_user/test.ts b/dev-packages/node-core-integration-tests/suites/public-api/setUser/unset_user/test.ts new file mode 100644 index 000000000000..9b7f3e2c23be --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/public-api/setUser/unset_user/test.ts @@ -0,0 +1,24 @@ +import { afterAll, test } from 'vitest'; +import { cleanupChildProcesses, createRunner } from '../../../../utils/runner'; + +afterAll(() => { + cleanupChildProcesses(); +}); + +test('should unset user', async () => { + await createRunner(__dirname, 'scenario.ts') + .expect({ event: { message: 'no_user' } }) + .expect({ + event: { + message: 'user', + user: { + id: 'foo', + ip_address: 'bar', + other_key: 'baz', + }, + }, + }) + .expect({ event: { message: 'unset_user' } }) + .start() + .completed(); +}); diff --git a/dev-packages/node-core-integration-tests/suites/public-api/setUser/update_user/scenario.ts b/dev-packages/node-core-integration-tests/suites/public-api/setUser/update_user/scenario.ts new file mode 100644 index 000000000000..98e25199eb4f --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/public-api/setUser/update_user/scenario.ts @@ -0,0 +1,24 @@ +import * as Sentry from '@sentry/node-core'; +import { loggingTransport } from '@sentry-internal/node-integration-tests'; +import { setupOtel } from '../../../../utils/setupOtel'; + +const client = Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', + transport: loggingTransport, +}); + +setupOtel(client); + +Sentry.setUser({ + id: 'foo', + ip_address: 'bar', +}); + +Sentry.captureMessage('first_user'); + +Sentry.setUser({ + id: 'baz', +}); + +Sentry.captureMessage('second_user'); diff --git a/dev-packages/node-core-integration-tests/suites/public-api/setUser/update_user/test.ts b/dev-packages/node-core-integration-tests/suites/public-api/setUser/update_user/test.ts new file mode 100644 index 000000000000..7a6c89f4c213 --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/public-api/setUser/update_user/test.ts @@ -0,0 +1,29 @@ +import { afterAll, test } from 'vitest'; +import { cleanupChildProcesses, createRunner } from '../../../../utils/runner'; + +afterAll(() => { + cleanupChildProcesses(); +}); + +test('should update user', async () => { + await createRunner(__dirname, 'scenario.ts') + .expect({ + event: { + message: 'first_user', + user: { + id: 'foo', + ip_address: 'bar', + }, + }, + }) + .expect({ + event: { + message: 'second_user', + user: { + id: 'baz', + }, + }, + }) + .start() + .completed(); +}); diff --git a/dev-packages/node-core-integration-tests/suites/public-api/startSpan/basic-usage/scenario.ts b/dev-packages/node-core-integration-tests/suites/public-api/startSpan/basic-usage/scenario.ts new file mode 100644 index 000000000000..5d6d8daa517a --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/public-api/startSpan/basic-usage/scenario.ts @@ -0,0 +1,14 @@ +import * as Sentry from '@sentry/node-core'; +import { loggingTransport } from '@sentry-internal/node-integration-tests'; +import { setupOtel } from '../../../../utils/setupOtel'; + +const client = Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', + tracesSampleRate: 1.0, + transport: loggingTransport, +}); + +setupOtel(client); + +Sentry.startSpan({ name: 'test_span' }, () => undefined); diff --git a/dev-packages/node-core-integration-tests/suites/public-api/startSpan/basic-usage/test.ts b/dev-packages/node-core-integration-tests/suites/public-api/startSpan/basic-usage/test.ts new file mode 100644 index 000000000000..8a72ecd7c8b3 --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/public-api/startSpan/basic-usage/test.ts @@ -0,0 +1,45 @@ +import { SEMANTIC_ATTRIBUTE_SENTRY_SOURCE } from '@sentry/node-core'; +import { afterAll, expect, test } from 'vitest'; +import { cleanupChildProcesses, createRunner } from '../../../../utils/runner'; + +afterAll(() => { + cleanupChildProcesses(); +}); + +test('sends a manually started root span with source custom', async () => { + await createRunner(__dirname, 'scenario.ts') + .expect({ + transaction: { + transaction: 'test_span', + transaction_info: { source: 'custom' }, + contexts: { + trace: { + span_id: expect.any(String), + trace_id: expect.any(String), + data: { [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'custom' }, + }, + }, + }, + }) + .start() + .completed(); +}); + +test("doesn't change the name for manually started spans even if attributes triggering inference are set", async () => { + await createRunner(__dirname, 'scenario.ts') + .expect({ + transaction: { + transaction: 'test_span', + transaction_info: { source: 'custom' }, + contexts: { + trace: { + span_id: expect.any(String), + trace_id: expect.any(String), + data: { [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'custom' }, + }, + }, + }, + }) + .start() + .completed(); +}); diff --git a/dev-packages/node-core-integration-tests/suites/public-api/startSpan/parallel-root-spans/scenario.ts b/dev-packages/node-core-integration-tests/suites/public-api/startSpan/parallel-root-spans/scenario.ts new file mode 100644 index 000000000000..ac0892fe5f6f --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/public-api/startSpan/parallel-root-spans/scenario.ts @@ -0,0 +1,33 @@ +import * as Sentry from '@sentry/node-core'; +import { loggingTransport } from '@sentry-internal/node-integration-tests'; +import { setupOtel } from '../../../../utils/setupOtel'; + +const client = Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', + tracesSampleRate: 1.0, + transport: loggingTransport, +}); + +setupOtel(client); + +Sentry.getCurrentScope().setPropagationContext({ + parentSpanId: '1234567890123456', + traceId: '12345678901234567890123456789012', + sampleRand: Math.random(), +}); + +const spanIdTraceId = Sentry.startSpan( + { + name: 'test_span_1', + }, + span1 => span1.spanContext().traceId, +); + +Sentry.startSpan( + { + name: 'test_span_2', + attributes: { spanIdTraceId }, + }, + () => undefined, +); diff --git a/dev-packages/node-core-integration-tests/suites/public-api/startSpan/parallel-root-spans/test.ts b/dev-packages/node-core-integration-tests/suites/public-api/startSpan/parallel-root-spans/test.ts new file mode 100644 index 000000000000..e1b8f793d9b6 --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/public-api/startSpan/parallel-root-spans/test.ts @@ -0,0 +1,31 @@ +import { afterAll, expect, test } from 'vitest'; +import { cleanupChildProcesses, createRunner } from '../../../../utils/runner'; + +afterAll(() => { + cleanupChildProcesses(); +}); + +test('should send manually started parallel root spans in root context', async () => { + expect.assertions(7); + + await createRunner(__dirname, 'scenario.ts') + .expect({ transaction: { transaction: 'test_span_1' } }) + .expect({ + transaction: transaction => { + expect(transaction).toBeDefined(); + const traceId = transaction.contexts?.trace?.trace_id; + expect(traceId).toBeDefined(); + + // It ignores propagation context of the root context + expect(traceId).not.toBe('12345678901234567890123456789012'); + expect(transaction.contexts?.trace?.parent_span_id).toBeUndefined(); + + // Different trace ID than the first span + const trace1Id = transaction.contexts?.trace?.data?.spanIdTraceId; + expect(trace1Id).toBeDefined(); + expect(trace1Id).not.toBe(traceId); + }, + }) + .start() + .completed(); +}); diff --git a/dev-packages/node-core-integration-tests/suites/public-api/startSpan/parallel-spans-in-scope-with-parentSpanId/scenario.ts b/dev-packages/node-core-integration-tests/suites/public-api/startSpan/parallel-spans-in-scope-with-parentSpanId/scenario.ts new file mode 100644 index 000000000000..3c8a707e9919 --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/public-api/startSpan/parallel-spans-in-scope-with-parentSpanId/scenario.ts @@ -0,0 +1,35 @@ +import * as Sentry from '@sentry/node-core'; +import { loggingTransport } from '@sentry-internal/node-integration-tests'; +import { setupOtel } from '../../../../utils/setupOtel'; + +const client = Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', + tracesSampleRate: 1.0, + transport: loggingTransport, +}); + +setupOtel(client); + +Sentry.withScope(scope => { + scope.setPropagationContext({ + parentSpanId: '1234567890123456', + traceId: '12345678901234567890123456789012', + sampleRand: Math.random(), + }); + + const spanIdTraceId = Sentry.startSpan( + { + name: 'test_span_1', + }, + span1 => span1.spanContext().traceId, + ); + + Sentry.startSpan( + { + name: 'test_span_2', + attributes: { spanIdTraceId }, + }, + () => undefined, + ); +}); diff --git a/dev-packages/node-core-integration-tests/suites/public-api/startSpan/parallel-spans-in-scope-with-parentSpanId/test.ts b/dev-packages/node-core-integration-tests/suites/public-api/startSpan/parallel-spans-in-scope-with-parentSpanId/test.ts new file mode 100644 index 000000000000..e10a1210a0c9 --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/public-api/startSpan/parallel-spans-in-scope-with-parentSpanId/test.ts @@ -0,0 +1,27 @@ +import { afterAll, expect, test } from 'vitest'; +import { cleanupChildProcesses, createRunner } from '../../../../utils/runner'; + +afterAll(() => { + cleanupChildProcesses(); +}); + +test('should send manually started parallel root spans outside of root context with parentSpanId', async () => { + await createRunner(__dirname, 'scenario.ts') + .expect({ transaction: { transaction: 'test_span_1' } }) + .expect({ + transaction: transaction => { + expect(transaction).toBeDefined(); + const traceId = transaction.contexts?.trace?.trace_id; + expect(traceId).toBeDefined(); + expect(transaction.contexts?.trace?.parent_span_id).toBeUndefined(); + + const trace1Id = transaction.contexts?.trace?.data?.spanIdTraceId; + expect(trace1Id).toBeDefined(); + + // Different trace ID as the first span + expect(trace1Id).not.toBe(traceId); + }, + }) + .start() + .completed(); +}); diff --git a/dev-packages/node-core-integration-tests/suites/public-api/startSpan/parallel-spans-in-scope/scenario.ts b/dev-packages/node-core-integration-tests/suites/public-api/startSpan/parallel-spans-in-scope/scenario.ts new file mode 100644 index 000000000000..7b5b56d18343 --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/public-api/startSpan/parallel-spans-in-scope/scenario.ts @@ -0,0 +1,29 @@ +import * as Sentry from '@sentry/node-core'; +import { loggingTransport } from '@sentry-internal/node-integration-tests'; +import { setupOtel } from '../../../../utils/setupOtel'; + +const client = Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', + tracesSampleRate: 1.0, + transport: loggingTransport, +}); + +setupOtel(client); + +Sentry.withScope(() => { + const spanIdTraceId = Sentry.startSpan( + { + name: 'test_span_1', + }, + span1 => span1.spanContext().traceId, + ); + + Sentry.startSpan( + { + name: 'test_span_2', + attributes: { spanIdTraceId }, + }, + () => undefined, + ); +}); diff --git a/dev-packages/node-core-integration-tests/suites/public-api/startSpan/parallel-spans-in-scope/test.ts b/dev-packages/node-core-integration-tests/suites/public-api/startSpan/parallel-spans-in-scope/test.ts new file mode 100644 index 000000000000..69fc2bc2774a --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/public-api/startSpan/parallel-spans-in-scope/test.ts @@ -0,0 +1,29 @@ +import { afterAll, expect, test } from 'vitest'; +import { cleanupChildProcesses, createRunner } from '../../../../utils/runner'; + +afterAll(() => { + cleanupChildProcesses(); +}); + +test('should send manually started parallel root spans outside of root context', async () => { + expect.assertions(6); + + await createRunner(__dirname, 'scenario.ts') + .expect({ transaction: { transaction: 'test_span_1' } }) + .expect({ + transaction: transaction => { + expect(transaction).toBeDefined(); + const traceId = transaction.contexts?.trace?.trace_id; + expect(traceId).toBeDefined(); + expect(transaction.contexts?.trace?.parent_span_id).toBeUndefined(); + + const trace1Id = transaction.contexts?.trace?.data?.spanIdTraceId; + expect(trace1Id).toBeDefined(); + + // Different trace ID as the first span + expect(trace1Id).not.toBe(traceId); + }, + }) + .start() + .completed(); +}); diff --git a/dev-packages/node-core-integration-tests/suites/public-api/startSpan/updateName-method/scenario.ts b/dev-packages/node-core-integration-tests/suites/public-api/startSpan/updateName-method/scenario.ts new file mode 100644 index 000000000000..faa3e76bcecb --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/public-api/startSpan/updateName-method/scenario.ts @@ -0,0 +1,19 @@ +import * as Sentry from '@sentry/node-core'; +import { loggingTransport } from '@sentry-internal/node-integration-tests'; +import { setupOtel } from '../../../../utils/setupOtel'; + +const client = Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', + tracesSampleRate: 1.0, + transport: loggingTransport, +}); + +setupOtel(client); + +Sentry.startSpan( + { name: 'test_span', attributes: { [Sentry.SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'url' } }, + (span: Sentry.Span) => { + span.updateName('new name'); + }, +); diff --git a/dev-packages/node-core-integration-tests/suites/public-api/startSpan/updateName-method/test.ts b/dev-packages/node-core-integration-tests/suites/public-api/startSpan/updateName-method/test.ts new file mode 100644 index 000000000000..f54cbeb84895 --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/public-api/startSpan/updateName-method/test.ts @@ -0,0 +1,26 @@ +import { SEMANTIC_ATTRIBUTE_SENTRY_SOURCE } from '@sentry/node-core'; +import { afterAll, expect, test } from 'vitest'; +import { cleanupChildProcesses, createRunner } from '../../../../utils/runner'; + +afterAll(() => { + cleanupChildProcesses(); +}); + +test('updates the span name when calling `span.updateName`', async () => { + createRunner(__dirname, 'scenario.ts') + .expect({ + transaction: { + transaction: 'new name', + transaction_info: { source: 'url' }, + contexts: { + trace: { + span_id: expect.any(String), + trace_id: expect.any(String), + data: { [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'url' }, + }, + }, + }, + }) + .start() + .completed(); +}); diff --git a/dev-packages/node-core-integration-tests/suites/public-api/startSpan/updateSpanName-function/scenario.ts b/dev-packages/node-core-integration-tests/suites/public-api/startSpan/updateSpanName-function/scenario.ts new file mode 100644 index 000000000000..e5581473b3f1 --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/public-api/startSpan/updateSpanName-function/scenario.ts @@ -0,0 +1,19 @@ +import * as Sentry from '@sentry/node-core'; +import { loggingTransport } from '@sentry-internal/node-integration-tests'; +import { setupOtel } from '../../../../utils/setupOtel'; + +const client = Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', + tracesSampleRate: 1.0, + transport: loggingTransport, +}); + +setupOtel(client); + +Sentry.startSpan( + { name: 'test_span', attributes: { [Sentry.SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'url' } }, + (span: Sentry.Span) => { + Sentry.updateSpanName(span, 'new name'); + }, +); diff --git a/dev-packages/node-core-integration-tests/suites/public-api/startSpan/updateSpanName-function/test.ts b/dev-packages/node-core-integration-tests/suites/public-api/startSpan/updateSpanName-function/test.ts new file mode 100644 index 000000000000..faa6a674bfc6 --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/public-api/startSpan/updateSpanName-function/test.ts @@ -0,0 +1,26 @@ +import { SEMANTIC_ATTRIBUTE_SENTRY_SOURCE } from '@sentry/node-core'; +import { afterAll, expect, test } from 'vitest'; +import { cleanupChildProcesses, createRunner } from '../../../../utils/runner'; + +afterAll(() => { + cleanupChildProcesses(); +}); + +test('updates the span name and source when calling `updateSpanName`', async () => { + await createRunner(__dirname, 'scenario.ts') + .expect({ + transaction: { + transaction: 'new name', + transaction_info: { source: 'custom' }, + contexts: { + trace: { + span_id: expect.any(String), + trace_id: expect.any(String), + data: { [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'custom' }, + }, + }, + }, + }) + .start() + .completed(); +}); diff --git a/dev-packages/node-core-integration-tests/suites/public-api/startSpan/with-nested-spans/scenario.ts b/dev-packages/node-core-integration-tests/suites/public-api/startSpan/with-nested-spans/scenario.ts new file mode 100644 index 000000000000..d1a2a44382cf --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/public-api/startSpan/with-nested-spans/scenario.ts @@ -0,0 +1,36 @@ +import * as Sentry from '@sentry/node-core'; +import { loggingTransport } from '@sentry-internal/node-integration-tests'; +import { setupOtel } from '../../../../utils/setupOtel'; + +const client = Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', + tracesSampleRate: 1.0, + transport: loggingTransport, +}); + +setupOtel(client); + +Sentry.startSpan({ name: 'root_span' }, () => { + Sentry.startSpan( + { + name: 'span_1', + attributes: { + foo: 'bar', + baz: [1, 2, 3], + }, + }, + () => undefined, + ); + + // span_2 doesn't finish + Sentry.startInactiveSpan({ name: 'span_2' }); + + Sentry.startSpan({ name: 'span_3' }, () => { + // span_4 is the child of span_3 but doesn't finish. + Sentry.startInactiveSpan({ name: 'span_4', attributes: { qux: 'quux' } }); + + // span_5 is another child of span_3 but finishes. + Sentry.startSpan({ name: 'span_5' }, () => undefined); + }); +}); diff --git a/dev-packages/node-core-integration-tests/suites/public-api/startSpan/with-nested-spans/test.ts b/dev-packages/node-core-integration-tests/suites/public-api/startSpan/with-nested-spans/test.ts new file mode 100644 index 000000000000..c01b837db5f7 --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/public-api/startSpan/with-nested-spans/test.ts @@ -0,0 +1,47 @@ +import type { SpanJSON } from '@sentry/core'; +import { afterAll, expect, test } from 'vitest'; +import { assertSentryTransaction } from '../../../../utils/assertions'; +import { cleanupChildProcesses, createRunner } from '../../../../utils/runner'; + +afterAll(() => { + cleanupChildProcesses(); +}); + +test('should report finished spans as children of the root transaction.', async () => { + await createRunner(__dirname, 'scenario.ts') + .expect({ + transaction: transaction => { + const rootSpanId = transaction.contexts?.trace?.span_id; + const span3Id = transaction.spans?.[1]?.span_id; + + expect(rootSpanId).toEqual(expect.any(String)); + expect(span3Id).toEqual(expect.any(String)); + + assertSentryTransaction(transaction, { + transaction: 'root_span', + spans: [ + { + description: 'span_1', + data: { + foo: 'bar', + baz: [1, 2, 3], + }, + parent_span_id: rootSpanId, + }, + { + description: 'span_3', + parent_span_id: rootSpanId, + data: {}, + }, + { + description: 'span_5', + parent_span_id: span3Id, + data: {}, + }, + ] as SpanJSON[], + }); + }, + }) + .start() + .completed(); +}); diff --git a/dev-packages/node-core-integration-tests/suites/public-api/withScope/nested-scopes/scenario.ts b/dev-packages/node-core-integration-tests/suites/public-api/withScope/nested-scopes/scenario.ts new file mode 100644 index 000000000000..d1a24307172a --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/public-api/withScope/nested-scopes/scenario.ts @@ -0,0 +1,30 @@ +import * as Sentry from '@sentry/node-core'; +import { loggingTransport } from '@sentry-internal/node-integration-tests'; +import { setupOtel } from '../../../../utils/setupOtel'; + +const client = Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', + transport: loggingTransport, +}); + +setupOtel(client); + +Sentry.setUser({ id: 'qux' }); +Sentry.captureMessage('root_before'); + +Sentry.withScope(scope => { + scope.setTag('foo', false); + Sentry.captureMessage('outer_before'); + + Sentry.withScope(scope => { + scope.setTag('bar', 10); + scope.setUser(null); + Sentry.captureMessage('inner'); + }); + + scope.setUser({ id: 'baz' }); + Sentry.captureMessage('outer_after'); +}); + +Sentry.captureMessage('root_after'); diff --git a/dev-packages/node-core-integration-tests/suites/public-api/withScope/nested-scopes/test.ts b/dev-packages/node-core-integration-tests/suites/public-api/withScope/nested-scopes/test.ts new file mode 100644 index 000000000000..4e646a233443 --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/public-api/withScope/nested-scopes/test.ts @@ -0,0 +1,59 @@ +import { afterAll, test } from 'vitest'; +import { cleanupChildProcesses, createRunner } from '../../../../utils/runner'; + +afterAll(() => { + cleanupChildProcesses(); +}); + +test('should allow nested scoping', async () => { + await createRunner(__dirname, 'scenario.ts') + .expect({ + event: { + message: 'root_before', + user: { + id: 'qux', + }, + }, + }) + .expect({ + event: { + message: 'outer_before', + user: { + id: 'qux', + }, + tags: { + foo: false, + }, + }, + }) + .expect({ + event: { + message: 'inner', + tags: { + foo: false, + bar: 10, + }, + }, + }) + .expect({ + event: { + message: 'outer_after', + user: { + id: 'baz', + }, + tags: { + foo: false, + }, + }, + }) + .expect({ + event: { + message: 'root_after', + user: { + id: 'qux', + }, + }, + }) + .start() + .completed(); +}); diff --git a/dev-packages/node-core-integration-tests/suites/sessions/errored-session-aggregate/test.ts b/dev-packages/node-core-integration-tests/suites/sessions/errored-session-aggregate/test.ts new file mode 100644 index 000000000000..ba8110a62675 --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/sessions/errored-session-aggregate/test.ts @@ -0,0 +1,29 @@ +import { afterAll, expect, test } from 'vitest'; +import { cleanupChildProcesses, createRunner } from '../../../utils/runner'; + +afterAll(() => { + cleanupChildProcesses(); +}); + +test('should aggregate successful, crashed and erroneous sessions', async () => { + const runner = createRunner(__dirname, '..', 'server.ts') + .ignore('transaction', 'event') + .unignore('sessions') + .expect({ + sessions: { + aggregates: [ + { + started: expect.any(String), + exited: 2, + errored: 1, + }, + ], + }, + }) + .start(); + + runner.makeRequest('get', '/test/success'); + runner.makeRequest('get', '/test/error_handled'); + runner.makeRequest('get', '/test/error_unhandled', { expectError: true }); + await runner.completed(); +}); diff --git a/dev-packages/node-core-integration-tests/suites/sessions/exited-session-aggregate/test.ts b/dev-packages/node-core-integration-tests/suites/sessions/exited-session-aggregate/test.ts new file mode 100644 index 000000000000..228ee9a98643 --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/sessions/exited-session-aggregate/test.ts @@ -0,0 +1,28 @@ +import { afterAll, expect, test } from 'vitest'; +import { cleanupChildProcesses, createRunner } from '../../../utils/runner'; + +afterAll(() => { + cleanupChildProcesses(); +}); + +test('should aggregate successful sessions', async () => { + const runner = createRunner(__dirname, '..', 'server.ts') + .ignore('transaction', 'event') + .unignore('sessions') + .expect({ + sessions: { + aggregates: [ + { + started: expect.any(String), + exited: 3, + }, + ], + }, + }) + .start(); + + runner.makeRequest('get', '/test/success'); + runner.makeRequest('get', '/test/success_next'); + runner.makeRequest('get', '/test/success_slow'); + await runner.completed(); +}); diff --git a/dev-packages/node-core-integration-tests/suites/sessions/server.ts b/dev-packages/node-core-integration-tests/suites/sessions/server.ts new file mode 100644 index 000000000000..5638b0946d73 --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/sessions/server.ts @@ -0,0 +1,51 @@ +import * as Sentry from '@sentry/node-core'; +import { loggingTransport, startExpressServerAndSendPortToRunner } from '@sentry-internal/node-core-integration-tests'; +import { setupOtel } from '../../utils/setupOtel'; + +const client = Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', + transport: loggingTransport, + integrations: [ + Sentry.httpIntegration({ + // Flush after 2 seconds (to avoid waiting for the default 60s) + sessionFlushingDelayMS: 2_000, + }), + ], +}); + +setupOtel(client); + +import express from 'express'; + +const app = express(); + +app.get('/test/success', (_req, res) => { + res.send('Success!'); +}); + +app.get('/test/success_next', (_req, res, next) => { + res.send('Success!'); + next(); +}); + +app.get('/test/success_slow', async (_req, res) => { + await new Promise(res => setTimeout(res, 50)); + + res.send('Success!'); +}); + +app.get('/test/error_unhandled', () => { + throw new Error('Crash!'); +}); + +app.get('/test/error_handled', (_req, res) => { + try { + throw new Error('Crash!'); + } catch (e) { + Sentry.captureException(e); + } + res.send('Crash!'); +}); + +startExpressServerAndSendPortToRunner(app); diff --git a/dev-packages/node-core-integration-tests/suites/tracing/dsc-txn-name-update/scenario-events.ts b/dev-packages/node-core-integration-tests/suites/tracing/dsc-txn-name-update/scenario-events.ts new file mode 100644 index 000000000000..034f8b1f60e4 --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/tracing/dsc-txn-name-update/scenario-events.ts @@ -0,0 +1,33 @@ +import * as Sentry from '@sentry/node-core'; +import { loggingTransport } from '@sentry-internal/node-core-integration-tests'; +import { setupOtel } from '../../../utils/setupOtel'; + +const client = Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', + transport: loggingTransport, + tracesSampleRate: 1, + environment: 'production', +}); + +setupOtel(client); + +// eslint-disable-next-line @typescript-eslint/no-floating-promises +Sentry.startSpan( + { name: 'initial-name', attributes: { [Sentry.SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'url' } }, + async span => { + Sentry.captureMessage('message-1'); + + span.updateName('updated-name-1'); + span.setAttribute(Sentry.SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, 'route'); + + Sentry.captureMessage('message-2'); + + span.updateName('updated-name-2'); + span.setAttribute(Sentry.SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, 'custom'); + + Sentry.captureMessage('message-3'); + + span.end(); + }, +); diff --git a/dev-packages/node-core-integration-tests/suites/tracing/dsc-txn-name-update/scenario-headers.ts b/dev-packages/node-core-integration-tests/suites/tracing/dsc-txn-name-update/scenario-headers.ts new file mode 100644 index 000000000000..1f7d68340f79 --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/tracing/dsc-txn-name-update/scenario-headers.ts @@ -0,0 +1,60 @@ +import { HttpInstrumentation } from '@opentelemetry/instrumentation-http'; +import * as Sentry from '@sentry/node-core'; +import { loggingTransport } from '@sentry-internal/node-core-integration-tests'; +import { setupOtel } from '../../../utils/setupOtel'; + +const client = Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', + transport: loggingTransport, + tracesSampleRate: 1, + environment: 'production', + openTelemetryInstrumentations: [new HttpInstrumentation()], +}); + +setupOtel(client); + +import http from 'http'; + +// eslint-disable-next-line @typescript-eslint/no-floating-promises +Sentry.startSpan( + { + name: 'initial-name', + attributes: { [Sentry.SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'url' }, + }, + async span => { + const serverUrl = process.env.SERVER_URL; + if (!serverUrl) { + throw new Error('SERVER_URL environment variable not set'); + } + + await makeHttpRequest(`${serverUrl}/api/v0`); + + span.updateName('updated-name-1'); + span.setAttribute(Sentry.SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, 'route'); + + await makeHttpRequest(`${serverUrl}/api/v1`); + + span.updateName('updated-name-2'); + span.setAttribute(Sentry.SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, 'custom'); + + await makeHttpRequest(`${serverUrl}/api/v2`); + + span.end(); + }, +); + +function makeHttpRequest(url: string): Promise { + return new Promise(resolve => { + http + .request(url, httpRes => { + httpRes.on('data', () => { + // we don't care about data + }); + httpRes.on('end', () => { + resolve(); + }); + }) + .end(); + }); +} diff --git a/dev-packages/node-core-integration-tests/suites/tracing/dsc-txn-name-update/test.ts b/dev-packages/node-core-integration-tests/suites/tracing/dsc-txn-name-update/test.ts new file mode 100644 index 000000000000..9fe401badaa7 --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/tracing/dsc-txn-name-update/test.ts @@ -0,0 +1,138 @@ +import { expect, test } from 'vitest'; +import { conditionalTest } from '../../../utils'; +import { createRunner } from '../../../utils/runner'; +import { createTestServer } from '../../../utils/server'; + +// This test requires Node.js 22+ because it depends on the 'http.client.request.created' +// diagnostic channel for baggage header propagation, which only exists since Node 22.12.0+ and 23.2.0+ +conditionalTest({ min: 22 })('node >=22', () => { + test('adds current transaction name to baggage when the txn name is high-quality', async () => { + expect.assertions(5); + + let traceId: string | undefined; + + const [SERVER_URL, closeTestServer] = await createTestServer() + .get('/api/v0', (headers: Record) => { + const baggageItems = getBaggageHeaderItems(headers); + traceId = baggageItems.find(item => item.startsWith('sentry-trace_id='))?.split('=')[1] as string; + + expect(traceId).toMatch(/^[0-9a-f]{32}$/); + + expect(baggageItems).toEqual([ + 'sentry-environment=production', + 'sentry-public_key=public', + 'sentry-release=1.0', + expect.stringMatching(/sentry-sample_rand=0\.[0-9]+/), + 'sentry-sample_rate=1', + 'sentry-sampled=true', + `sentry-trace_id=${traceId}`, + ]); + }) + .get('/api/v1', (headers: Record) => { + expect(getBaggageHeaderItems(headers)).toEqual([ + 'sentry-environment=production', + 'sentry-public_key=public', + 'sentry-release=1.0', + expect.stringMatching(/sentry-sample_rand=0\.[0-9]+/), + 'sentry-sample_rate=1', + 'sentry-sampled=true', + `sentry-trace_id=${traceId}`, + 'sentry-transaction=updated-name-1', + ]); + }) + .get('/api/v2', (headers: Record) => { + expect(getBaggageHeaderItems(headers)).toEqual([ + 'sentry-environment=production', + 'sentry-public_key=public', + 'sentry-release=1.0', + expect.stringMatching(/sentry-sample_rand=0\.[0-9]+/), + 'sentry-sample_rate=1', + 'sentry-sampled=true', + `sentry-trace_id=${traceId}`, + 'sentry-transaction=updated-name-2', + ]); + }) + .start(); + + await createRunner(__dirname, 'scenario-headers.ts') + .withEnv({ SERVER_URL }) + .expect({ + transaction: {}, + }) + .start() + .completed(); + closeTestServer(); + }); +}); + +test('adds current transaction name to trace envelope header when the txn name is high-quality', async () => { + expect.assertions(4); + + await createRunner(__dirname, 'scenario-events.ts') + .expectHeader({ + event: { + trace: { + environment: 'production', + public_key: 'public', + release: '1.0', + sample_rate: '1', + sampled: 'true', + trace_id: expect.stringMatching(/[a-f0-9]{32}/), + sample_rand: expect.any(String), + }, + }, + }) + .expectHeader({ + event: { + trace: { + environment: 'production', + public_key: 'public', + release: '1.0', + sample_rate: '1', + sampled: 'true', + trace_id: expect.stringMatching(/[a-f0-9]{32}/), + transaction: 'updated-name-1', + sample_rand: expect.any(String), + }, + }, + }) + .expectHeader({ + event: { + trace: { + environment: 'production', + public_key: 'public', + release: '1.0', + sample_rate: '1', + sampled: 'true', + trace_id: expect.stringMatching(/[a-f0-9]{32}/), + transaction: 'updated-name-2', + sample_rand: expect.any(String), + }, + }, + }) + .expectHeader({ + transaction: { + trace: { + environment: 'production', + public_key: 'public', + release: '1.0', + sample_rate: '1', + sampled: 'true', + trace_id: expect.stringMatching(/[a-f0-9]{32}/), + transaction: 'updated-name-2', + sample_rand: expect.any(String), + }, + }, + }) + .start() + .completed(); +}); + +function getBaggageHeaderItems(headers: Record) { + const baggage = headers['baggage'] as string; + const baggageItems = baggage + .split(',') + .map(b => b.trim()) + .sort(); + return baggageItems; +} diff --git a/dev-packages/node-core-integration-tests/suites/tracing/envelope-header/error-active-span-unsampled/scenario.ts b/dev-packages/node-core-integration-tests/suites/tracing/envelope-header/error-active-span-unsampled/scenario.ts new file mode 100644 index 000000000000..ee86f615d220 --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/tracing/envelope-header/error-active-span-unsampled/scenario.ts @@ -0,0 +1,18 @@ +import * as Sentry from '@sentry/node-core'; +import { loggingTransport } from '@sentry-internal/node-core-integration-tests'; +import { setupOtel } from '../../../../utils/setupOtel'; + +const client = Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', + tracePropagationTargets: [/\/v0/, 'v1'], + tracesSampleRate: 0, + integrations: [], + transport: loggingTransport, +}); + +setupOtel(client); + +Sentry.startSpan({ name: 'test span' }, () => { + Sentry.captureException(new Error('foo')); +}); diff --git a/dev-packages/node-core-integration-tests/suites/tracing/envelope-header/error-active-span-unsampled/test.ts b/dev-packages/node-core-integration-tests/suites/tracing/envelope-header/error-active-span-unsampled/test.ts new file mode 100644 index 000000000000..bba04c788282 --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/tracing/envelope-header/error-active-span-unsampled/test.ts @@ -0,0 +1,22 @@ +import { expect, test } from 'vitest'; +import { createRunner } from '../../../../utils/runner'; + +test('envelope header for error event during active unsampled span is correct', async () => { + await createRunner(__dirname, 'scenario.ts') + .ignore('transaction') + .expectHeader({ + event: { + trace: { + trace_id: expect.stringMatching(/[a-f0-9]{32}/), + public_key: 'public', + environment: 'production', + release: '1.0', + sample_rate: '0', + sampled: 'false', + sample_rand: expect.any(String), + }, + }, + }) + .start() + .completed(); +}); diff --git a/dev-packages/node-core-integration-tests/suites/tracing/envelope-header/error-active-span/scenario.ts b/dev-packages/node-core-integration-tests/suites/tracing/envelope-header/error-active-span/scenario.ts new file mode 100644 index 000000000000..72ef4f49a2f6 --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/tracing/envelope-header/error-active-span/scenario.ts @@ -0,0 +1,20 @@ +import * as Sentry from '@sentry/node-core'; +import { loggingTransport } from '@sentry-internal/node-core-integration-tests'; +import { setupOtel } from '../../../../utils/setupOtel'; + +const client = Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', + tracePropagationTargets: [/\/v0/, 'v1'], + tracesSampleRate: 1, + integrations: [], + transport: loggingTransport, +}); + +setupOtel(client); + +Sentry.startSpan({ name: 'test span' }, () => { + Sentry.startSpan({ name: 'test inner span' }, () => { + Sentry.captureException(new Error('foo')); + }); +}); diff --git a/dev-packages/node-core-integration-tests/suites/tracing/envelope-header/error-active-span/test.ts b/dev-packages/node-core-integration-tests/suites/tracing/envelope-header/error-active-span/test.ts new file mode 100644 index 000000000000..f11defc490c8 --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/tracing/envelope-header/error-active-span/test.ts @@ -0,0 +1,23 @@ +import { expect, test } from 'vitest'; +import { createRunner } from '../../../../utils/runner'; + +test('envelope header for error event during active span is correct', async () => { + await createRunner(__dirname, 'scenario.ts') + .ignore('transaction') + .expectHeader({ + event: { + trace: { + trace_id: expect.stringMatching(/[a-f0-9]{32}/), + public_key: 'public', + environment: 'production', + release: '1.0', + sample_rate: '1', + sampled: 'true', + transaction: 'test span', + sample_rand: expect.any(String), + }, + }, + }) + .start() + .completed(); +}); diff --git a/dev-packages/node-core-integration-tests/suites/tracing/envelope-header/error/scenario.ts b/dev-packages/node-core-integration-tests/suites/tracing/envelope-header/error/scenario.ts new file mode 100644 index 000000000000..edeee6176370 --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/tracing/envelope-header/error/scenario.ts @@ -0,0 +1,16 @@ +import * as Sentry from '@sentry/node-core'; +import { loggingTransport } from '@sentry-internal/node-core-integration-tests'; +import { setupOtel } from '../../../../utils/setupOtel'; + +const client = Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', + tracePropagationTargets: [/\/v0/, 'v1'], + tracesSampleRate: 0, + integrations: [], + transport: loggingTransport, +}); + +setupOtel(client); + +Sentry.captureException(new Error('foo')); diff --git a/dev-packages/node-core-integration-tests/suites/tracing/envelope-header/error/test.ts b/dev-packages/node-core-integration-tests/suites/tracing/envelope-header/error/test.ts new file mode 100644 index 000000000000..9d39209d456f --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/tracing/envelope-header/error/test.ts @@ -0,0 +1,18 @@ +import { expect, test } from 'vitest'; +import { createRunner } from '../../../../utils/runner'; + +test('envelope header for error events is correct', async () => { + await createRunner(__dirname, 'scenario.ts') + .expectHeader({ + event: { + trace: { + trace_id: expect.stringMatching(/[a-f0-9]{32}/), + environment: 'production', + public_key: 'public', + release: '1.0', + }, + }, + }) + .start() + .completed(); +}); diff --git a/dev-packages/node-core-integration-tests/suites/tracing/envelope-header/sampleRate-propagation/server.js b/dev-packages/node-core-integration-tests/suites/tracing/envelope-header/sampleRate-propagation/server.js new file mode 100644 index 000000000000..d3a9be811778 --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/tracing/envelope-header/sampleRate-propagation/server.js @@ -0,0 +1,38 @@ +const { loggingTransport } = require('@sentry-internal/node-core-integration-tests'); +const Sentry = require('@sentry/node-core'); +const { setupOtel } = require('../../../../utils/setupOtel.js'); + +const client = Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', + // disable attaching headers to /test/* endpoints + tracePropagationTargets: [/^(?!.*test).*$/], + tracesSampleRate: 1.0, + transport: loggingTransport, +}); + +setupOtel(client); + +// express must be required after Sentry is initialized +const express = require('express'); +const cors = require('cors'); +const bodyParser = require('body-parser'); +const { startExpressServerAndSendPortToRunner } = require('@sentry-internal/node-core-integration-tests'); + +const app = express(); + +app.use(cors()); +app.use(bodyParser.json()); +app.use(bodyParser.text()); +app.use(bodyParser.raw()); + +app.get('/test', (req, res) => { + // Create a transaction to trigger trace continuation from headers + // because node-core doesn't create spans for http requests due to + // the lack of @opentelemetry/instrumentation-http + Sentry.startSpan({ name: 'test-transaction', op: 'http.server' }, () => { + res.send({ headers: req.headers }); + }); +}); + +startExpressServerAndSendPortToRunner(app); diff --git a/dev-packages/node-core-integration-tests/suites/tracing/envelope-header/sampleRate-propagation/test.ts b/dev-packages/node-core-integration-tests/suites/tracing/envelope-header/sampleRate-propagation/test.ts new file mode 100644 index 000000000000..63db6ff4e820 --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/tracing/envelope-header/sampleRate-propagation/test.ts @@ -0,0 +1,33 @@ +import { afterAll, describe, test } from 'vitest'; +import { cleanupChildProcesses, createRunner } from '../../../../utils/runner'; + +describe('tracesSampleRate propagation', () => { + afterAll(() => { + cleanupChildProcesses(); + }); + + const traceId = '12345678123456781234567812345678'; + + test('uses sample rate from incoming baggage header in trace envelope item', async () => { + const runner = createRunner(__dirname, 'server.js') + .expectHeader({ + transaction: { + trace: { + sample_rate: '0.05', + sampled: 'true', + trace_id: traceId, + transaction: 'myTransaction', + sample_rand: '0.42', + }, + }, + }) + .start(); + runner.makeRequest('get', '/test', { + headers: { + 'sentry-trace': `${traceId}-1234567812345678-1`, + baggage: `sentry-sample_rate=0.05,sentry-trace_id=${traceId},sentry-sampled=true,sentry-transaction=myTransaction,sentry-sample_rand=0.42`, + }, + }); + await runner.completed(); + }); +}); diff --git a/dev-packages/node-core-integration-tests/suites/tracing/envelope-header/transaction-route/scenario.ts b/dev-packages/node-core-integration-tests/suites/tracing/envelope-header/transaction-route/scenario.ts new file mode 100644 index 000000000000..cd28c63fc4b7 --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/tracing/envelope-header/transaction-route/scenario.ts @@ -0,0 +1,29 @@ +import * as Sentry from '@sentry/node-core'; +import { loggingTransport } from '@sentry-internal/node-core-integration-tests'; +import { setupOtel } from '../../../../utils/setupOtel'; + +const client = Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', + tracePropagationTargets: [/\/v0/, 'v1'], + tracesSampleRate: 1, + integrations: [], + transport: loggingTransport, +}); + +setupOtel(client); + +Sentry.startSpan( + { + name: 'GET /route', + attributes: { + 'http.method': 'GET', + 'http.route': '/route', + [Sentry.SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'http.server', + [Sentry.SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route', + }, + }, + () => { + // noop + }, +); diff --git a/dev-packages/node-core-integration-tests/suites/tracing/envelope-header/transaction-route/test.ts b/dev-packages/node-core-integration-tests/suites/tracing/envelope-header/transaction-route/test.ts new file mode 100644 index 000000000000..f4bb6e2b4293 --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/tracing/envelope-header/transaction-route/test.ts @@ -0,0 +1,22 @@ +import { expect, test } from 'vitest'; +import { createRunner } from '../../../../utils/runner'; + +test('envelope header for transaction event of route correct', async () => { + await createRunner(__dirname, 'scenario.ts') + .expectHeader({ + transaction: { + trace: { + trace_id: expect.stringMatching(/[a-f0-9]{32}/), + public_key: 'public', + transaction: 'GET /route', + environment: 'production', + release: '1.0', + sample_rate: '1', + sampled: 'true', + sample_rand: expect.any(String), + }, + }, + }) + .start() + .completed(); +}); diff --git a/dev-packages/node-core-integration-tests/suites/tracing/envelope-header/transaction-url/scenario.ts b/dev-packages/node-core-integration-tests/suites/tracing/envelope-header/transaction-url/scenario.ts new file mode 100644 index 000000000000..b47f9cfc73dc --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/tracing/envelope-header/transaction-url/scenario.ts @@ -0,0 +1,29 @@ +import * as Sentry from '@sentry/node-core'; +import { loggingTransport } from '@sentry-internal/node-core-integration-tests'; +import { setupOtel } from '../../../../utils/setupOtel'; + +const client = Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', + tracePropagationTargets: [/\/v0/, 'v1'], + tracesSampleRate: 1, + integrations: [], + transport: loggingTransport, +}); + +setupOtel(client); + +Sentry.startSpan( + { + name: 'GET /route/1', + attributes: { + 'http.method': 'GET', + 'http.route': '/route', + [Sentry.SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'http.server', + [Sentry.SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'url', + }, + }, + () => { + // noop + }, +); diff --git a/dev-packages/node-core-integration-tests/suites/tracing/envelope-header/transaction-url/test.ts b/dev-packages/node-core-integration-tests/suites/tracing/envelope-header/transaction-url/test.ts new file mode 100644 index 000000000000..c4ed5ae4983f --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/tracing/envelope-header/transaction-url/test.ts @@ -0,0 +1,21 @@ +import { expect, test } from 'vitest'; +import { createRunner } from '../../../../utils/runner'; + +test('envelope header for transaction event with source=url correct', async () => { + await createRunner(__dirname, 'scenario.ts') + .expectHeader({ + transaction: { + trace: { + trace_id: expect.stringMatching(/[a-f0-9]{32}/), + public_key: 'public', + environment: 'production', + release: '1.0', + sample_rate: '1', + sampled: 'true', + sample_rand: expect.any(String), + }, + }, + }) + .start() + .completed(); +}); diff --git a/dev-packages/node-core-integration-tests/suites/tracing/envelope-header/transaction/scenario.ts b/dev-packages/node-core-integration-tests/suites/tracing/envelope-header/transaction/scenario.ts new file mode 100644 index 000000000000..0bec8720f01b --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/tracing/envelope-header/transaction/scenario.ts @@ -0,0 +1,18 @@ +import * as Sentry from '@sentry/node-core'; +import { loggingTransport } from '@sentry-internal/node-core-integration-tests'; +import { setupOtel } from '../../../../utils/setupOtel'; + +const client = Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', + tracePropagationTargets: [/\/v0/, 'v1'], + tracesSampleRate: 1, + integrations: [], + transport: loggingTransport, +}); + +setupOtel(client); + +Sentry.startSpan({ name: 'test span' }, () => { + // noop +}); diff --git a/dev-packages/node-core-integration-tests/suites/tracing/envelope-header/transaction/test.ts b/dev-packages/node-core-integration-tests/suites/tracing/envelope-header/transaction/test.ts new file mode 100644 index 000000000000..104761d52c86 --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/tracing/envelope-header/transaction/test.ts @@ -0,0 +1,22 @@ +import { expect, test } from 'vitest'; +import { createRunner } from '../../../../utils/runner'; + +test('envelope header for transaction event is correct', async () => { + await createRunner(__dirname, 'scenario.ts') + .expectHeader({ + transaction: { + trace: { + trace_id: expect.stringMatching(/[a-f0-9]{32}/), + public_key: 'public', + environment: 'production', + release: '1.0', + sample_rate: '1', + sampled: 'true', + transaction: 'test span', + sample_rand: expect.any(String), + }, + }, + }) + .start() + .completed(); +}); diff --git a/dev-packages/node-core-integration-tests/suites/tracing/linking/scenario-addLink-nested.ts b/dev-packages/node-core-integration-tests/suites/tracing/linking/scenario-addLink-nested.ts new file mode 100644 index 000000000000..2923e2d3414f --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/tracing/linking/scenario-addLink-nested.ts @@ -0,0 +1,36 @@ +import * as Sentry from '@sentry/node-core'; +import { loggingTransport } from '@sentry-internal/node-core-integration-tests'; +import { setupOtel } from '../../../utils/setupOtel'; + +const client = Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', + tracesSampleRate: 1.0, + integrations: [], + transport: loggingTransport, +}); + +setupOtel(client); + +// eslint-disable-next-line @typescript-eslint/no-floating-promises +Sentry.startSpan({ name: 'parent1' }, async parentSpan1 => { + // eslint-disable-next-line @typescript-eslint/no-floating-promises + Sentry.startSpan({ name: 'child1.1' }, async childSpan1 => { + childSpan1.addLink({ + context: parentSpan1.spanContext(), + attributes: { 'sentry.link.type': 'previous_trace' }, + }); + + childSpan1.end(); + }); + + // eslint-disable-next-line @typescript-eslint/no-floating-promises + Sentry.startSpan({ name: 'child1.2' }, async childSpan2 => { + childSpan2.addLink({ + context: parentSpan1.spanContext(), + attributes: { 'sentry.link.type': 'previous_trace' }, + }); + + childSpan2.end(); + }); +}); diff --git a/dev-packages/node-core-integration-tests/suites/tracing/linking/scenario-addLink.ts b/dev-packages/node-core-integration-tests/suites/tracing/linking/scenario-addLink.ts new file mode 100644 index 000000000000..d12fb52ad748 --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/tracing/linking/scenario-addLink.ts @@ -0,0 +1,23 @@ +import * as Sentry from '@sentry/node-core'; +import { loggingTransport } from '@sentry-internal/node-core-integration-tests'; +import { setupOtel } from '../../../utils/setupOtel'; + +const client = Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', + tracesSampleRate: 1.0, + integrations: [], + transport: loggingTransport, +}); + +setupOtel(client); + +const span1 = Sentry.startInactiveSpan({ name: 'span1' }); +span1.end(); + +Sentry.startSpan({ name: 'rootSpan' }, rootSpan => { + rootSpan.addLink({ + context: span1.spanContext(), + attributes: { 'sentry.link.type': 'previous_trace' }, + }); +}); diff --git a/dev-packages/node-core-integration-tests/suites/tracing/linking/scenario-addLinks-nested.ts b/dev-packages/node-core-integration-tests/suites/tracing/linking/scenario-addLinks-nested.ts new file mode 100644 index 000000000000..5a1d45845f85 --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/tracing/linking/scenario-addLinks-nested.ts @@ -0,0 +1,34 @@ +import * as Sentry from '@sentry/node-core'; +import { loggingTransport } from '@sentry-internal/node-core-integration-tests'; +import { setupOtel } from '../../../utils/setupOtel'; + +const client = Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', + tracesSampleRate: 1.0, + integrations: [], + transport: loggingTransport, +}); + +setupOtel(client); + +// eslint-disable-next-line @typescript-eslint/no-floating-promises +Sentry.startSpan({ name: 'parent1' }, async parentSpan1 => { + // eslint-disable-next-line @typescript-eslint/no-floating-promises + Sentry.startSpan({ name: 'child1.1' }, async childSpan1 => { + // eslint-disable-next-line @typescript-eslint/no-floating-promises + Sentry.startSpan({ name: 'child2.1' }, async childSpan2 => { + childSpan2.addLinks([ + { context: parentSpan1.spanContext() }, + { + context: childSpan1.spanContext(), + attributes: { 'sentry.link.type': 'previous_trace' }, + }, + ]); + + childSpan2.end(); + }); + + childSpan1.end(); + }); +}); diff --git a/dev-packages/node-core-integration-tests/suites/tracing/linking/scenario-addLinks.ts b/dev-packages/node-core-integration-tests/suites/tracing/linking/scenario-addLinks.ts new file mode 100644 index 000000000000..c2c1e765b3d2 --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/tracing/linking/scenario-addLinks.ts @@ -0,0 +1,29 @@ +import * as Sentry from '@sentry/node-core'; +import { loggingTransport } from '@sentry-internal/node-core-integration-tests'; +import { setupOtel } from '../../../utils/setupOtel'; + +const client = Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', + tracesSampleRate: 1.0, + integrations: [], + transport: loggingTransport, +}); + +setupOtel(client); + +const span1 = Sentry.startInactiveSpan({ name: 'span1' }); +span1.end(); + +const span2 = Sentry.startInactiveSpan({ name: 'span2' }); +span2.end(); + +Sentry.startSpan({ name: 'rootSpan' }, rootSpan => { + rootSpan.addLinks([ + { context: span1.spanContext() }, + { + context: span2.spanContext(), + attributes: { 'sentry.link.type': 'previous_trace' }, + }, + ]); +}); diff --git a/dev-packages/node-core-integration-tests/suites/tracing/linking/scenario-span-options.ts b/dev-packages/node-core-integration-tests/suites/tracing/linking/scenario-span-options.ts new file mode 100644 index 000000000000..0488758a03df --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/tracing/linking/scenario-span-options.ts @@ -0,0 +1,30 @@ +import * as Sentry from '@sentry/node-core'; +import { loggingTransport } from '@sentry-internal/node-core-integration-tests'; +import { setupOtel } from '../../../utils/setupOtel'; + +const client = Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', + tracesSampleRate: 1.0, + integrations: [], + transport: loggingTransport, +}); + +setupOtel(client); + +const parentSpan1 = Sentry.startInactiveSpan({ name: 'parent1' }); +parentSpan1.end(); + +// eslint-disable-next-line @typescript-eslint/no-floating-promises +Sentry.startSpan( + { + name: 'parent2', + links: [{ context: parentSpan1.spanContext(), attributes: { 'sentry.link.type': 'previous_trace' } }], + }, + async () => { + // eslint-disable-next-line @typescript-eslint/no-floating-promises + Sentry.startSpan({ name: 'child2.1' }, async childSpan1 => { + childSpan1.end(); + }); + }, +); diff --git a/dev-packages/node-core-integration-tests/suites/tracing/linking/test.ts b/dev-packages/node-core-integration-tests/suites/tracing/linking/test.ts new file mode 100644 index 000000000000..a0874274d2bd --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/tracing/linking/test.ts @@ -0,0 +1,193 @@ +import { describe, expect, test } from 'vitest'; +import { createRunner } from '../../../utils/runner'; + +describe('span links', () => { + test('should link spans by adding "links" to span options', async () => { + let span1_traceId: string, span1_spanId: string; + + await createRunner(__dirname, 'scenario-span-options.ts') + .expect({ + transaction: event => { + expect(event.transaction).toBe('parent1'); + + const traceContext = event.contexts?.trace; + span1_traceId = traceContext?.trace_id as string; + span1_spanId = traceContext?.span_id as string; + }, + }) + .expect({ + transaction: event => { + expect(event.transaction).toBe('parent2'); + + const traceContext = event.contexts?.trace; + expect(traceContext).toBeDefined(); + expect(traceContext?.links).toEqual([ + expect.objectContaining({ + trace_id: expect.stringMatching(span1_traceId), + span_id: expect.stringMatching(span1_spanId), + }), + ]); + }, + }) + .start() + .completed(); + }); + + test('should link spans with addLink() in trace context', async () => { + let span1_traceId: string, span1_spanId: string; + + await createRunner(__dirname, 'scenario-addLink.ts') + .expect({ + transaction: event => { + expect(event.transaction).toBe('span1'); + + span1_traceId = event.contexts?.trace?.trace_id as string; + span1_spanId = event.contexts?.trace?.span_id as string; + + expect(event.spans).toEqual([]); + }, + }) + .expect({ + transaction: event => { + expect(event.transaction).toBe('rootSpan'); + + expect(event.contexts?.trace?.links).toEqual([ + expect.objectContaining({ + trace_id: expect.stringMatching(span1_traceId), + span_id: expect.stringMatching(span1_spanId), + attributes: expect.objectContaining({ + 'sentry.link.type': 'previous_trace', + }), + }), + ]); + }, + }) + .start() + .completed(); + }); + + test('should link spans with addLinks() in trace context', async () => { + let span1_traceId: string, span1_spanId: string, span2_traceId: string, span2_spanId: string; + + await createRunner(__dirname, 'scenario-addLinks.ts') + .expect({ + transaction: event => { + expect(event.transaction).toBe('span1'); + + span1_traceId = event.contexts?.trace?.trace_id as string; + span1_spanId = event.contexts?.trace?.span_id as string; + + expect(event.spans).toEqual([]); + }, + }) + .expect({ + transaction: event => { + expect(event.transaction).toBe('span2'); + + span2_traceId = event.contexts?.trace?.trace_id as string; + span2_spanId = event.contexts?.trace?.span_id as string; + + expect(event.spans).toEqual([]); + }, + }) + .expect({ + transaction: event => { + expect(event.transaction).toBe('rootSpan'); + + expect(event.contexts?.trace?.links).toEqual([ + expect.not.objectContaining({ attributes: expect.anything() }) && + expect.objectContaining({ + trace_id: expect.stringMatching(span1_traceId), + span_id: expect.stringMatching(span1_spanId), + }), + expect.objectContaining({ + trace_id: expect.stringMatching(span2_traceId), + span_id: expect.stringMatching(span2_spanId), + attributes: expect.objectContaining({ + 'sentry.link.type': 'previous_trace', + }), + }), + ]); + }, + }) + .start() + .completed(); + }); + + test('should link spans with addLink() in nested startSpan() calls', async () => { + await createRunner(__dirname, 'scenario-addLink-nested.ts') + .expect({ + transaction: event => { + expect(event.transaction).toBe('parent1'); + + const parent1_traceId = event.contexts?.trace?.trace_id as string; + const parent1_spanId = event.contexts?.trace?.span_id as string; + + const spans = event.spans || []; + const child1_1 = spans.find(span => span.description === 'child1.1'); + const child1_2 = spans.find(span => span.description === 'child1.2'); + + expect(child1_1).toBeDefined(); + expect(child1_1?.links).toEqual([ + expect.objectContaining({ + trace_id: expect.stringMatching(parent1_traceId), + span_id: expect.stringMatching(parent1_spanId), + attributes: expect.objectContaining({ + 'sentry.link.type': 'previous_trace', + }), + }), + ]); + + expect(child1_2).toBeDefined(); + expect(child1_2?.links).toEqual([ + expect.objectContaining({ + trace_id: expect.stringMatching(parent1_traceId), + span_id: expect.stringMatching(parent1_spanId), + attributes: expect.objectContaining({ + 'sentry.link.type': 'previous_trace', + }), + }), + ]); + }, + }) + .start() + .completed(); + }); + + test('should link spans with addLinks() in nested startSpan() calls', async () => { + await createRunner(__dirname, 'scenario-addLinks-nested.ts') + .expect({ + transaction: event => { + expect(event.transaction).toBe('parent1'); + + const parent1_traceId = event.contexts?.trace?.trace_id as string; + const parent1_spanId = event.contexts?.trace?.span_id as string; + + const spans = event.spans || []; + const child1_1 = spans.find(span => span.description === 'child1.1'); + const child2_1 = spans.find(span => span.description === 'child2.1'); + + expect(child1_1).toBeDefined(); + + expect(child2_1).toBeDefined(); + + expect(child2_1?.links).toEqual([ + expect.not.objectContaining({ attributes: expect.anything() }) && + expect.objectContaining({ + trace_id: expect.stringMatching(parent1_traceId), + span_id: expect.stringMatching(parent1_spanId), + }), + expect.objectContaining({ + trace_id: expect.stringMatching(child1_1?.trace_id || 'non-existent-id-fallback'), + span_id: expect.stringMatching(child1_1?.span_id || 'non-existent-id-fallback'), + attributes: expect.objectContaining({ + 'sentry.link.type': 'previous_trace', + }), + }), + ]); + }, + }) + .start() + .completed(); + }); +}); diff --git a/dev-packages/node-core-integration-tests/suites/tracing/maxSpans/scenario.ts b/dev-packages/node-core-integration-tests/suites/tracing/maxSpans/scenario.ts new file mode 100644 index 000000000000..0241785b0535 --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/tracing/maxSpans/scenario.ts @@ -0,0 +1,18 @@ +import * as Sentry from '@sentry/node-core'; +import { loggingTransport } from '@sentry-internal/node-core-integration-tests'; +import { setupOtel } from '../../../utils/setupOtel'; + +const client = Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', + tracesSampleRate: 1.0, + transport: loggingTransport, +}); + +setupOtel(client); + +Sentry.startSpan({ name: 'parent' }, () => { + for (let i = 0; i < 5000; i++) { + Sentry.startInactiveSpan({ name: `child ${i}` }).end(); + } +}); diff --git a/dev-packages/node-core-integration-tests/suites/tracing/maxSpans/test.ts b/dev-packages/node-core-integration-tests/suites/tracing/maxSpans/test.ts new file mode 100644 index 000000000000..31b0af762d9a --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/tracing/maxSpans/test.ts @@ -0,0 +1,20 @@ +import type { SpanJSON } from '@sentry/core'; +import { expect, test } from 'vitest'; +import { createRunner } from '../../../utils/runner'; + +test('it limits spans to 1000', async () => { + const expectedSpans: SpanJSON[] = []; + for (let i = 0; i < 1000; i++) { + expectedSpans.push(expect.objectContaining({ description: `child ${i}` })); + } + + await createRunner(__dirname, 'scenario.ts') + .expect({ + transaction: { + transaction: 'parent', + spans: expectedSpans, + }, + }) + .start() + .completed(); +}); diff --git a/dev-packages/node-core-integration-tests/suites/tracing/meta-tags-twp-errors/no-server.js b/dev-packages/node-core-integration-tests/suites/tracing/meta-tags-twp-errors/no-server.js new file mode 100644 index 000000000000..517326720e58 --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/tracing/meta-tags-twp-errors/no-server.js @@ -0,0 +1,23 @@ +const { loggingTransport } = require('@sentry-internal/node-core-integration-tests'); +const Sentry = require('@sentry/node-core'); +const { setupOtel } = require('../../../utils/setupOtel.js'); + +const client = Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', + transport: loggingTransport, + beforeSend(event) { + event.contexts = { + ...event.contexts, + traceData: { + ...Sentry.getTraceData(), + metaTags: Sentry.getTraceMetaTags(), + }, + }; + return event; + }, +}); + +setupOtel(client); + +Sentry.captureException(new Error('test error')); diff --git a/dev-packages/node-core-integration-tests/suites/tracing/meta-tags-twp-errors/server.js b/dev-packages/node-core-integration-tests/suites/tracing/meta-tags-twp-errors/server.js new file mode 100644 index 000000000000..5a030898467e --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/tracing/meta-tags-twp-errors/server.js @@ -0,0 +1,31 @@ +const { + loggingTransport, + startExpressServerAndSendPortToRunner, +} = require('@sentry-internal/node-core-integration-tests'); +const Sentry = require('@sentry/node-core'); +const { setupOtel } = require('../../../utils/setupOtel.js'); + +const client = Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + transport: loggingTransport, +}); + +setupOtel(client); + +// express must be required after Sentry is initialized +const express = require('express'); + +const app = express(); + +app.get('/test', (_req, res) => { + Sentry.withScope(scope => { + scope.setContext('traceData', { + ...Sentry.getTraceData(), + metaTags: Sentry.getTraceMetaTags(), + }); + Sentry.captureException(new Error('test error 2')); + }); + res.status(200).send(); +}); + +startExpressServerAndSendPortToRunner(app); diff --git a/dev-packages/node-core-integration-tests/suites/tracing/meta-tags-twp-errors/test.ts b/dev-packages/node-core-integration-tests/suites/tracing/meta-tags-twp-errors/test.ts new file mode 100644 index 000000000000..7c6612a0f4f7 --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/tracing/meta-tags-twp-errors/test.ts @@ -0,0 +1,67 @@ +import { afterAll, describe, expect, test } from 'vitest'; +import { cleanupChildProcesses, createRunner } from '../../../utils/runner'; + +describe('errors in TwP mode have same trace in trace context and getTraceData()', () => { + afterAll(() => { + cleanupChildProcesses(); + }); + + // In a request handler, the spanId is consistent inside of the request + test('in incoming request', async () => { + const runner = createRunner(__dirname, 'server.js') + .expect({ + event: event => { + const { contexts } = event; + const { trace_id, span_id } = contexts?.trace || {}; + expect(trace_id).toMatch(/^[a-f0-9]{32}$/); + expect(span_id).toMatch(/^[a-f0-9]{16}$/); + + const traceData = contexts?.traceData || {}; + + expect(traceData['sentry-trace']).toEqual(`${trace_id}-${span_id}`); + + expect(traceData.baggage).toContain(`sentry-trace_id=${trace_id}`); + expect(traceData.baggage).not.toContain('sentry-sampled='); + + expect(traceData.metaTags).toContain(``); + expect(traceData.metaTags).toContain(`sentry-trace_id=${trace_id}`); + expect(traceData.metaTags).not.toContain('sentry-sampled='); + }, + }) + .start(); + runner.makeRequest('get', '/test'); + await runner.completed(); + }); + + // Outside of a request handler, the spanId is random + test('outside of a request handler', async () => { + await createRunner(__dirname, 'no-server.js') + .expect({ + event: event => { + const { contexts } = event; + const { trace_id, span_id } = contexts?.trace || {}; + expect(trace_id).toMatch(/^[a-f0-9]{32}$/); + expect(span_id).toMatch(/^[a-f0-9]{16}$/); + + const traceData = contexts?.traceData || {}; + + expect(traceData['sentry-trace']).toMatch(/^[a-f0-9]{32}-[a-f0-9]{16}$/); + expect(traceData['sentry-trace']).toContain(`${trace_id}-`); + // span_id is a random span ID + expect(traceData['sentry-trace']).not.toContain(span_id); + + expect(traceData.baggage).toContain(`sentry-trace_id=${trace_id}`); + expect(traceData.baggage).not.toContain('sentry-sampled='); + + expect(traceData.metaTags).toMatch(//); + expect(traceData.metaTags).toContain(`/); + expect(html).toContain(''); + }); + + test('injects tags with new trace if no incoming headers', async () => { + const runner = createRunner(__dirname, 'server.js').start(); + + const response = await runner.makeRequest<{ response: string }>('get', '/test'); + + const html = response?.response; + + const traceId = html?.match(//)?.[1]; + expect(traceId).not.toBeUndefined(); + + expect(html).toContain(' tags if SDK is disabled", async () => { + const traceId = 'cd7ee7a6fe3ebe7ab9c3271559bc203c'; + const parentSpanId = '100ff0980e7a4ead'; + + const runner = createRunner(__dirname, 'server-sdk-disabled.js').start(); + + const response = await runner.makeRequest<{ response: string }>('get', '/test', { + headers: { + 'sentry-trace': `${traceId}-${parentSpanId}-1`, + baggage: 'sentry-environment=production', + }, + }); + + const html = response?.response; + + expect(html).not.toContain('"sentry-trace"'); + expect(html).not.toContain('"baggage"'); + }); +}); diff --git a/dev-packages/node-core-integration-tests/suites/tracing/requests/fetch-breadcrumbs/instrument.mjs b/dev-packages/node-core-integration-tests/suites/tracing/requests/fetch-breadcrumbs/instrument.mjs new file mode 100644 index 000000000000..8834d9742502 --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/tracing/requests/fetch-breadcrumbs/instrument.mjs @@ -0,0 +1,21 @@ +import * as Sentry from '@sentry/node-core'; +import { loggingTransport } from '@sentry-internal/node-integration-tests'; +import { setupOtel } from '../../../../utils/setupOtel.js'; + +const client = Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', + tracePropagationTargets: [/\/v0/, 'v1'], + integrations: [], + transport: loggingTransport, + tracesSampleRate: 0.0, + // Ensure this gets a correct hint + beforeBreadcrumb(breadcrumb, hint) { + breadcrumb.data = breadcrumb.data || {}; + const req = hint?.request; + breadcrumb.data.ADDED_PATH = req?.path; + return breadcrumb; + }, +}); + +setupOtel(client); diff --git a/dev-packages/node-core-integration-tests/suites/tracing/requests/fetch-breadcrumbs/scenario.mjs b/dev-packages/node-core-integration-tests/suites/tracing/requests/fetch-breadcrumbs/scenario.mjs new file mode 100644 index 000000000000..50d1391ee577 --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/tracing/requests/fetch-breadcrumbs/scenario.mjs @@ -0,0 +1,16 @@ +import * as Sentry from '@sentry/node-core'; + +async function run() { + Sentry.addBreadcrumb({ message: 'manual breadcrumb' }); + + await fetch(`${process.env.SERVER_URL}/api/v0`).then(res => res.text()); + await fetch(`${process.env.SERVER_URL}/api/v1`).then(res => res.text()); + await fetch(`${process.env.SERVER_URL}/api/v2`).then(res => res.text()); + await fetch(`${process.env.SERVER_URL}/api/v3`).then(res => res.text()); + + await Sentry.suppressTracing(() => fetch(`${process.env.SERVER_URL}/api/v4`).then(res => res.text())); + + Sentry.captureException(new Error('foo')); +} + +run(); diff --git a/dev-packages/node-core-integration-tests/suites/tracing/requests/fetch-breadcrumbs/test.ts b/dev-packages/node-core-integration-tests/suites/tracing/requests/fetch-breadcrumbs/test.ts new file mode 100644 index 000000000000..0d1d33bb5fc9 --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/tracing/requests/fetch-breadcrumbs/test.ts @@ -0,0 +1,83 @@ +import { describe, expect } from 'vitest'; +import { conditionalTest } from '../../../../utils'; +import { createEsmAndCjsTests } from '../../../../utils/runner'; +import { createTestServer } from '../../../../utils/server'; + +describe('outgoing fetch', () => { + createEsmAndCjsTests(__dirname, 'scenario.mjs', 'instrument.mjs', (createRunner, test) => { + conditionalTest({ min: 22 })('node >=22', () => { + test('outgoing fetch requests create breadcrumbs', async () => { + const [SERVER_URL, closeTestServer] = await createTestServer().start(); + + await createRunner() + .withEnv({ SERVER_URL }) + .expect({ + event: { + breadcrumbs: [ + { + message: 'manual breadcrumb', + timestamp: expect.any(Number), + }, + { + category: 'http', + data: { + 'http.method': 'GET', + url: `${SERVER_URL}/api/v0`, + status_code: 404, + ADDED_PATH: '/api/v0', + }, + timestamp: expect.any(Number), + type: 'http', + }, + { + category: 'http', + data: { + 'http.method': 'GET', + url: `${SERVER_URL}/api/v1`, + status_code: 404, + ADDED_PATH: '/api/v1', + }, + timestamp: expect.any(Number), + type: 'http', + }, + { + category: 'http', + data: { + 'http.method': 'GET', + url: `${SERVER_URL}/api/v2`, + status_code: 404, + ADDED_PATH: '/api/v2', + }, + timestamp: expect.any(Number), + type: 'http', + }, + { + category: 'http', + data: { + 'http.method': 'GET', + url: `${SERVER_URL}/api/v3`, + status_code: 404, + ADDED_PATH: '/api/v3', + }, + timestamp: expect.any(Number), + type: 'http', + }, + ], + exception: { + values: [ + { + type: 'Error', + value: 'foo', + }, + ], + }, + }, + }) + .start() + .completed(); + + closeTestServer(); + }); + }); + }); +}); diff --git a/dev-packages/node-core-integration-tests/suites/tracing/requests/fetch-no-tracing-no-spans/instrument.mjs b/dev-packages/node-core-integration-tests/suites/tracing/requests/fetch-no-tracing-no-spans/instrument.mjs new file mode 100644 index 000000000000..687969d7ec1b --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/tracing/requests/fetch-no-tracing-no-spans/instrument.mjs @@ -0,0 +1,13 @@ +import * as Sentry from '@sentry/node-core'; +import { loggingTransport } from '@sentry-internal/node-integration-tests'; +import { setupOtel } from '../../../../utils/setupOtel.js'; + +const client = Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', + tracePropagationTargets: [/\/v0/, 'v1'], + integrations: [Sentry.nativeNodeFetchIntegration({ spans: false })], + transport: loggingTransport, +}); + +setupOtel(client); diff --git a/dev-packages/node-core-integration-tests/suites/tracing/requests/fetch-no-tracing-no-spans/scenario.mjs b/dev-packages/node-core-integration-tests/suites/tracing/requests/fetch-no-tracing-no-spans/scenario.mjs new file mode 100644 index 000000000000..dce36bdb9262 --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/tracing/requests/fetch-no-tracing-no-spans/scenario.mjs @@ -0,0 +1,12 @@ +import * as Sentry from '@sentry/node-core'; + +async function run() { + await fetch(`${process.env.SERVER_URL}/api/v0`).then(res => res.text()); + await fetch(`${process.env.SERVER_URL}/api/v1`).then(res => res.text()); + await fetch(`${process.env.SERVER_URL}/api/v2`).then(res => res.text()); + await fetch(`${process.env.SERVER_URL}/api/v3`).then(res => res.text()); + + Sentry.captureException(new Error('foo')); +} + +run(); diff --git a/dev-packages/node-core-integration-tests/suites/tracing/requests/fetch-no-tracing-no-spans/test.ts b/dev-packages/node-core-integration-tests/suites/tracing/requests/fetch-no-tracing-no-spans/test.ts new file mode 100644 index 000000000000..f61532d9de8b --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/tracing/requests/fetch-no-tracing-no-spans/test.ts @@ -0,0 +1,50 @@ +import { describe, expect } from 'vitest'; +import { createEsmAndCjsTests } from '../../../../utils/runner'; +import { createTestServer } from '../../../../utils/server'; + +describe('outgoing fetch', () => { + createEsmAndCjsTests(__dirname, 'scenario.mjs', 'instrument.mjs', (createRunner, test) => { + test('outgoing fetch requests are correctly instrumented with tracing & spans are disabled', async () => { + expect.assertions(11); + + const [SERVER_URL, closeTestServer] = await createTestServer() + .get('/api/v0', headers => { + expect(headers['sentry-trace']).toEqual(expect.stringMatching(/^([a-f0-9]{32})-([a-f0-9]{16})$/)); + expect(headers['sentry-trace']).not.toEqual('00000000000000000000000000000000-0000000000000000'); + expect(headers['baggage']).toEqual(expect.any(String)); + }) + .get('/api/v1', headers => { + expect(headers['sentry-trace']).toEqual(expect.stringMatching(/^([a-f0-9]{32})-([a-f0-9]{16})$/)); + expect(headers['sentry-trace']).not.toEqual('00000000000000000000000000000000-0000000000000000'); + expect(headers['baggage']).toEqual(expect.any(String)); + }) + .get('/api/v2', headers => { + expect(headers['baggage']).toBeUndefined(); + expect(headers['sentry-trace']).toBeUndefined(); + }) + .get('/api/v3', headers => { + expect(headers['baggage']).toBeUndefined(); + expect(headers['sentry-trace']).toBeUndefined(); + }) + .start(); + + await createRunner() + .withEnv({ SERVER_URL }) + .expect({ + event: { + exception: { + values: [ + { + type: 'Error', + value: 'foo', + }, + ], + }, + }, + }) + .start() + .completed(); + closeTestServer; + }); + }); +}); diff --git a/dev-packages/node-core-integration-tests/suites/tracing/requests/fetch-no-tracing/instrument.mjs b/dev-packages/node-core-integration-tests/suites/tracing/requests/fetch-no-tracing/instrument.mjs new file mode 100644 index 000000000000..b2c76f80e13a --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/tracing/requests/fetch-no-tracing/instrument.mjs @@ -0,0 +1,13 @@ +import * as Sentry from '@sentry/node-core'; +import { loggingTransport } from '@sentry-internal/node-integration-tests'; +import { setupOtel } from '../../../../utils/setupOtel.js'; + +const client = Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', + tracePropagationTargets: [/\/v0/, 'v1'], + integrations: [], + transport: loggingTransport, +}); + +setupOtel(client); diff --git a/dev-packages/node-core-integration-tests/suites/tracing/requests/fetch-no-tracing/scenario.mjs b/dev-packages/node-core-integration-tests/suites/tracing/requests/fetch-no-tracing/scenario.mjs new file mode 100644 index 000000000000..dce36bdb9262 --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/tracing/requests/fetch-no-tracing/scenario.mjs @@ -0,0 +1,12 @@ +import * as Sentry from '@sentry/node-core'; + +async function run() { + await fetch(`${process.env.SERVER_URL}/api/v0`).then(res => res.text()); + await fetch(`${process.env.SERVER_URL}/api/v1`).then(res => res.text()); + await fetch(`${process.env.SERVER_URL}/api/v2`).then(res => res.text()); + await fetch(`${process.env.SERVER_URL}/api/v3`).then(res => res.text()); + + Sentry.captureException(new Error('foo')); +} + +run(); diff --git a/dev-packages/node-core-integration-tests/suites/tracing/requests/fetch-no-tracing/test.ts b/dev-packages/node-core-integration-tests/suites/tracing/requests/fetch-no-tracing/test.ts new file mode 100644 index 000000000000..b4594c4d9c41 --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/tracing/requests/fetch-no-tracing/test.ts @@ -0,0 +1,50 @@ +import { describe, expect } from 'vitest'; +import { createEsmAndCjsTests } from '../../../../utils/runner'; +import { createTestServer } from '../../../../utils/server'; + +describe('outgoing fetch', () => { + createEsmAndCjsTests(__dirname, 'scenario.mjs', 'instrument.mjs', (createRunner, test) => { + test('outgoing fetch requests are correctly instrumented with tracing disabled', async () => { + expect.assertions(11); + + const [SERVER_URL, closeTestServer] = await createTestServer() + .get('/api/v0', headers => { + expect(headers['sentry-trace']).toEqual(expect.stringMatching(/^([a-f0-9]{32})-([a-f0-9]{16})$/)); + expect(headers['sentry-trace']).not.toEqual('00000000000000000000000000000000-0000000000000000'); + expect(headers['baggage']).toEqual(expect.any(String)); + }) + .get('/api/v1', headers => { + expect(headers['sentry-trace']).toEqual(expect.stringMatching(/^([a-f0-9]{32})-([a-f0-9]{16})$/)); + expect(headers['sentry-trace']).not.toEqual('00000000000000000000000000000000-0000000000000000'); + expect(headers['baggage']).toEqual(expect.any(String)); + }) + .get('/api/v2', headers => { + expect(headers['baggage']).toBeUndefined(); + expect(headers['sentry-trace']).toBeUndefined(); + }) + .get('/api/v3', headers => { + expect(headers['baggage']).toBeUndefined(); + expect(headers['sentry-trace']).toBeUndefined(); + }) + .start(); + + await createRunner() + .withEnv({ SERVER_URL }) + .expect({ + event: { + exception: { + values: [ + { + type: 'Error', + value: 'foo', + }, + ], + }, + }, + }) + .start() + .completed(); + closeTestServer(); + }); + }); +}); diff --git a/dev-packages/node-core-integration-tests/suites/tracing/requests/fetch-sampled-no-active-span/instrument.mjs b/dev-packages/node-core-integration-tests/suites/tracing/requests/fetch-sampled-no-active-span/instrument.mjs new file mode 100644 index 000000000000..fea0bfd36c11 --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/tracing/requests/fetch-sampled-no-active-span/instrument.mjs @@ -0,0 +1,14 @@ +import * as Sentry from '@sentry/node-core'; +import { loggingTransport } from '@sentry-internal/node-integration-tests'; +import { setupOtel } from '../../../../utils/setupOtel.js'; + +const client = Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', + tracePropagationTargets: [/\/v0/, 'v1'], + tracesSampleRate: 1.0, + integrations: [], + transport: loggingTransport, +}); + +setupOtel(client); diff --git a/dev-packages/node-core-integration-tests/suites/tracing/requests/fetch-sampled-no-active-span/scenario.mjs b/dev-packages/node-core-integration-tests/suites/tracing/requests/fetch-sampled-no-active-span/scenario.mjs new file mode 100644 index 000000000000..dce36bdb9262 --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/tracing/requests/fetch-sampled-no-active-span/scenario.mjs @@ -0,0 +1,12 @@ +import * as Sentry from '@sentry/node-core'; + +async function run() { + await fetch(`${process.env.SERVER_URL}/api/v0`).then(res => res.text()); + await fetch(`${process.env.SERVER_URL}/api/v1`).then(res => res.text()); + await fetch(`${process.env.SERVER_URL}/api/v2`).then(res => res.text()); + await fetch(`${process.env.SERVER_URL}/api/v3`).then(res => res.text()); + + Sentry.captureException(new Error('foo')); +} + +run(); diff --git a/dev-packages/node-core-integration-tests/suites/tracing/requests/fetch-sampled-no-active-span/test.ts b/dev-packages/node-core-integration-tests/suites/tracing/requests/fetch-sampled-no-active-span/test.ts new file mode 100644 index 000000000000..32f24517b3f6 --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/tracing/requests/fetch-sampled-no-active-span/test.ts @@ -0,0 +1,50 @@ +import { describe, expect } from 'vitest'; +import { createEsmAndCjsTests } from '../../../../utils/runner'; +import { createTestServer } from '../../../../utils/server'; + +describe('outgoing fetch', () => { + createEsmAndCjsTests(__dirname, 'scenario.mjs', 'instrument.mjs', (createRunner, test) => { + test('outgoing sampled fetch requests without active span are correctly instrumented', async () => { + expect.assertions(11); + + const [SERVER_URL, closeTestServer] = await createTestServer() + .get('/api/v0', headers => { + expect(headers['baggage']).toEqual(expect.any(String)); + expect(headers['sentry-trace']).toEqual(expect.stringMatching(/^([a-f0-9]{32})-([a-f0-9]{16})$/)); + expect(headers['sentry-trace']).not.toEqual('00000000000000000000000000000000-0000000000000000'); + }) + .get('/api/v1', headers => { + expect(headers['baggage']).toEqual(expect.any(String)); + expect(headers['sentry-trace']).toEqual(expect.stringMatching(/^([a-f0-9]{32})-([a-f0-9]{16})$/)); + expect(headers['sentry-trace']).not.toEqual('00000000000000000000000000000000-0000000000000000'); + }) + .get('/api/v2', headers => { + expect(headers['baggage']).toBeUndefined(); + expect(headers['sentry-trace']).toBeUndefined(); + }) + .get('/api/v3', headers => { + expect(headers['baggage']).toBeUndefined(); + expect(headers['sentry-trace']).toBeUndefined(); + }) + .start(); + + await createRunner() + .withEnv({ SERVER_URL }) + .expect({ + event: { + exception: { + values: [ + { + type: 'Error', + value: 'foo', + }, + ], + }, + }, + }) + .start() + .completed(); + closeTestServer(); + }); + }); +}); diff --git a/dev-packages/node-core-integration-tests/suites/tracing/requests/fetch-unsampled/instrument.mjs b/dev-packages/node-core-integration-tests/suites/tracing/requests/fetch-unsampled/instrument.mjs new file mode 100644 index 000000000000..0c77fb8702b7 --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/tracing/requests/fetch-unsampled/instrument.mjs @@ -0,0 +1,14 @@ +import * as Sentry from '@sentry/node-core'; +import { loggingTransport } from '@sentry-internal/node-integration-tests'; +import { setupOtel } from '../../../../utils/setupOtel.js'; + +const client = Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', + tracePropagationTargets: [/\/v0/, 'v1'], + tracesSampleRate: 0, + integrations: [], + transport: loggingTransport, +}); + +setupOtel(client); diff --git a/dev-packages/node-core-integration-tests/suites/tracing/requests/fetch-unsampled/scenario.mjs b/dev-packages/node-core-integration-tests/suites/tracing/requests/fetch-unsampled/scenario.mjs new file mode 100644 index 000000000000..38735e01aaa8 --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/tracing/requests/fetch-unsampled/scenario.mjs @@ -0,0 +1,15 @@ +import * as Sentry from '@sentry/node-core'; + +async function run() { + // Wrap in span that is not sampled + await Sentry.startSpan({ name: 'outer' }, async () => { + await fetch(`${process.env.SERVER_URL}/api/v0`).then(res => res.text()); + await fetch(`${process.env.SERVER_URL}/api/v1`).then(res => res.text()); + await fetch(`${process.env.SERVER_URL}/api/v2`).then(res => res.text()); + await fetch(`${process.env.SERVER_URL}/api/v3`).then(res => res.text()); + }); + + Sentry.captureException(new Error('foo')); +} + +run(); diff --git a/dev-packages/node-core-integration-tests/suites/tracing/requests/fetch-unsampled/test.ts b/dev-packages/node-core-integration-tests/suites/tracing/requests/fetch-unsampled/test.ts new file mode 100644 index 000000000000..097236ba4e7f --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/tracing/requests/fetch-unsampled/test.ts @@ -0,0 +1,50 @@ +import { describe, expect } from 'vitest'; +import { createEsmAndCjsTests } from '../../../../utils/runner'; +import { createTestServer } from '../../../../utils/server'; + +describe('outgoing fetch', () => { + createEsmAndCjsTests(__dirname, 'scenario.mjs', 'instrument.mjs', (createRunner, test) => { + test('outgoing fetch requests are correctly instrumented when not sampled', async () => { + expect.assertions(11); + + const [SERVER_URL, closeTestServer] = await createTestServer() + .get('/api/v0', headers => { + expect(headers['baggage']).toEqual(expect.any(String)); + expect(headers['sentry-trace']).toEqual(expect.stringMatching(/^([a-f0-9]{32})-([a-f0-9]{16})-0$/)); + expect(headers['sentry-trace']).not.toEqual('00000000000000000000000000000000-0000000000000000-0'); + }) + .get('/api/v1', headers => { + expect(headers['baggage']).toEqual(expect.any(String)); + expect(headers['sentry-trace']).toEqual(expect.stringMatching(/^([a-f0-9]{32})-([a-f0-9]{16})-0$/)); + expect(headers['sentry-trace']).not.toEqual('00000000000000000000000000000000-0000000000000000-0'); + }) + .get('/api/v2', headers => { + expect(headers['baggage']).toBeUndefined(); + expect(headers['sentry-trace']).toBeUndefined(); + }) + .get('/api/v3', headers => { + expect(headers['baggage']).toBeUndefined(); + expect(headers['sentry-trace']).toBeUndefined(); + }) + .start(); + + await createRunner() + .withEnv({ SERVER_URL }) + .expect({ + event: { + exception: { + values: [ + { + type: 'Error', + value: 'foo', + }, + ], + }, + }, + }) + .start() + .completed(); + closeTestServer(); + }); + }); +}); diff --git a/dev-packages/node-core-integration-tests/suites/tracing/requests/http-breadcrumbs/instrument.mjs b/dev-packages/node-core-integration-tests/suites/tracing/requests/http-breadcrumbs/instrument.mjs new file mode 100644 index 000000000000..1465fc45ca46 --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/tracing/requests/http-breadcrumbs/instrument.mjs @@ -0,0 +1,20 @@ +import * as Sentry from '@sentry/node-core'; +import { loggingTransport } from '@sentry-internal/node-integration-tests'; +import { setupOtel } from '../../../../utils/setupOtel.js'; + +const client = Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', + tracePropagationTargets: [/\/v0/, 'v1'], + integrations: [], + transport: loggingTransport, + // Ensure this gets a correct hint + beforeBreadcrumb(breadcrumb, hint) { + breadcrumb.data = breadcrumb.data || {}; + const req = hint?.request; + breadcrumb.data.ADDED_PATH = req?.path; + return breadcrumb; + }, +}); + +setupOtel(client); diff --git a/dev-packages/node-core-integration-tests/suites/tracing/requests/http-breadcrumbs/scenario.mjs b/dev-packages/node-core-integration-tests/suites/tracing/requests/http-breadcrumbs/scenario.mjs new file mode 100644 index 000000000000..746e6487281a --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/tracing/requests/http-breadcrumbs/scenario.mjs @@ -0,0 +1,45 @@ +import * as Sentry from '@sentry/node-core'; +import * as http from 'http'; + +async function run() { + Sentry.addBreadcrumb({ message: 'manual breadcrumb' }); + + await makeHttpRequest(`${process.env.SERVER_URL}/api/v0`); + await makeHttpGet(`${process.env.SERVER_URL}/api/v1`); + await makeHttpRequest(`${process.env.SERVER_URL}/api/v2`); + await makeHttpRequest(`${process.env.SERVER_URL}/api/v3`); + + await Sentry.suppressTracing(() => makeHttpRequest(`${process.env.SERVER_URL}/api/v4`)); + + Sentry.captureException(new Error('foo')); +} + +run(); + +function makeHttpRequest(url) { + return new Promise(resolve => { + http + .request(url, httpRes => { + httpRes.on('data', () => { + // we don't care about data + }); + httpRes.on('end', () => { + resolve(); + }); + }) + .end(); + }); +} + +function makeHttpGet(url) { + return new Promise(resolve => { + http.get(url, httpRes => { + httpRes.on('data', () => { + // we don't care about data + }); + httpRes.on('end', () => { + resolve(); + }); + }); + }); +} diff --git a/dev-packages/node-core-integration-tests/suites/tracing/requests/http-breadcrumbs/test.ts b/dev-packages/node-core-integration-tests/suites/tracing/requests/http-breadcrumbs/test.ts new file mode 100644 index 000000000000..318d4628453b --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/tracing/requests/http-breadcrumbs/test.ts @@ -0,0 +1,79 @@ +import { describe, expect } from 'vitest'; +import { createEsmAndCjsTests } from '../../../../utils/runner'; +import { createTestServer } from '../../../../utils/server'; + +describe('outgoing http', () => { + createEsmAndCjsTests(__dirname, 'scenario.mjs', 'instrument.mjs', (createRunner, test) => { + test('outgoing http requests create breadcrumbs', async () => { + const [SERVER_URL, closeTestServer] = await createTestServer().start(); + + await createRunner() + .withEnv({ SERVER_URL }) + .expect({ + event: { + breadcrumbs: [ + { + message: 'manual breadcrumb', + timestamp: expect.any(Number), + }, + { + category: 'http', + data: { + 'http.method': 'GET', + url: `${SERVER_URL}/api/v0`, + status_code: 404, + ADDED_PATH: '/api/v0', + }, + timestamp: expect.any(Number), + type: 'http', + }, + { + category: 'http', + data: { + 'http.method': 'GET', + url: `${SERVER_URL}/api/v1`, + status_code: 404, + ADDED_PATH: '/api/v1', + }, + timestamp: expect.any(Number), + type: 'http', + }, + { + category: 'http', + data: { + 'http.method': 'GET', + url: `${SERVER_URL}/api/v2`, + status_code: 404, + ADDED_PATH: '/api/v2', + }, + timestamp: expect.any(Number), + type: 'http', + }, + { + category: 'http', + data: { + 'http.method': 'GET', + url: `${SERVER_URL}/api/v3`, + status_code: 404, + ADDED_PATH: '/api/v3', + }, + timestamp: expect.any(Number), + type: 'http', + }, + ], + exception: { + values: [ + { + type: 'Error', + value: 'foo', + }, + ], + }, + }, + }) + .start() + .completed(); + closeTestServer(); + }); + }); +}); diff --git a/dev-packages/node-core-integration-tests/suites/tracing/requests/http-no-tracing-no-spans/instrument.mjs b/dev-packages/node-core-integration-tests/suites/tracing/requests/http-no-tracing-no-spans/instrument.mjs new file mode 100644 index 000000000000..61706a36eca6 --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/tracing/requests/http-no-tracing-no-spans/instrument.mjs @@ -0,0 +1,20 @@ +import * as Sentry from '@sentry/node-core'; +import { loggingTransport } from '@sentry-internal/node-integration-tests'; +import { setupOtel } from '../../../../utils/setupOtel.js'; + +const client = Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', + tracePropagationTargets: [/\/v0/, 'v1'], + integrations: [Sentry.httpIntegration({ spans: false })], + transport: loggingTransport, + // Ensure this gets a correct hint + beforeBreadcrumb(breadcrumb, hint) { + breadcrumb.data = breadcrumb.data || {}; + const req = hint?.request; + breadcrumb.data.ADDED_PATH = req?.path; + return breadcrumb; + }, +}); + +setupOtel(client); diff --git a/dev-packages/node-core-integration-tests/suites/tracing/requests/http-no-tracing-no-spans/scenario.mjs b/dev-packages/node-core-integration-tests/suites/tracing/requests/http-no-tracing-no-spans/scenario.mjs new file mode 100644 index 000000000000..861d6c29bd2f --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/tracing/requests/http-no-tracing-no-spans/scenario.mjs @@ -0,0 +1,43 @@ +import * as Sentry from '@sentry/node-core'; +import * as http from 'http'; + +async function run() { + Sentry.addBreadcrumb({ message: 'manual breadcrumb' }); + + await makeHttpRequest(`${process.env.SERVER_URL}/api/v0`); + await makeHttpGet(`${process.env.SERVER_URL}/api/v1`); + await makeHttpRequest(`${process.env.SERVER_URL}/api/v2`); + await makeHttpRequest(`${process.env.SERVER_URL}/api/v3`); + + Sentry.captureException(new Error('foo')); +} + +run(); + +function makeHttpRequest(url) { + return new Promise(resolve => { + http + .request(url, httpRes => { + httpRes.on('data', () => { + // we don't care about data + }); + httpRes.on('end', () => { + resolve(); + }); + }) + .end(); + }); +} + +function makeHttpGet(url) { + return new Promise(resolve => { + http.get(url, httpRes => { + httpRes.on('data', () => { + // we don't care about data + }); + httpRes.on('end', () => { + resolve(); + }); + }); + }); +} diff --git a/dev-packages/node-core-integration-tests/suites/tracing/requests/http-no-tracing-no-spans/test.ts b/dev-packages/node-core-integration-tests/suites/tracing/requests/http-no-tracing-no-spans/test.ts new file mode 100644 index 000000000000..fe9cba032344 --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/tracing/requests/http-no-tracing-no-spans/test.ts @@ -0,0 +1,202 @@ +import { describe, expect } from 'vitest'; +import { conditionalTest } from '../../../../utils'; +import { createEsmAndCjsTests } from '../../../../utils/runner'; +import { createTestServer } from '../../../../utils/server'; + +describe('outgoing http requests with tracing & spans disabled', () => { + createEsmAndCjsTests(__dirname, 'scenario.mjs', 'instrument.mjs', (createRunner, test) => { + conditionalTest({ min: 22 })('node >=22', () => { + test('outgoing http requests are correctly instrumented with tracing & spans disabled', async () => { + expect.assertions(11); + + const [SERVER_URL, closeTestServer] = await createTestServer() + .get('/api/v0', headers => { + expect(headers['sentry-trace']).toEqual(expect.stringMatching(/^([a-f0-9]{32})-([a-f0-9]{16})$/)); + expect(headers['sentry-trace']).not.toEqual('00000000000000000000000000000000-0000000000000000'); + expect(headers['baggage']).toEqual(expect.any(String)); + }) + .get('/api/v1', headers => { + expect(headers['sentry-trace']).toEqual(expect.stringMatching(/^([a-f0-9]{32})-([a-f0-9]{16})$/)); + expect(headers['sentry-trace']).not.toEqual('00000000000000000000000000000000-0000000000000000'); + expect(headers['baggage']).toEqual(expect.any(String)); + }) + .get('/api/v2', headers => { + expect(headers['baggage']).toBeUndefined(); + expect(headers['sentry-trace']).toBeUndefined(); + }) + .get('/api/v3', headers => { + expect(headers['baggage']).toBeUndefined(); + expect(headers['sentry-trace']).toBeUndefined(); + }) + .start(); + + await createRunner() + .withEnv({ SERVER_URL }) + .expect({ + event: { + exception: { + values: [ + { + type: 'Error', + value: 'foo', + }, + ], + }, + breadcrumbs: [ + { + message: 'manual breadcrumb', + timestamp: expect.any(Number), + }, + { + category: 'http', + data: { + 'http.method': 'GET', + url: `${SERVER_URL}/api/v0`, + status_code: 200, + ADDED_PATH: '/api/v0', + }, + timestamp: expect.any(Number), + type: 'http', + }, + { + category: 'http', + data: { + 'http.method': 'GET', + url: `${SERVER_URL}/api/v1`, + status_code: 200, + ADDED_PATH: '/api/v1', + }, + timestamp: expect.any(Number), + type: 'http', + }, + { + category: 'http', + data: { + 'http.method': 'GET', + url: `${SERVER_URL}/api/v2`, + status_code: 200, + ADDED_PATH: '/api/v2', + }, + timestamp: expect.any(Number), + type: 'http', + }, + { + category: 'http', + data: { + 'http.method': 'GET', + url: `${SERVER_URL}/api/v3`, + status_code: 200, + ADDED_PATH: '/api/v3', + }, + timestamp: expect.any(Number), + type: 'http', + }, + ], + }, + }) + .start() + .completed(); + + closeTestServer(); + }); + }); + + // On older node versions, outgoing requests do not get trace-headers injected, sadly + // This is because the necessary diagnostics channel hook is not available yet + conditionalTest({ max: 21 })('node <22', () => { + test('outgoing http requests generate breadcrumbs correctly with tracing & spans disabled', async () => { + expect.assertions(9); + + const [SERVER_URL, closeTestServer] = await createTestServer() + .get('/api/v0', headers => { + // This is not instrumented, sadly + expect(headers['baggage']).toBeUndefined(); + expect(headers['sentry-trace']).toBeUndefined(); + }) + .get('/api/v1', headers => { + // This is not instrumented, sadly + expect(headers['baggage']).toBeUndefined(); + expect(headers['sentry-trace']).toBeUndefined(); + }) + .get('/api/v2', headers => { + expect(headers['baggage']).toBeUndefined(); + expect(headers['sentry-trace']).toBeUndefined(); + }) + .get('/api/v3', headers => { + expect(headers['baggage']).toBeUndefined(); + expect(headers['sentry-trace']).toBeUndefined(); + }) + .start(); + + await createRunner() + .withEnv({ SERVER_URL }) + .expect({ + event: { + exception: { + values: [ + { + type: 'Error', + value: 'foo', + }, + ], + }, + breadcrumbs: [ + { + message: 'manual breadcrumb', + timestamp: expect.any(Number), + }, + { + category: 'http', + data: { + 'http.method': 'GET', + url: `${SERVER_URL}/api/v0`, + status_code: 200, + ADDED_PATH: '/api/v0', + }, + timestamp: expect.any(Number), + type: 'http', + }, + { + category: 'http', + data: { + 'http.method': 'GET', + url: `${SERVER_URL}/api/v1`, + status_code: 200, + ADDED_PATH: '/api/v1', + }, + timestamp: expect.any(Number), + type: 'http', + }, + { + category: 'http', + data: { + 'http.method': 'GET', + url: `${SERVER_URL}/api/v2`, + status_code: 200, + ADDED_PATH: '/api/v2', + }, + timestamp: expect.any(Number), + type: 'http', + }, + { + category: 'http', + data: { + 'http.method': 'GET', + url: `${SERVER_URL}/api/v3`, + status_code: 200, + ADDED_PATH: '/api/v3', + }, + timestamp: expect.any(Number), + type: 'http', + }, + ], + }, + }) + .start() + .completed(); + + closeTestServer(); + }); + }); + }); +}); diff --git a/dev-packages/node-core-integration-tests/suites/tracing/requests/http-no-tracing/instrument.mjs b/dev-packages/node-core-integration-tests/suites/tracing/requests/http-no-tracing/instrument.mjs new file mode 100644 index 000000000000..1465fc45ca46 --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/tracing/requests/http-no-tracing/instrument.mjs @@ -0,0 +1,20 @@ +import * as Sentry from '@sentry/node-core'; +import { loggingTransport } from '@sentry-internal/node-integration-tests'; +import { setupOtel } from '../../../../utils/setupOtel.js'; + +const client = Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', + tracePropagationTargets: [/\/v0/, 'v1'], + integrations: [], + transport: loggingTransport, + // Ensure this gets a correct hint + beforeBreadcrumb(breadcrumb, hint) { + breadcrumb.data = breadcrumb.data || {}; + const req = hint?.request; + breadcrumb.data.ADDED_PATH = req?.path; + return breadcrumb; + }, +}); + +setupOtel(client); diff --git a/dev-packages/node-core-integration-tests/suites/tracing/requests/http-no-tracing/scenario.mjs b/dev-packages/node-core-integration-tests/suites/tracing/requests/http-no-tracing/scenario.mjs new file mode 100644 index 000000000000..861d6c29bd2f --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/tracing/requests/http-no-tracing/scenario.mjs @@ -0,0 +1,43 @@ +import * as Sentry from '@sentry/node-core'; +import * as http from 'http'; + +async function run() { + Sentry.addBreadcrumb({ message: 'manual breadcrumb' }); + + await makeHttpRequest(`${process.env.SERVER_URL}/api/v0`); + await makeHttpGet(`${process.env.SERVER_URL}/api/v1`); + await makeHttpRequest(`${process.env.SERVER_URL}/api/v2`); + await makeHttpRequest(`${process.env.SERVER_URL}/api/v3`); + + Sentry.captureException(new Error('foo')); +} + +run(); + +function makeHttpRequest(url) { + return new Promise(resolve => { + http + .request(url, httpRes => { + httpRes.on('data', () => { + // we don't care about data + }); + httpRes.on('end', () => { + resolve(); + }); + }) + .end(); + }); +} + +function makeHttpGet(url) { + return new Promise(resolve => { + http.get(url, httpRes => { + httpRes.on('data', () => { + // we don't care about data + }); + httpRes.on('end', () => { + resolve(); + }); + }); + }); +} diff --git a/dev-packages/node-core-integration-tests/suites/tracing/requests/http-no-tracing/test.ts b/dev-packages/node-core-integration-tests/suites/tracing/requests/http-no-tracing/test.ts new file mode 100644 index 000000000000..8727f1cad0de --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/tracing/requests/http-no-tracing/test.ts @@ -0,0 +1,103 @@ +import { describe, expect } from 'vitest'; +import { conditionalTest } from '../../../../utils'; +import { createEsmAndCjsTests } from '../../../../utils/runner'; +import { createTestServer } from '../../../../utils/server'; + +describe('outgoing http', () => { + createEsmAndCjsTests(__dirname, 'scenario.mjs', 'instrument.mjs', (createRunner, test) => { + conditionalTest({ min: 22 })('node >=22', () => { + test('outgoing http requests are correctly instrumented with tracing disabled', async () => { + expect.assertions(11); + + const [SERVER_URL, closeTestServer] = await createTestServer() + .get('/api/v0', headers => { + expect(headers['sentry-trace']).toEqual(expect.stringMatching(/^([a-f0-9]{32})-([a-f0-9]{16})$/)); + expect(headers['sentry-trace']).not.toEqual('00000000000000000000000000000000-0000000000000000'); + expect(headers['baggage']).toEqual(expect.any(String)); + }) + .get('/api/v1', headers => { + expect(headers['sentry-trace']).toEqual(expect.stringMatching(/^([a-f0-9]{32})-([a-f0-9]{16})$/)); + expect(headers['sentry-trace']).not.toEqual('00000000000000000000000000000000-0000000000000000'); + expect(headers['baggage']).toEqual(expect.any(String)); + }) + .get('/api/v2', headers => { + expect(headers['baggage']).toBeUndefined(); + expect(headers['sentry-trace']).toBeUndefined(); + }) + .get('/api/v3', headers => { + expect(headers['baggage']).toBeUndefined(); + expect(headers['sentry-trace']).toBeUndefined(); + }) + .start(); + + await createRunner() + .withEnv({ SERVER_URL }) + .expect({ + event: { + exception: { + values: [ + { + type: 'Error', + value: 'foo', + }, + ], + }, + breadcrumbs: [ + { + message: 'manual breadcrumb', + timestamp: expect.any(Number), + }, + { + category: 'http', + data: { + 'http.method': 'GET', + url: `${SERVER_URL}/api/v0`, + status_code: 200, + ADDED_PATH: '/api/v0', + }, + timestamp: expect.any(Number), + type: 'http', + }, + { + category: 'http', + data: { + 'http.method': 'GET', + url: `${SERVER_URL}/api/v1`, + status_code: 200, + ADDED_PATH: '/api/v1', + }, + timestamp: expect.any(Number), + type: 'http', + }, + { + category: 'http', + data: { + 'http.method': 'GET', + url: `${SERVER_URL}/api/v2`, + status_code: 200, + ADDED_PATH: '/api/v2', + }, + timestamp: expect.any(Number), + type: 'http', + }, + { + category: 'http', + data: { + 'http.method': 'GET', + url: `${SERVER_URL}/api/v3`, + status_code: 200, + ADDED_PATH: '/api/v3', + }, + timestamp: expect.any(Number), + type: 'http', + }, + ], + }, + }) + .start() + .completed(); + closeTestServer(); + }); + }); + }); +}); diff --git a/dev-packages/node-core-integration-tests/suites/tracing/requests/http-sampled-no-active-span/instrument.mjs b/dev-packages/node-core-integration-tests/suites/tracing/requests/http-sampled-no-active-span/instrument.mjs new file mode 100644 index 000000000000..33213733efef --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/tracing/requests/http-sampled-no-active-span/instrument.mjs @@ -0,0 +1,14 @@ +import * as Sentry from '@sentry/node-core'; +import { loggingTransport } from '@sentry-internal/node-integration-tests'; +import { setupOtel } from '../../../../utils/setupOtel.js'; + +const client = Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', + tracesSampleRate: 1.0, + tracePropagationTargets: [/\/v0/, 'v1'], + integrations: [], + transport: loggingTransport, +}); + +setupOtel(client); diff --git a/dev-packages/node-core-integration-tests/suites/tracing/requests/http-sampled-no-active-span/scenario.mjs b/dev-packages/node-core-integration-tests/suites/tracing/requests/http-sampled-no-active-span/scenario.mjs new file mode 100644 index 000000000000..f1603c6dcd8b --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/tracing/requests/http-sampled-no-active-span/scenario.mjs @@ -0,0 +1,28 @@ +import * as Sentry from '@sentry/node-core'; +import * as http from 'http'; + +async function run() { + await makeHttpRequest(`${process.env.SERVER_URL}/api/v0`); + await makeHttpRequest(`${process.env.SERVER_URL}/api/v1`); + await makeHttpRequest(`${process.env.SERVER_URL}/api/v2`); + await makeHttpRequest(`${process.env.SERVER_URL}/api/v3`); + + Sentry.captureException(new Error('foo')); +} + +run(); + +function makeHttpRequest(url) { + return new Promise(resolve => { + http + .request(url, httpRes => { + httpRes.on('data', () => { + // we don't care about data + }); + httpRes.on('end', () => { + resolve(); + }); + }) + .end(); + }); +} diff --git a/dev-packages/node-core-integration-tests/suites/tracing/requests/http-sampled-no-active-span/test.ts b/dev-packages/node-core-integration-tests/suites/tracing/requests/http-sampled-no-active-span/test.ts new file mode 100644 index 000000000000..d89992dd362e --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/tracing/requests/http-sampled-no-active-span/test.ts @@ -0,0 +1,53 @@ +import { describe, expect } from 'vitest'; +import { conditionalTest } from '../../../../utils'; +import { createEsmAndCjsTests } from '../../../../utils/runner'; +import { createTestServer } from '../../../../utils/server'; + +describe('outgoing http', () => { + createEsmAndCjsTests(__dirname, 'scenario.mjs', 'instrument.mjs', (createRunner, test) => { + conditionalTest({ min: 22 })('node >=22', () => { + test('outgoing sampled http requests without active span are correctly instrumented', async () => { + expect.assertions(11); + + const [SERVER_URL, closeTestServer] = await createTestServer() + .get('/api/v0', headers => { + expect(headers['baggage']).toEqual(expect.any(String)); + expect(headers['sentry-trace']).toEqual(expect.stringMatching(/^([a-f0-9]{32})-([a-f0-9]{16})$/)); + expect(headers['sentry-trace']).not.toEqual('00000000000000000000000000000000-0000000000000000'); + }) + .get('/api/v1', headers => { + expect(headers['baggage']).toEqual(expect.any(String)); + expect(headers['sentry-trace']).toEqual(expect.stringMatching(/^([a-f0-9]{32})-([a-f0-9]{16})$/)); + expect(headers['sentry-trace']).not.toEqual('00000000000000000000000000000000-0000000000000000'); + }) + .get('/api/v2', headers => { + expect(headers['baggage']).toBeUndefined(); + expect(headers['sentry-trace']).toBeUndefined(); + }) + .get('/api/v3', headers => { + expect(headers['baggage']).toBeUndefined(); + expect(headers['sentry-trace']).toBeUndefined(); + }) + .start(); + + await createRunner() + .withEnv({ SERVER_URL }) + .expect({ + event: { + exception: { + values: [ + { + type: 'Error', + value: 'foo', + }, + ], + }, + }, + }) + .start() + .completed(); + closeTestServer(); + }); + }); + }); +}); diff --git a/dev-packages/node-core-integration-tests/suites/tracing/requests/http-sampled/instrument.mjs b/dev-packages/node-core-integration-tests/suites/tracing/requests/http-sampled/instrument.mjs new file mode 100644 index 000000000000..33213733efef --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/tracing/requests/http-sampled/instrument.mjs @@ -0,0 +1,14 @@ +import * as Sentry from '@sentry/node-core'; +import { loggingTransport } from '@sentry-internal/node-integration-tests'; +import { setupOtel } from '../../../../utils/setupOtel.js'; + +const client = Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', + tracesSampleRate: 1.0, + tracePropagationTargets: [/\/v0/, 'v1'], + integrations: [], + transport: loggingTransport, +}); + +setupOtel(client); diff --git a/dev-packages/node-core-integration-tests/suites/tracing/requests/http-sampled/scenario.mjs b/dev-packages/node-core-integration-tests/suites/tracing/requests/http-sampled/scenario.mjs new file mode 100644 index 000000000000..18f508d309a2 --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/tracing/requests/http-sampled/scenario.mjs @@ -0,0 +1,24 @@ +import * as Sentry from '@sentry/node-core'; +import * as http from 'http'; + +Sentry.startSpan({ name: 'test_span' }, async () => { + await makeHttpRequest(`${process.env.SERVER_URL}/api/v0`); + await makeHttpRequest(`${process.env.SERVER_URL}/api/v1`); + await makeHttpRequest(`${process.env.SERVER_URL}/api/v2`); + await makeHttpRequest(`${process.env.SERVER_URL}/api/v3`); +}); + +function makeHttpRequest(url) { + return new Promise(resolve => { + http + .request(url, httpRes => { + httpRes.on('data', () => { + // we don't care about data + }); + httpRes.on('end', () => { + resolve(); + }); + }) + .end(); + }); +} diff --git a/dev-packages/node-core-integration-tests/suites/tracing/requests/http-sampled/test.ts b/dev-packages/node-core-integration-tests/suites/tracing/requests/http-sampled/test.ts new file mode 100644 index 000000000000..1189afc502e5 --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/tracing/requests/http-sampled/test.ts @@ -0,0 +1,46 @@ +import { describe, expect } from 'vitest'; +import { conditionalTest } from '../../../../utils'; +import { createEsmAndCjsTests } from '../../../../utils/runner'; +import { createTestServer } from '../../../../utils/server'; + +describe('outgoing http', () => { + createEsmAndCjsTests(__dirname, 'scenario.mjs', 'instrument.mjs', (createRunner, test) => { + conditionalTest({ min: 22 })('node >=22', () => { + test('outgoing sampled http requests are correctly instrumented', async () => { + expect.assertions(11); + + const [SERVER_URL, closeTestServer] = await createTestServer() + .get('/api/v0', headers => { + expect(headers['baggage']).toEqual(expect.any(String)); + expect(headers['sentry-trace']).toEqual(expect.stringMatching(/^([a-f0-9]{32})-([a-f0-9]{16})-1$/)); + expect(headers['sentry-trace']).not.toEqual('00000000000000000000000000000000-0000000000000000-1'); + }) + .get('/api/v1', headers => { + expect(headers['baggage']).toEqual(expect.any(String)); + expect(headers['sentry-trace']).toEqual(expect.stringMatching(/^([a-f0-9]{32})-([a-f0-9]{16})-1$/)); + expect(headers['sentry-trace']).not.toEqual('00000000000000000000000000000000-0000000000000000-1'); + }) + .get('/api/v2', headers => { + expect(headers['baggage']).toBeUndefined(); + expect(headers['sentry-trace']).toBeUndefined(); + }) + .get('/api/v3', headers => { + expect(headers['baggage']).toBeUndefined(); + expect(headers['sentry-trace']).toBeUndefined(); + }) + .start(); + + await createRunner() + .withEnv({ SERVER_URL }) + .expect({ + transaction: { + // we're not too concerned with the actual transaction here since this is tested elsewhere + }, + }) + .start() + .completed(); + closeTestServer(); + }); + }); + }); +}); diff --git a/dev-packages/node-core-integration-tests/suites/tracing/requests/http-unsampled/instrument.mjs b/dev-packages/node-core-integration-tests/suites/tracing/requests/http-unsampled/instrument.mjs new file mode 100644 index 000000000000..0c77fb8702b7 --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/tracing/requests/http-unsampled/instrument.mjs @@ -0,0 +1,14 @@ +import * as Sentry from '@sentry/node-core'; +import { loggingTransport } from '@sentry-internal/node-integration-tests'; +import { setupOtel } from '../../../../utils/setupOtel.js'; + +const client = Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', + tracePropagationTargets: [/\/v0/, 'v1'], + tracesSampleRate: 0, + integrations: [], + transport: loggingTransport, +}); + +setupOtel(client); diff --git a/dev-packages/node-core-integration-tests/suites/tracing/requests/http-unsampled/scenario.mjs b/dev-packages/node-core-integration-tests/suites/tracing/requests/http-unsampled/scenario.mjs new file mode 100644 index 000000000000..e470ae986985 --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/tracing/requests/http-unsampled/scenario.mjs @@ -0,0 +1,31 @@ +import * as Sentry from '@sentry/node-core'; +import * as http from 'http'; + +async function run() { + // Wrap in span that is not sampled + await Sentry.startSpan({ name: 'outer' }, async () => { + await makeHttpRequest(`${process.env.SERVER_URL}/api/v0`); + await makeHttpRequest(`${process.env.SERVER_URL}/api/v1`); + await makeHttpRequest(`${process.env.SERVER_URL}/api/v2`); + await makeHttpRequest(`${process.env.SERVER_URL}/api/v3`); + }); + + Sentry.captureException(new Error('foo')); +} + +run(); + +function makeHttpRequest(url) { + return new Promise(resolve => { + http + .request(url, httpRes => { + httpRes.on('data', () => { + // we don't care about data + }); + httpRes.on('end', () => { + resolve(); + }); + }) + .end(); + }); +} diff --git a/dev-packages/node-core-integration-tests/suites/tracing/requests/http-unsampled/test.ts b/dev-packages/node-core-integration-tests/suites/tracing/requests/http-unsampled/test.ts new file mode 100644 index 000000000000..60d3345dcb51 --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/tracing/requests/http-unsampled/test.ts @@ -0,0 +1,53 @@ +import { describe, expect } from 'vitest'; +import { conditionalTest } from '../../../../utils'; +import { createEsmAndCjsTests } from '../../../../utils/runner'; +import { createTestServer } from '../../../../utils/server'; + +describe('outgoing http', () => { + createEsmAndCjsTests(__dirname, 'scenario.mjs', 'instrument.mjs', (createRunner, test) => { + conditionalTest({ min: 22 })('node >=22', () => { + test('outgoing http requests are correctly instrumented when not sampled', async () => { + expect.assertions(11); + + const [SERVER_URL, closeTestServer] = await createTestServer() + .get('/api/v0', headers => { + expect(headers['baggage']).toEqual(expect.any(String)); + expect(headers['sentry-trace']).toEqual(expect.stringMatching(/^([a-f0-9]{32})-([a-f0-9]{16})-0$/)); + expect(headers['sentry-trace']).not.toEqual('00000000000000000000000000000000-0000000000000000-0'); + }) + .get('/api/v1', headers => { + expect(headers['baggage']).toEqual(expect.any(String)); + expect(headers['sentry-trace']).toEqual(expect.stringMatching(/^([a-f0-9]{32})-([a-f0-9]{16})-0$/)); + expect(headers['sentry-trace']).not.toEqual('00000000000000000000000000000000-0000000000000000-0'); + }) + .get('/api/v2', headers => { + expect(headers['baggage']).toBeUndefined(); + expect(headers['sentry-trace']).toBeUndefined(); + }) + .get('/api/v3', headers => { + expect(headers['baggage']).toBeUndefined(); + expect(headers['sentry-trace']).toBeUndefined(); + }) + .start(); + + await createRunner() + .withEnv({ SERVER_URL }) + .expect({ + event: { + exception: { + values: [ + { + type: 'Error', + value: 'foo', + }, + ], + }, + }, + }) + .start() + .completed(); + closeTestServer(); + }); + }); + }); +}); diff --git a/dev-packages/node-core-integration-tests/suites/tracing/sample-rand-propagation/server.js b/dev-packages/node-core-integration-tests/suites/tracing/sample-rand-propagation/server.js new file mode 100644 index 000000000000..81cc0fc8829c --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/tracing/sample-rand-propagation/server.js @@ -0,0 +1,41 @@ +const { loggingTransport } = require('@sentry-internal/node-core-integration-tests'); +const Sentry = require('@sentry/node-core'); +const { setupOtel } = require('../../../utils/setupOtel.js'); + +const client = Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + transport: loggingTransport, + tracesSampleRate: 0.00000001, // It's important that this is not 1, so that we also check logic for NonRecordingSpans, which is usually the edge-case +}); + +setupOtel(client); + +// express must be required after Sentry is initialized +const express = require('express'); +const cors = require('cors'); +const { + startExpressServerAndSendPortToRunner, + getPortAppIsRunningOn, +} = require('@sentry-internal/node-core-integration-tests'); + +const app = express(); + +app.use(cors()); + +app.get('/check', (req, res) => { + const appPort = getPortAppIsRunningOn(app); + // eslint-disable-next-line no-undef + fetch(`http://localhost:${appPort}/bounce`) + .then(r => r.json()) + .then(bounceRes => { + res.json({ propagatedData: bounceRes }); + }); +}); + +app.get('/bounce', (req, res) => { + res.json({ + baggage: req.headers['baggage'], + }); +}); + +startExpressServerAndSendPortToRunner(app); diff --git a/dev-packages/node-core-integration-tests/suites/tracing/sample-rand-propagation/test.ts b/dev-packages/node-core-integration-tests/suites/tracing/sample-rand-propagation/test.ts new file mode 100644 index 000000000000..40001a9f62f5 --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/tracing/sample-rand-propagation/test.ts @@ -0,0 +1,82 @@ +import { afterAll, describe, expect, test } from 'vitest'; +import { cleanupChildProcesses, createRunner } from '../../../utils/runner'; + +describe('sample_rand propagation', () => { + afterAll(() => { + cleanupChildProcesses(); + }); + + test('propagates a sample rand when there is a sentry-trace header and incoming sentry baggage', async () => { + const runner = createRunner(__dirname, 'server.js').start(); + const response = await runner.makeRequest('get', '/check', { + headers: { + 'sentry-trace': '530699e319cc067ce440315d74acb312-414dc2a08d5d1dac-1', + baggage: 'sentry-release=foo,sentry-sample_rand=0.424242', + }, + }); + expect(response).toEqual({ + propagatedData: { + baggage: expect.stringMatching(/sentry-sample_rand=0\.424242/), + }, + }); + }); + + test('does not propagate a sample rand when there is an incoming sentry-trace header but no baggage header', async () => { + const runner = createRunner(__dirname, 'server.js').start(); + const response = await runner.makeRequest('get', '/check', { + headers: { + 'sentry-trace': '530699e319cc067ce440315d74acb312-414dc2a08d5d1dac-1', + }, + }); + expect(response).toEqual({ + propagatedData: { + baggage: expect.not.stringMatching(/sentry-sample_rand=0\.[0-9]+/), + }, + }); + }); + + test('propagates a sample_rand that would lead to a positive sampling decision when there is an incoming positive sampling decision but no sample_rand in the baggage header', async () => { + const runner = createRunner(__dirname, 'server.js').start(); + const response = await runner.makeRequest('get', '/check', { + headers: { + 'sentry-trace': '530699e319cc067ce440315d74acb312-414dc2a08d5d1dac-1', + baggage: 'sentry-sample_rate=0.25', + }, + }); + + const sampleRand = Number((response as any).propagatedData.baggage.match(/sentry-sample_rand=(0\.[0-9]+)/)[1]); + + expect(sampleRand).toStrictEqual(expect.any(Number)); + expect(sampleRand).not.toBeNaN(); + expect(sampleRand).toBeLessThan(0.25); + expect(sampleRand).toBeGreaterThanOrEqual(0); + }); + + test('propagates a sample_rand that would lead to a negative sampling decision when there is an incoming negative sampling decision but no sample_rand in the baggage header', async () => { + const runner = createRunner(__dirname, 'server.js').start(); + const response = await runner.makeRequest('get', '/check', { + headers: { + 'sentry-trace': '530699e319cc067ce440315d74acb312-414dc2a08d5d1dac-0', + baggage: 'sentry-sample_rate=0.75', + }, + }); + + const sampleRand = Number((response as any).propagatedData.baggage.match(/sentry-sample_rand=(0\.[0-9]+)/)[1]); + + expect(sampleRand).toStrictEqual(expect.any(Number)); + expect(sampleRand).not.toBeNaN(); + expect(sampleRand).toBeGreaterThanOrEqual(0.75); + expect(sampleRand).toBeLessThan(1); + }); + + test('no sample_rand when there is no sentry-trace header but a baggage header with sample_rand', async () => { + const runner = createRunner(__dirname, 'server.js').start(); + const response = await runner.makeRequest('get', '/check', { + headers: { + baggage: 'sentry-sample_rate=0.75,sentry-sample_rand=0.5', + }, + }); + + expect((response as any).propagatedData.baggage).not.toMatch(/sentry-sample_rand=0\.[0-9]+/); + }); +}); diff --git a/dev-packages/node-core-integration-tests/suites/tracing/sample-rate-propagation/no-tracing-enabled/server.js b/dev-packages/node-core-integration-tests/suites/tracing/sample-rate-propagation/no-tracing-enabled/server.js new file mode 100644 index 000000000000..42121915d601 --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/tracing/sample-rate-propagation/no-tracing-enabled/server.js @@ -0,0 +1,40 @@ +const { loggingTransport } = require('@sentry-internal/node-core-integration-tests'); +const Sentry = require('@sentry/node-core'); +const { setupOtel } = require('../../../../utils/setupOtel.js'); + +const client = Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + transport: loggingTransport, +}); + +setupOtel(client); + +// express must be required after Sentry is initialized +const express = require('express'); +const cors = require('cors'); +const { + startExpressServerAndSendPortToRunner, + getPortAppIsRunningOn, +} = require('@sentry-internal/node-core-integration-tests'); + +const app = express(); + +app.use(cors()); + +app.get('/check', (req, res) => { + const appPort = getPortAppIsRunningOn(app); + // eslint-disable-next-line no-undef + fetch(`http://localhost:${appPort}/bounce`) + .then(r => r.json()) + .then(bounceRes => { + res.json({ propagatedData: bounceRes }); + }); +}); + +app.get('/bounce', (req, res) => { + res.json({ + baggage: req.headers['baggage'], + }); +}); + +startExpressServerAndSendPortToRunner(app); diff --git a/dev-packages/node-core-integration-tests/suites/tracing/sample-rate-propagation/no-tracing-enabled/test.ts b/dev-packages/node-core-integration-tests/suites/tracing/sample-rate-propagation/no-tracing-enabled/test.ts new file mode 100644 index 000000000000..b3040dc0cfa4 --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/tracing/sample-rate-propagation/no-tracing-enabled/test.ts @@ -0,0 +1,26 @@ +import { afterAll, describe, expect, test } from 'vitest'; +import { cleanupChildProcesses, createRunner } from '../../../../utils/runner'; + +describe('parentSampleRate propagation with no tracing enabled', () => { + afterAll(() => { + cleanupChildProcesses(); + }); + + test('should propagate an incoming sample rate', async () => { + const runner = createRunner(__dirname, 'server.js').start(); + const response = await runner.makeRequest('get', '/check', { + headers: { + 'sentry-trace': '530699e319cc067ce440315d74acb312-414dc2a08d5d1dac-1', + baggage: 'sentry-sample_rate=0.1337', + }, + }); + + expect((response as any).propagatedData.baggage).toMatch(/sentry-sample_rate=0\.1337/); + }); + + test('should not propagate a sample rate for root traces', async () => { + const runner = createRunner(__dirname, 'server.js').start(); + const response = await runner.makeRequest('get', '/check'); + expect((response as any).propagatedData.baggage).not.toMatch(/sentry-sample_rate/); + }); +}); diff --git a/dev-packages/node-core-integration-tests/suites/tracing/sample-rate-propagation/tracesSampleRate-0/server.js b/dev-packages/node-core-integration-tests/suites/tracing/sample-rate-propagation/tracesSampleRate-0/server.js new file mode 100644 index 000000000000..6c7042813246 --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/tracing/sample-rate-propagation/tracesSampleRate-0/server.js @@ -0,0 +1,41 @@ +const { loggingTransport } = require('@sentry-internal/node-core-integration-tests'); +const Sentry = require('@sentry/node-core'); +const { setupOtel } = require('../../../../utils/setupOtel.js'); + +const client = Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + transport: loggingTransport, + tracesSampleRate: 0, +}); + +setupOtel(client); + +// express must be required after Sentry is initialized +const express = require('express'); +const cors = require('cors'); +const { + startExpressServerAndSendPortToRunner, + getPortAppIsRunningOn, +} = require('@sentry-internal/node-core-integration-tests'); + +const app = express(); + +app.use(cors()); + +app.get('/check', (req, res) => { + const appPort = getPortAppIsRunningOn(app); + // eslint-disable-next-line no-undef + fetch(`http://localhost:${appPort}/bounce`) + .then(r => r.json()) + .then(bounceRes => { + res.json({ propagatedData: bounceRes }); + }); +}); + +app.get('/bounce', (req, res) => { + res.json({ + baggage: req.headers['baggage'], + }); +}); + +startExpressServerAndSendPortToRunner(app); diff --git a/dev-packages/node-core-integration-tests/suites/tracing/sample-rate-propagation/tracesSampleRate-0/test.ts b/dev-packages/node-core-integration-tests/suites/tracing/sample-rate-propagation/tracesSampleRate-0/test.ts new file mode 100644 index 000000000000..219e82dfeb12 --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/tracing/sample-rate-propagation/tracesSampleRate-0/test.ts @@ -0,0 +1,62 @@ +import { afterAll, describe, expect, test } from 'vitest'; +import { cleanupChildProcesses, createRunner } from '../../../../utils/runner'; + +describe('parentSampleRate propagation with tracesSampleRate=0', () => { + afterAll(() => { + cleanupChildProcesses(); + }); + + test('should propagate incoming sample rate when inheriting a positive sampling decision', async () => { + const runner = createRunner(__dirname, 'server.js').start(); + const response = await runner.makeRequest('get', '/check', { + headers: { + 'sentry-trace': '530699e319cc067ce440315d74acb312-414dc2a08d5d1dac-1', + baggage: 'sentry-sample_rate=0.1337', + }, + }); + + expect((response as any).propagatedData.baggage).toMatch(/sentry-sample_rate=0\.1337/); + }); + + test('should propagate incoming sample rate when inheriting a negative sampling decision', async () => { + const runner = createRunner(__dirname, 'server.js').start(); + const response = await runner.makeRequest('get', '/check', { + headers: { + 'sentry-trace': '530699e319cc067ce440315d74acb312-414dc2a08d5d1dac-0', + baggage: 'sentry-sample_rate=0.1337', + }, + }); + + expect((response as any).propagatedData.baggage).toMatch(/sentry-sample_rate=0\.1337/); + }); + + test('should not propagate a sample rate when receiving a trace without sampling decision and sample rate', async () => { + const runner = createRunner(__dirname, 'server.js').start(); + const response = await runner.makeRequest('get', '/check', { + headers: { + 'sentry-trace': '530699e319cc067ce440315d74acb312-414dc2a08d5d1dac', + baggage: '', + }, + }); + + expect((response as any).propagatedData.baggage).not.toMatch(/sentry-sample_rate=0/); + }); + + test('should propagate configured sample rate when receiving a trace without sampling decision, but with sample rate', async () => { + const runner = createRunner(__dirname, 'server.js').start(); + const response = await runner.makeRequest('get', '/check', { + headers: { + 'sentry-trace': '530699e319cc067ce440315d74acb312-414dc2a08d5d1dac', + baggage: 'sentry-sample_rate=0.1337', + }, + }); + + expect((response as any).propagatedData.baggage).toMatch(/sentry-sample_rate=0/); + }); + + test('should not propagate configured sample rate when there is no incoming trace', async () => { + const runner = createRunner(__dirname, 'server.js').start(); + const response = await runner.makeRequest('get', '/check'); + expect((response as any).propagatedData.baggage).not.toMatch(/sentry-sample_rate=0/); + }); +}); diff --git a/dev-packages/node-core-integration-tests/suites/tracing/sample-rate-propagation/tracesSampleRate/server.js b/dev-packages/node-core-integration-tests/suites/tracing/sample-rate-propagation/tracesSampleRate/server.js new file mode 100644 index 000000000000..a0ffeb98867a --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/tracing/sample-rate-propagation/tracesSampleRate/server.js @@ -0,0 +1,41 @@ +const { loggingTransport } = require('@sentry-internal/node-core-integration-tests'); +const Sentry = require('@sentry/node-core'); +const { setupOtel } = require('../../../../utils/setupOtel.js'); + +const client = Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + transport: loggingTransport, + tracesSampleRate: 0.69, +}); + +setupOtel(client); + +// express must be required after Sentry is initialized +const express = require('express'); +const cors = require('cors'); +const { + startExpressServerAndSendPortToRunner, + getPortAppIsRunningOn, +} = require('@sentry-internal/node-core-integration-tests'); + +const app = express(); + +app.use(cors()); + +app.get('/check', (req, res) => { + const appPort = getPortAppIsRunningOn(app); + // eslint-disable-next-line no-undef + fetch(`http://localhost:${appPort}/bounce`) + .then(r => r.json()) + .then(bounceRes => { + res.json({ propagatedData: bounceRes }); + }); +}); + +app.get('/bounce', (req, res) => { + res.json({ + baggage: req.headers['baggage'], + }); +}); + +startExpressServerAndSendPortToRunner(app); diff --git a/dev-packages/node-core-integration-tests/suites/tracing/sample-rate-propagation/tracesSampleRate/test.ts b/dev-packages/node-core-integration-tests/suites/tracing/sample-rate-propagation/tracesSampleRate/test.ts new file mode 100644 index 000000000000..147b4c13a1e1 --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/tracing/sample-rate-propagation/tracesSampleRate/test.ts @@ -0,0 +1,62 @@ +import { afterAll, describe, expect, test } from 'vitest'; +import { cleanupChildProcesses, createRunner } from '../../../../utils/runner'; + +describe('parentSampleRate propagation with tracesSampleRate', () => { + afterAll(() => { + cleanupChildProcesses(); + }); + + test('should propagate incoming sample rate when inheriting a positive sampling decision', async () => { + const runner = createRunner(__dirname, 'server.js').start(); + const response = await runner.makeRequest('get', '/check', { + headers: { + 'sentry-trace': '530699e319cc067ce440315d74acb312-414dc2a08d5d1dac-1', + baggage: 'sentry-sample_rate=0.1337', + }, + }); + + expect((response as any).propagatedData.baggage).toMatch(/sentry-sample_rate=0\.1337/); + }); + + test('should propagate incoming sample rate when inheriting a negative sampling decision', async () => { + const runner = createRunner(__dirname, 'server.js').start(); + const response = await runner.makeRequest('get', '/check', { + headers: { + 'sentry-trace': '530699e319cc067ce440315d74acb312-414dc2a08d5d1dac-0', + baggage: 'sentry-sample_rate=0.1337', + }, + }); + + expect((response as any).propagatedData.baggage).toMatch(/sentry-sample_rate=0\.1337/); + }); + + test('should not propagate configured sample rate when receiving a trace without sampling decision and sample rate', async () => { + const runner = createRunner(__dirname, 'server.js').start(); + const response = await runner.makeRequest('get', '/check', { + headers: { + 'sentry-trace': '530699e319cc067ce440315d74acb312-414dc2a08d5d1dac', + baggage: '', + }, + }); + + expect((response as any).propagatedData.baggage).not.toMatch(/sentry-sample_rate=0\.69/); + }); + + test('should not propagate configured sample rate when receiving a trace without sampling decision, but with sample rate', async () => { + const runner = createRunner(__dirname, 'server.js').start(); + const response = await runner.makeRequest('get', '/check', { + headers: { + 'sentry-trace': '530699e319cc067ce440315d74acb312-414dc2a08d5d1dac', + baggage: 'sentry-sample_rate=0.1337', + }, + }); + + expect((response as any).propagatedData.baggage).not.toMatch(/sentry-sample_rate=0\.69/); + }); + + test('should not propagate configured sample rate when there is no incoming trace', async () => { + const runner = createRunner(__dirname, 'server.js').start(); + const response = await runner.makeRequest('get', '/check'); + expect((response as any).propagatedData.baggage).not.toMatch(/sentry-sample_rate=0\.69/); + }); +}); diff --git a/dev-packages/node-core-integration-tests/suites/tracing/sample-rate-propagation/tracesSampler-with-otel-http-instrumentation/server.js b/dev-packages/node-core-integration-tests/suites/tracing/sample-rate-propagation/tracesSampler-with-otel-http-instrumentation/server.js new file mode 100644 index 000000000000..a4595dd8f3c8 --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/tracing/sample-rate-propagation/tracesSampler-with-otel-http-instrumentation/server.js @@ -0,0 +1,55 @@ +const { loggingTransport } = require('@sentry-internal/node-core-integration-tests'); +const { HttpInstrumentation } = require('@opentelemetry/instrumentation-http'); +const Sentry = require('@sentry/node-core'); +const { setupOtel } = require('../../../../utils/setupOtel.js'); + +const client = Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + transport: loggingTransport, + tracesSampler: ({ inheritOrSampleWith }) => { + return inheritOrSampleWith(0.69); + }, + openTelemetryInstrumentations: [new HttpInstrumentation()], +}); + +setupOtel(client); + +// express must be required after Sentry is initialized +const express = require('express'); +const cors = require('cors'); +const { + startExpressServerAndSendPortToRunner, + getPortAppIsRunningOn, +} = require('@sentry-internal/node-core-integration-tests'); + +const app = express(); + +app.use(cors()); + +app.get('/check', (req, res) => { + Sentry.startSpan({ name: 'check-endpoint' }, async () => { + const appPort = getPortAppIsRunningOn(app); + try { + // eslint-disable-next-line no-undef + const response = await fetch(`http://localhost:${appPort}/bounce`); + const bounceRes = await response.json(); + // eslint-disable-next-line no-console + console.log('Bounce response:', bounceRes); + res.json({ propagatedData: bounceRes }); + } catch (err) { + // eslint-disable-next-line no-console + console.error('Error fetching bounce:', err); + res.status(500).json({ error: err.message }); + } + }); +}); + +app.get('/bounce', (req, res) => { + // eslint-disable-next-line no-console + console.log('Bounce headers:', req.headers); + res.json({ + baggage: req.headers['baggage'], + }); +}); + +startExpressServerAndSendPortToRunner(app); diff --git a/dev-packages/node-core-integration-tests/suites/tracing/sample-rate-propagation/tracesSampler-with-otel-http-instrumentation/test.ts b/dev-packages/node-core-integration-tests/suites/tracing/sample-rate-propagation/tracesSampler-with-otel-http-instrumentation/test.ts new file mode 100644 index 000000000000..ffab071bbc26 --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/tracing/sample-rate-propagation/tracesSampler-with-otel-http-instrumentation/test.ts @@ -0,0 +1,62 @@ +import { afterAll, describe, expect, test } from 'vitest'; +import { cleanupChildProcesses, createRunner } from '../../../../utils/runner'; + +describe('parentSampleRate propagation with tracesSampler and OpenTelemetry HTTP instrumentation', () => { + afterAll(() => { + cleanupChildProcesses(); + }); + + test('should propagate sample_rate equivalent to sample rate returned by tracesSampler when there is no incoming trace', async () => { + const runner = createRunner(__dirname, 'server.js').start(); + const response = await runner.makeRequest('get', '/check'); + expect((response as any).propagatedData.baggage).toMatch(/sentry-sample_rate=0\.69/); + }); + + test('should propagate sample_rate equivalent to sample rate returned by tracesSampler when there is no incoming sample rate (1 -> because there is a positive sampling decision and inheritOrSampleWith was used)', async () => { + const runner = createRunner(__dirname, 'server.js').start(); + const response = await runner.makeRequest('get', '/check', { + headers: { + 'sentry-trace': '530699e319cc067ce440315d74acb312-414dc2a08d5d1dac-1', + baggage: '', + }, + }); + + expect((response as any).propagatedData.baggage).toMatch(/sentry-sample_rate=1/); + }); + + test('should propagate sample_rate equivalent to sample rate returned by tracesSampler when there is no incoming sample rate (0 -> because there is a negative sampling decision and inheritOrSampleWith was used)', async () => { + const runner = createRunner(__dirname, 'server.js').start(); + const response = await runner.makeRequest('get', '/check', { + headers: { + 'sentry-trace': '530699e319cc067ce440315d74acb312-414dc2a08d5d1dac-0', + baggage: '', + }, + }); + + expect((response as any).propagatedData.baggage).toMatch(/sentry-sample_rate=0/); + }); + + test('should propagate sample_rate equivalent to sample rate returned by tracesSampler when there is no incoming sample rate (the fallback value -> because there is no sampling decision and inheritOrSampleWith was used)', async () => { + const runner = createRunner(__dirname, 'server.js').start(); + const response = await runner.makeRequest('get', '/check', { + headers: { + 'sentry-trace': '530699e319cc067ce440315d74acb312-414dc2a08d5d1dac', + baggage: '', + }, + }); + + expect((response as any).propagatedData.baggage).toMatch(/sentry-sample_rate=0\.69/); + }); + + test('should propagate sample_rate equivalent to incoming sample_rate (because tracesSampler is configured that way)', async () => { + const runner = createRunner(__dirname, 'server.js').start(); + const response = await runner.makeRequest('get', '/check', { + headers: { + 'sentry-trace': '530699e319cc067ce440315d74acb312-414dc2a08d5d1dac-1', + baggage: 'sentry-sample_rate=0.1337', + }, + }); + + expect((response as any).propagatedData.baggage).toMatch(/sentry-sample_rate=0\.1337/); + }); +}); diff --git a/dev-packages/node-core-integration-tests/suites/tracing/tracePropagationTargets/baggage-org-id/server-no-explicit-org-id.ts b/dev-packages/node-core-integration-tests/suites/tracing/tracePropagationTargets/baggage-org-id/server-no-explicit-org-id.ts new file mode 100644 index 000000000000..2676a2f77bef --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/tracing/tracePropagationTargets/baggage-org-id/server-no-explicit-org-id.ts @@ -0,0 +1,35 @@ +import * as Sentry from '@sentry/node-core'; +import { loggingTransport, startExpressServerAndSendPortToRunner } from '@sentry-internal/node-core-integration-tests'; +import { setupOtel } from '../../../../utils/setupOtel'; + +export type TestAPIResponse = { test_data: { host: string; 'sentry-trace': string; baggage: string } }; + +const client = Sentry.init({ + dsn: 'https://public@o01234987.ingest.sentry.io/1337', + release: '1.0', + environment: 'prod', + tracesSampleRate: 1.0, + transport: loggingTransport, +}); + +setupOtel(client); + +import cors from 'cors'; +import express from 'express'; +import * as http from 'http'; + +const app = express(); + +app.use(cors()); + +app.get('/test/express', (_req, res) => { + const headers = http + .get({ + hostname: 'example.com', + }) + .getHeaders(); + + res.send({ test_data: headers }); +}); + +startExpressServerAndSendPortToRunner(app); diff --git a/dev-packages/node-core-integration-tests/suites/tracing/tracePropagationTargets/baggage-org-id/server-no-org-id.ts b/dev-packages/node-core-integration-tests/suites/tracing/tracePropagationTargets/baggage-org-id/server-no-org-id.ts new file mode 100644 index 000000000000..e291ab122ba1 --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/tracing/tracePropagationTargets/baggage-org-id/server-no-org-id.ts @@ -0,0 +1,35 @@ +import * as Sentry from '@sentry/node-core'; +import { loggingTransport, startExpressServerAndSendPortToRunner } from '@sentry-internal/node-core-integration-tests'; +import { setupOtel } from '../../../../utils/setupOtel'; + +export type TestAPIResponse = { test_data: { host: string; 'sentry-trace': string; baggage: string } }; + +const client = Sentry.init({ + dsn: 'https://public@public.ingest.sentry.io/1337', + release: '1.0', + environment: 'prod', + tracesSampleRate: 1.0, + transport: loggingTransport, +}); + +setupOtel(client); + +import cors from 'cors'; +import express from 'express'; +import * as http from 'http'; + +const app = express(); + +app.use(cors()); + +app.get('/test/express', (_req, res) => { + const headers = http + .get({ + hostname: 'example.com', + }) + .getHeaders(); + + res.send({ test_data: headers }); +}); + +startExpressServerAndSendPortToRunner(app); diff --git a/dev-packages/node-core-integration-tests/suites/tracing/tracePropagationTargets/baggage-org-id/server.ts b/dev-packages/node-core-integration-tests/suites/tracing/tracePropagationTargets/baggage-org-id/server.ts new file mode 100644 index 000000000000..5e4c9a7ea3dc --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/tracing/tracePropagationTargets/baggage-org-id/server.ts @@ -0,0 +1,36 @@ +import * as Sentry from '@sentry/node-core'; +import { loggingTransport, startExpressServerAndSendPortToRunner } from '@sentry-internal/node-core-integration-tests'; +import { setupOtel } from '../../../../utils/setupOtel'; + +export type TestAPIResponse = { test_data: { host: string; 'sentry-trace': string; baggage: string } }; + +const client = Sentry.init({ + dsn: 'https://public@o0000987.ingest.sentry.io/1337', + release: '1.0', + environment: 'prod', + orgId: '01234987', + tracesSampleRate: 1.0, + transport: loggingTransport, +}); + +setupOtel(client); + +import cors from 'cors'; +import express from 'express'; +import * as http from 'http'; + +const app = express(); + +app.use(cors()); + +app.get('/test/express', (_req, res) => { + const headers = http + .get({ + hostname: 'example.com', + }) + .getHeaders(); + + res.send({ test_data: headers }); +}); + +startExpressServerAndSendPortToRunner(app); diff --git a/dev-packages/node-core-integration-tests/suites/tracing/tracePropagationTargets/baggage-org-id/test.ts b/dev-packages/node-core-integration-tests/suites/tracing/tracePropagationTargets/baggage-org-id/test.ts new file mode 100644 index 000000000000..1c1fa4e6cf5f --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/tracing/tracePropagationTargets/baggage-org-id/test.ts @@ -0,0 +1,42 @@ +import { afterAll, expect, test } from 'vitest'; +import { conditionalTest } from '../../../../utils'; +import { cleanupChildProcesses, createRunner } from '../../../../utils/runner'; +import type { TestAPIResponse } from './server'; + +afterAll(() => { + cleanupChildProcesses(); +}); + +// This test requires Node.js 22+ because it depends on the 'http.client.request.created' +// diagnostic channel for baggage header propagation, which only exists since Node 22.12.0+ and 23.2.0+ +conditionalTest({ min: 22 })('node >=22', () => { + test('should include explicitly set org_id in the baggage header', async () => { + const runner = createRunner(__dirname, 'server.ts').start(); + + const response = await runner.makeRequest('get', '/test/express'); + expect(response).toBeDefined(); + + const baggage = response?.test_data.baggage; + expect(baggage).toContain('sentry-org_id=01234987'); + }); + + test('should extract org_id from DSN host when not explicitly set', async () => { + const runner = createRunner(__dirname, 'server-no-explicit-org-id.ts').start(); + + const response = await runner.makeRequest('get', '/test/express'); + expect(response).toBeDefined(); + + const baggage = response?.test_data.baggage; + expect(baggage).toContain('sentry-org_id=01234987'); + }); + + test('should set undefined org_id when it cannot be extracted', async () => { + const runner = createRunner(__dirname, 'server-no-org-id.ts').start(); + + const response = await runner.makeRequest('get', '/test/express'); + expect(response).toBeDefined(); + + const baggage = response?.test_data.baggage; + expect(baggage).not.toContain('sentry-org_id'); + }); +}); diff --git a/dev-packages/node-core-integration-tests/suites/tracing/tracePropagationTargets/scenario.ts b/dev-packages/node-core-integration-tests/suites/tracing/tracePropagationTargets/scenario.ts new file mode 100644 index 000000000000..52fc94f0496f --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/tracing/tracePropagationTargets/scenario.ts @@ -0,0 +1,39 @@ +import * as Sentry from '@sentry/node-core'; +import { loggingTransport } from '@sentry-internal/node-core-integration-tests'; +import { setupOtel } from '../../../utils/setupOtel'; + +const client = Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', + tracesSampleRate: 1.0, + tracePropagationTargets: [/\/v0/, 'v1'], + integrations: [], + transport: loggingTransport, +}); + +setupOtel(client); + +import * as http from 'http'; + +// eslint-disable-next-line @typescript-eslint/no-floating-promises +Sentry.startSpan({ name: 'test_span' }, async () => { + await makeHttpRequest(`${process.env.SERVER_URL}/api/v0`); + await makeHttpRequest(`${process.env.SERVER_URL}/api/v1`); + await makeHttpRequest(`${process.env.SERVER_URL}/api/v2`); + await makeHttpRequest(`${process.env.SERVER_URL}/api/v3`); +}); + +function makeHttpRequest(url: string): Promise { + return new Promise(resolve => { + http + .request(url, httpRes => { + httpRes.on('data', () => { + // we don't care about data + }); + httpRes.on('end', () => { + resolve(); + }); + }) + .end(); + }); +} diff --git a/dev-packages/node-core-integration-tests/suites/tracing/tracePropagationTargets/test.ts b/dev-packages/node-core-integration-tests/suites/tracing/tracePropagationTargets/test.ts new file mode 100644 index 000000000000..8ae06f883b38 --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/tracing/tracePropagationTargets/test.ts @@ -0,0 +1,44 @@ +import { expect, test } from 'vitest'; +import { conditionalTest } from '../../../utils'; +import { createRunner } from '../../../utils/runner'; +import { createTestServer } from '../../../utils/server'; + +// This test requires Node.js 22+ because it depends on the 'http.client.request.created' +// diagnostic channel for baggage header propagation, which only exists since Node 22.12.0+ and 23.2.0+ +conditionalTest({ min: 22 })('node >=22', () => { + test('SentryHttpIntegration should instrument correct requests when tracePropagationTargets option is provided', async () => { + expect.assertions(11); + + const [SERVER_URL, closeTestServer] = await createTestServer() + .get('/api/v0', headers => { + expect(headers['baggage']).toEqual(expect.any(String)); + expect(headers['sentry-trace']).toEqual(expect.stringMatching(/^([a-f0-9]{32})-([a-f0-9]{16})-1$/)); + expect(headers['sentry-trace']).not.toEqual('00000000000000000000000000000000-0000000000000000-1'); + }) + .get('/api/v1', headers => { + expect(headers['baggage']).toEqual(expect.any(String)); + expect(headers['sentry-trace']).toEqual(expect.stringMatching(/^([a-f0-9]{32})-([a-f0-9]{16})-1$/)); + expect(headers['sentry-trace']).not.toEqual('00000000000000000000000000000000-0000000000000000-1'); + }) + .get('/api/v2', headers => { + expect(headers['baggage']).toBeUndefined(); + expect(headers['sentry-trace']).toBeUndefined(); + }) + .get('/api/v3', headers => { + expect(headers['baggage']).toBeUndefined(); + expect(headers['sentry-trace']).toBeUndefined(); + }) + .start(); + + await createRunner(__dirname, 'scenario.ts') + .withEnv({ SERVER_URL }) + .expect({ + transaction: { + // we're not too concerned with the actual transaction here since this is tested elsewhere + }, + }) + .start() + .completed(); + closeTestServer(); + }); +}); diff --git a/dev-packages/node-core-integration-tests/suites/tsconfig.json b/dev-packages/node-core-integration-tests/suites/tsconfig.json new file mode 100644 index 000000000000..38ca0b13bcdd --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/tsconfig.json @@ -0,0 +1,3 @@ +{ + "extends": "../tsconfig.test.json" +} diff --git a/dev-packages/node-core-integration-tests/suites/winston/subject.ts b/dev-packages/node-core-integration-tests/suites/winston/subject.ts new file mode 100644 index 000000000000..c8840b855f9b --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/winston/subject.ts @@ -0,0 +1,76 @@ +import * as Sentry from '@sentry/node-core'; +import { loggingTransport } from '@sentry-internal/node-integration-tests'; +import winston from 'winston'; +import Transport from 'winston-transport'; +import { setupOtel } from '../../utils/setupOtel'; + +const client = Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0.0', + environment: 'test', + _experiments: { + enableLogs: true, + }, + transport: loggingTransport, +}); + +setupOtel(client); + +async function run(): Promise { + // Create a custom transport that extends winston-transport + const SentryWinstonTransport = Sentry.createSentryWinstonTransport(Transport); + + // Create logger with default levels + const logger = winston.createLogger({ + transports: [new SentryWinstonTransport()], + }); + + // Test basic logging + logger.info('Test info message'); + logger.error('Test error message'); + + // If custom levels are requested + if (process.env.CUSTOM_LEVELS === 'true') { + const customLevels = { + levels: { + error: 0, + warn: 1, + info: 2, + http: 3, + verbose: 4, + debug: 5, + silly: 6, + }, + colors: { + error: 'red', + warn: 'yellow', + info: 'green', + http: 'magenta', + verbose: 'cyan', + debug: 'blue', + silly: 'grey', + }, + }; + + const customLogger = winston.createLogger({ + levels: customLevels.levels, + transports: [new SentryWinstonTransport()], + }); + + customLogger.info('Test info message'); + customLogger.error('Test error message'); + } + + // If metadata is requested + if (process.env.WITH_METADATA === 'true') { + logger.info('Test message with metadata', { + foo: 'bar', + number: 42, + }); + } + + await Sentry.flush(); +} + +// eslint-disable-next-line @typescript-eslint/no-floating-promises +void run(); diff --git a/dev-packages/node-core-integration-tests/suites/winston/test.ts b/dev-packages/node-core-integration-tests/suites/winston/test.ts new file mode 100644 index 000000000000..034210f8690b --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/winston/test.ts @@ -0,0 +1,186 @@ +import { afterAll, describe, expect, test } from 'vitest'; +import { cleanupChildProcesses, createRunner } from '../../utils/runner'; + +describe('winston integration', () => { + afterAll(() => { + cleanupChildProcesses(); + }); + + test('should capture winston logs with default levels', async () => { + const runner = createRunner(__dirname, 'subject.ts') + .expect({ + log: { + items: [ + { + timestamp: expect.any(Number), + level: 'info', + body: 'Test info message', + severity_number: expect.any(Number), + trace_id: expect.any(String), + attributes: { + 'sentry.origin': { value: 'auto.logging.winston', type: 'string' }, + 'sentry.release': { value: '1.0.0', type: 'string' }, + 'sentry.environment': { value: 'test', type: 'string' }, + 'sentry.sdk.name': { value: 'sentry.javascript.node', type: 'string' }, + 'sentry.sdk.version': { value: expect.any(String), type: 'string' }, + 'server.address': { value: expect.any(String), type: 'string' }, + }, + }, + { + timestamp: expect.any(Number), + level: 'error', + body: 'Test error message', + severity_number: expect.any(Number), + trace_id: expect.any(String), + attributes: { + 'sentry.origin': { value: 'auto.logging.winston', type: 'string' }, + 'sentry.release': { value: '1.0.0', type: 'string' }, + 'sentry.environment': { value: 'test', type: 'string' }, + 'sentry.sdk.name': { value: 'sentry.javascript.node', type: 'string' }, + 'sentry.sdk.version': { value: expect.any(String), type: 'string' }, + 'server.address': { value: expect.any(String), type: 'string' }, + }, + }, + ], + }, + }) + .start(); + + await runner.completed(); + }); + + test('should capture winston logs with custom levels', async () => { + const runner = createRunner(__dirname, 'subject.ts') + .withEnv({ CUSTOM_LEVELS: 'true' }) + .expect({ + log: { + items: [ + { + timestamp: expect.any(Number), + level: 'info', + body: 'Test info message', + severity_number: expect.any(Number), + trace_id: expect.any(String), + attributes: { + 'sentry.origin': { value: 'auto.logging.winston', type: 'string' }, + 'sentry.release': { value: '1.0.0', type: 'string' }, + 'sentry.environment': { value: 'test', type: 'string' }, + 'sentry.sdk.name': { value: 'sentry.javascript.node', type: 'string' }, + 'sentry.sdk.version': { value: expect.any(String), type: 'string' }, + 'server.address': { value: expect.any(String), type: 'string' }, + }, + }, + { + timestamp: expect.any(Number), + level: 'error', + body: 'Test error message', + severity_number: expect.any(Number), + trace_id: expect.any(String), + attributes: { + 'sentry.origin': { value: 'auto.logging.winston', type: 'string' }, + 'sentry.release': { value: '1.0.0', type: 'string' }, + 'sentry.environment': { value: 'test', type: 'string' }, + 'sentry.sdk.name': { value: 'sentry.javascript.node', type: 'string' }, + 'sentry.sdk.version': { value: expect.any(String), type: 'string' }, + 'server.address': { value: expect.any(String), type: 'string' }, + }, + }, + { + timestamp: expect.any(Number), + level: 'info', + body: 'Test info message', + severity_number: expect.any(Number), + trace_id: expect.any(String), + attributes: { + 'sentry.origin': { value: 'auto.logging.winston', type: 'string' }, + 'sentry.release': { value: '1.0.0', type: 'string' }, + 'sentry.environment': { value: 'test', type: 'string' }, + 'sentry.sdk.name': { value: 'sentry.javascript.node', type: 'string' }, + 'sentry.sdk.version': { value: expect.any(String), type: 'string' }, + 'server.address': { value: expect.any(String), type: 'string' }, + }, + }, + { + timestamp: expect.any(Number), + level: 'error', + body: 'Test error message', + severity_number: expect.any(Number), + trace_id: expect.any(String), + attributes: { + 'sentry.origin': { value: 'auto.logging.winston', type: 'string' }, + 'sentry.release': { value: '1.0.0', type: 'string' }, + 'sentry.environment': { value: 'test', type: 'string' }, + 'sentry.sdk.name': { value: 'sentry.javascript.node', type: 'string' }, + 'sentry.sdk.version': { value: expect.any(String), type: 'string' }, + 'server.address': { value: expect.any(String), type: 'string' }, + }, + }, + ], + }, + }) + .start(); + + await runner.completed(); + }); + + test('should capture winston logs with metadata', async () => { + const runner = createRunner(__dirname, 'subject.ts') + .withEnv({ WITH_METADATA: 'true' }) + .expect({ + log: { + items: [ + { + timestamp: expect.any(Number), + level: 'info', + body: 'Test info message', + severity_number: expect.any(Number), + trace_id: expect.any(String), + attributes: { + 'sentry.origin': { value: 'auto.logging.winston', type: 'string' }, + 'sentry.release': { value: '1.0.0', type: 'string' }, + 'sentry.environment': { value: 'test', type: 'string' }, + 'sentry.sdk.name': { value: 'sentry.javascript.node', type: 'string' }, + 'sentry.sdk.version': { value: expect.any(String), type: 'string' }, + 'server.address': { value: expect.any(String), type: 'string' }, + }, + }, + { + timestamp: expect.any(Number), + level: 'error', + body: 'Test error message', + severity_number: expect.any(Number), + trace_id: expect.any(String), + attributes: { + 'sentry.origin': { value: 'auto.logging.winston', type: 'string' }, + 'sentry.release': { value: '1.0.0', type: 'string' }, + 'sentry.environment': { value: 'test', type: 'string' }, + 'sentry.sdk.name': { value: 'sentry.javascript.node', type: 'string' }, + 'sentry.sdk.version': { value: expect.any(String), type: 'string' }, + 'server.address': { value: expect.any(String), type: 'string' }, + }, + }, + { + timestamp: expect.any(Number), + level: 'info', + body: 'Test message with metadata', + severity_number: expect.any(Number), + trace_id: expect.any(String), + attributes: { + 'sentry.origin': { value: 'auto.logging.winston', type: 'string' }, + 'sentry.release': { value: '1.0.0', type: 'string' }, + 'sentry.environment': { value: 'test', type: 'string' }, + 'sentry.sdk.name': { value: 'sentry.javascript.node', type: 'string' }, + 'sentry.sdk.version': { value: expect.any(String), type: 'string' }, + 'server.address': { value: expect.any(String), type: 'string' }, + foo: { value: 'bar', type: 'string' }, + number: { value: 42, type: 'integer' }, + }, + }, + ], + }, + }) + .start(); + + await runner.completed(); + }); +}); diff --git a/dev-packages/node-core-integration-tests/test.txt b/dev-packages/node-core-integration-tests/test.txt new file mode 100644 index 000000000000..0a0fa7f94de9 --- /dev/null +++ b/dev-packages/node-core-integration-tests/test.txt @@ -0,0 +1,213 @@ +yarn run v1.22.22 +$ /Users/abhijeetprasad/workspace/sentry-javascript/node_modules/.bin/jest contextLines/memory-leak + console.log + starting scenario /Users/abhijeetprasad/workspace/sentry-javascript/dev-packages/node-integration-tests/suites/contextLines/memory-leak/scenario.ts [ '-r', 'ts-node/register' ] undefined + + at log (utils/runner.ts:462:11) + + console.log + line COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME + + at log (utils/runner.ts:462:11) + + console.log + line node 90932 abhijeetprasad cwd DIR 1,16 608 107673020 /Users/abhijeetprasad/workspace/sentry-javascript/dev-packages/node-integration-tests + + at log (utils/runner.ts:462:11) + + console.log + line node 90932 abhijeetprasad txt REG 1,16 88074480 114479727 /Users/abhijeetprasad/.volta/tools/image/node/18.20.5/bin/node + + at log (utils/runner.ts:462:11) + + console.log + line node 90932 abhijeetprasad 0u unix 0x6a083c8cc83ea8db 0t0 ->0xf2cacdd1d3a0ebec + + at log (utils/runner.ts:462:11) + + console.log + line node 90932 abhijeetprasad 1u unix 0xd99cc422a76ba47f 0t0 ->0x542148981a0b9ef2 + + at log (utils/runner.ts:462:11) + + console.log + line node 90932 abhijeetprasad 2u unix 0x97e70527ed5803f8 0t0 ->0xbafdaf00ef20de83 + + at log (utils/runner.ts:462:11) + + console.log + line node 90932 abhijeetprasad 3u KQUEUE count=0, state=0 + + at log (utils/runner.ts:462:11) + + console.log + line node 90932 abhijeetprasad 4 PIPE 0x271836c29e42bc67 16384 ->0x16ac23fcfd4fe1a3 + + at log (utils/runner.ts:462:11) + + console.log + line node 90932 abhijeetprasad 5 PIPE 0x16ac23fcfd4fe1a3 16384 ->0x271836c29e42bc67 + + at log (utils/runner.ts:462:11) + + console.log + line node 90932 abhijeetprasad 6 PIPE 0xd76fcd4ca2a35fcf 16384 ->0x30d26cd4f0e069b2 + + at log (utils/runner.ts:462:11) + + console.log + line node 90932 abhijeetprasad 7 PIPE 0x30d26cd4f0e069b2 16384 ->0xd76fcd4ca2a35fcf + + at log (utils/runner.ts:462:11) + + console.log + line node 90932 abhijeetprasad 8 PIPE 0x37691847717c3d6 16384 ->0x966eedd79d018252 + + at log (utils/runner.ts:462:11) + + console.log + line node 90932 abhijeetprasad 9 PIPE 0x966eedd79d018252 16384 ->0x37691847717c3d6 + + at log (utils/runner.ts:462:11) + + console.log + line node 90932 abhijeetprasad 10u KQUEUE count=0, state=0xa + + at log (utils/runner.ts:462:11) + + console.log + line node 90932 abhijeetprasad 11 PIPE 0x99c1186f14b865be 16384 ->0xe88675eb1eefb2b + + at log (utils/runner.ts:462:11) + + console.log + line node 90932 abhijeetprasad 12 PIPE 0xe88675eb1eefb2b 16384 ->0x99c1186f14b865be + + at log (utils/runner.ts:462:11) + + console.log + line node 90932 abhijeetprasad 13 PIPE 0x52173210451cdda9 16384 ->0x50bbc31a0f1cc1af + + at log (utils/runner.ts:462:11) + + console.log + line node 90932 abhijeetprasad 14 PIPE 0x50bbc31a0f1cc1af 16384 ->0x52173210451cdda9 + + at log (utils/runner.ts:462:11) + + console.log + line node 90932 abhijeetprasad 15u KQUEUE count=0, state=0 + + at log (utils/runner.ts:462:11) + + console.log + line node 90932 abhijeetprasad 16 PIPE 0xa115aa0653327e72 16384 ->0x100525c465ee1eb0 + + at log (utils/runner.ts:462:11) + + console.log + line node 90932 abhijeetprasad 17 PIPE 0x100525c465ee1eb0 16384 ->0xa115aa0653327e72 + + at log (utils/runner.ts:462:11) + + console.log + line node 90932 abhijeetprasad 18 PIPE 0x41945cf9fe740277 16384 ->0x8791d18eade5b1e0 + + at log (utils/runner.ts:462:11) + + console.log + line node 90932 abhijeetprasad 19 PIPE 0x8791d18eade5b1e0 16384 ->0x41945cf9fe740277 + + at log (utils/runner.ts:462:11) + + console.log + line node 90932 abhijeetprasad 20r CHR 3,2 0t0 333 /dev/null + + at log (utils/runner.ts:462:11) + + console.log + line node 90932 abhijeetprasad 21u KQUEUE count=0, state=0xa + + at log (utils/runner.ts:462:11) + + console.log + line node 90932 abhijeetprasad 22 PIPE 0xf4c6a2f47fb0bff5 16384 ->0xa00185e1c59cedbe + + at log (utils/runner.ts:462:11) + + console.log + line node 90932 abhijeetprasad 23 PIPE 0xa00185e1c59cedbe 16384 ->0xf4c6a2f47fb0bff5 + + at log (utils/runner.ts:462:11) + + console.log + line node 90932 abhijeetprasad 24 PIPE 0x4ac25a99f45f7ca4 16384 ->0x2032aef840c94700 + + at log (utils/runner.ts:462:11) + + console.log + line node 90932 abhijeetprasad 25 PIPE 0x2032aef840c94700 16384 ->0x4ac25a99f45f7ca4 + + at log (utils/runner.ts:462:11) + + console.log + line null + + at log (utils/runner.ts:462:11) + + console.log + line [{"sent_at":"2025-01-13T21:47:47.663Z","sdk":{"name":"sentry.javascript.node","version":"8.45.0"}},[[{"type":"session"},{"sid":"0ae9ef2ac2ba49dd92b6dab9d81444ac","init":true,"started":"2025-01-13T21:47:47.502Z","timestamp":"2025-01-13T21:47:47.663Z","status":"ok","errors":1,"duration":0.16146087646484375,"attrs":{"release":"1.0","environment":"production"}}]]] + + at log (utils/runner.ts:462:11) + + console.log + line [{"event_id":"2626269e3c634fc289338c441e76412c","sent_at":"2025-01-13T21:47:47.663Z","sdk":{"name":"sentry.javascript.node","version":"8.45.0"},"trace":{"environment":"production","release":"1.0","public_key":"public","trace_id":"efdb9350effb47959d48bd0aaf395824"}},[[{"type":"event"},{"exception":{"values":[{"type":"Error","value":"error in loop 0","stacktrace":{"frames":[{"filename":"node:internal/main/run_main_module","module":"run_main_module","function":"?","lineno":28,"colno":49,"in_app":false},{"filename":"node:internal/modules/run_main","module":"run_main","function":"Function.executeUserEntryPoint [as runMain]","lineno":128,"colno":12,"in_app":false},{"filename":"node:internal/modules/cjs/loader","module":"loader","function":"Module._load","lineno":1019,"colno":12,"in_app":false},{"filename":"node:internal/modules/cjs/loader","module":"loader","function":"Module.load","lineno":1203,"colno":32,"in_app":false},{"filename":"/Users/abhijeetprasad/workspace/sentry-javascript/node_modules/ts-node/src/index.ts","module":"ts-node.src:index.ts","function":"Object.require.extensions. [as .ts]","lineno":1621,"colno":12,"in_app":false},{"filename":"node:internal/modules/cjs/loader","module":"loader","function":"Module._extensions..js","lineno":1422,"colno":10,"in_app":false},{"filename":"/Users/abhijeetprasad/workspace/sentry-javascript/node_modules/ts-node/src/index.ts","module":"ts-node.src:index.ts","function":"Module.m._compile","lineno":1618,"colno":23,"in_app":false},{"filename":"node:internal/modules/cjs/loader","module":"loader","function":"Module._compile","lineno":1364,"colno":14,"in_app":false},{"filename":"/Users/abhijeetprasad/workspace/sentry-javascript/dev-packages/node-integration-tests/suites/contextLines/memory-leak/scenario.ts","module":"scenario.ts","function":"Object.?","lineno":14,"colno":10,"in_app":true,"pre_context":[" dsn: 'https://public@dsn.ingest.sentry.io/1337',"," release: '1.0',"," transport: loggingTransport,","});","","import { runSentry } from './other-file';",""],"context_line":"runSentry();","post_context":["","console.log(execSync(`lsof -p ${process.pid}`, { stdio: 'inherit', cwd: process.cwd() }));"]},{"filename":"/Users/abhijeetprasad/workspace/sentry-javascript/dev-packages/node-integration-tests/suites/contextLines/memory-leak/other-file.ts","module":"other-file.ts","function":"runSentry","lineno":5,"colno":29,"in_app":true,"pre_context":["import * as Sentry from '@sentry/node';","","export function runSentry(): void {"," for (let i = 0; i < 10; i++) {"],"context_line":" Sentry.captureException(new Error(`error in loop ${i}`));","post_context":[" }","}"]}]},"mechanism":{"type":"generic","handled":true}}]},"event_id":"2626269e3c634fc289338c441e76412c","level":"error","platform":"node","contexts":{"trace":{"trace_id":"efdb9350effb47959d48bd0aaf395824","span_id":"b1e1b8a0d410ef14"},"runtime":{"name":"node","version":"v18.20.5"},"app":{"app_start_time":"2025-01-13T21:47:46.327Z","app_memory":270073856},"os":{"kernel_version":"23.6.0","name":"macOS","version":"14.7","build":"23H124"},"device":{"boot_time":"2024-12-23T16:56:50.637Z","arch":"arm64","memory_size":34359738368,"free_memory":355794944,"processor_count":10,"cpu_description":"Apple M1 Pro","processor_frequency":24},"culture":{"locale":"en-CA","timezone":"America/Toronto"},"cloud_resource":{}},"server_name":"GT9RQ02WW5.local","timestamp":1736804867.528,"environment":"production","release":"1.0","sdk":{"integrations":["InboundFilters","FunctionToString","LinkedErrors","RequestData","Console","Http","NodeFetch","OnUncaughtException","OnUnhandledRejection","ContextLines","LocalVariables","Context","ChildProcess","Modules"],"name":"sentry.javascript.node","version":"8.45.0","packages":[{"name":"npm:@sentry/node","version":"8.45.0"}]},"modules":{"ts-node":"10.9.1","make-error":"1.3.6","yn":"3.1.1","arg":"4.1.3","v8-compile-cache-lib":"3.0.1","typescript":"5.0.4","tslib":"2.7.0","semver":"7.6.3","shimmer":"1.2.1","require-in-the-middle":"7.2.0","resolve":"1.22.1","is-core-module":"2.11.0","has":"1.0.3","function-bind":"1.1.1","debug":"4.3.4","supports-color":"7.2.0","has-flag":"4.0.0","module-details-from-path":"1.0.3","import-in-the-middle":"1.12.0","forwarded-parse":"2.1.2"}}]]] + + at log (utils/runner.ts:462:11) + + console.log + line [{"event_id":"f58236bf0a7f4a999f7daf5283f0400f","sent_at":"2025-01-13T21:47:47.664Z","sdk":{"name":"sentry.javascript.node","version":"8.45.0"},"trace":{"environment":"production","release":"1.0","public_key":"public","trace_id":"efdb9350effb47959d48bd0aaf395824"}},[[{"type":"event"},{"exception":{"values":[{"type":"Error","value":"error in loop 1","stacktrace":{"frames":[{"filename":"node:internal/main/run_main_module","module":"run_main_module","function":"?","lineno":28,"colno":49,"in_app":false},{"filename":"node:internal/modules/run_main","module":"run_main","function":"Function.executeUserEntryPoint [as runMain]","lineno":128,"colno":12,"in_app":false},{"filename":"node:internal/modules/cjs/loader","module":"loader","function":"Module._load","lineno":1019,"colno":12,"in_app":false},{"filename":"node:internal/modules/cjs/loader","module":"loader","function":"Module.load","lineno":1203,"colno":32,"in_app":false},{"filename":"/Users/abhijeetprasad/workspace/sentry-javascript/node_modules/ts-node/src/index.ts","module":"ts-node.src:index.ts","function":"Object.require.extensions. [as .ts]","lineno":1621,"colno":12,"in_app":false},{"filename":"node:internal/modules/cjs/loader","module":"loader","function":"Module._extensions..js","lineno":1422,"colno":10,"in_app":false},{"filename":"/Users/abhijeetprasad/workspace/sentry-javascript/node_modules/ts-node/src/index.ts","module":"ts-node.src:index.ts","function":"Module.m._compile","lineno":1618,"colno":23,"in_app":false},{"filename":"node:internal/modules/cjs/loader","module":"loader","function":"Module._compile","lineno":1364,"colno":14,"in_app":false},{"filename":"/Users/abhijeetprasad/workspace/sentry-javascript/dev-packages/node-integration-tests/suites/contextLines/memory-leak/scenario.ts","module":"scenario.ts","function":"Object.?","lineno":14,"colno":10,"in_app":true,"pre_context":[" dsn: 'https://public@dsn.ingest.sentry.io/1337',"," release: '1.0',"," transport: loggingTransport,","});","","import { runSentry } from './other-file';",""],"context_line":"runSentry();","post_context":["","console.log(execSync(`lsof -p ${process.pid}`, { stdio: 'inherit', cwd: process.cwd() }));"]},{"filename":"/Users/abhijeetprasad/workspace/sentry-javascript/dev-packages/node-integration-tests/suites/contextLines/memory-leak/other-file.ts","module":"other-file.ts","function":"runSentry","lineno":5,"colno":29,"in_app":true,"pre_context":["import * as Sentry from '@sentry/node';","","export function runSentry(): void {"," for (let i = 0; i < 10; i++) {"],"context_line":" Sentry.captureException(new Error(`error in loop ${i}`));","post_context":[" }","}"]}]},"mechanism":{"type":"generic","handled":true}}]},"event_id":"f58236bf0a7f4a999f7daf5283f0400f","level":"error","platform":"node","contexts":{"trace":{"trace_id":"efdb9350effb47959d48bd0aaf395824","span_id":"9b6ccaf59536bcb4"},"runtime":{"name":"node","version":"v18.20.5"},"app":{"app_start_time":"2025-01-13T21:47:46.327Z","app_memory":270073856},"os":{"kernel_version":"23.6.0","name":"macOS","version":"14.7","build":"23H124"},"device":{"boot_time":"2024-12-23T16:56:50.637Z","arch":"arm64","memory_size":34359738368,"free_memory":355794944,"processor_count":10,"cpu_description":"Apple M1 Pro","processor_frequency":24},"culture":{"locale":"en-CA","timezone":"America/Toronto"},"cloud_resource":{}},"server_name":"GT9RQ02WW5.local","timestamp":1736804867.531,"environment":"production","release":"1.0","sdk":{"integrations":["InboundFilters","FunctionToString","LinkedErrors","RequestData","Console","Http","NodeFetch","OnUncaughtException","OnUnhandledRejection","ContextLines","LocalVariables","Context","ChildProcess","Modules"],"name":"sentry.javascript.node","version":"8.45.0","packages":[{"name":"npm:@sentry/node","version":"8.45.0"}]},"modules":{"ts-node":"10.9.1","make-error":"1.3.6","yn":"3.1.1","arg":"4.1.3","v8-compile-cache-lib":"3.0.1","typescript":"5.0.4","tslib":"2.7.0","semver":"7.6.3","shimmer":"1.2.1","require-in-the-middle":"7.2.0","resolve":"1.22.1","is-core-module":"2.11.0","has":"1.0.3","function-bind":"1.1.1","debug":"4.3.4","supports-color":"7.2.0","has-flag":"4.0.0","module-details-from-path":"1.0.3","import-in-the-middle":"1.12.0","forwarded-parse":"2.1.2"}}]]] + + at log (utils/runner.ts:462:11) + + console.log + line [{"event_id":"d4d1b66dc41b44b98df2d2ff5d5370a2","sent_at":"2025-01-13T21:47:47.665Z","sdk":{"name":"sentry.javascript.node","version":"8.45.0"},"trace":{"environment":"production","release":"1.0","public_key":"public","trace_id":"efdb9350effb47959d48bd0aaf395824"}},[[{"type":"event"},{"exception":{"values":[{"type":"Error","value":"error in loop 2","stacktrace":{"frames":[{"filename":"node:internal/main/run_main_module","module":"run_main_module","function":"?","lineno":28,"colno":49,"in_app":false},{"filename":"node:internal/modules/run_main","module":"run_main","function":"Function.executeUserEntryPoint [as runMain]","lineno":128,"colno":12,"in_app":false},{"filename":"node:internal/modules/cjs/loader","module":"loader","function":"Module._load","lineno":1019,"colno":12,"in_app":false},{"filename":"node:internal/modules/cjs/loader","module":"loader","function":"Module.load","lineno":1203,"colno":32,"in_app":false},{"filename":"/Users/abhijeetprasad/workspace/sentry-javascript/node_modules/ts-node/src/index.ts","module":"ts-node.src:index.ts","function":"Object.require.extensions. [as .ts]","lineno":1621,"colno":12,"in_app":false},{"filename":"node:internal/modules/cjs/loader","module":"loader","function":"Module._extensions..js","lineno":1422,"colno":10,"in_app":false},{"filename":"/Users/abhijeetprasad/workspace/sentry-javascript/node_modules/ts-node/src/index.ts","module":"ts-node.src:index.ts","function":"Module.m._compile","lineno":1618,"colno":23,"in_app":false},{"filename":"node:internal/modules/cjs/loader","module":"loader","function":"Module._compile","lineno":1364,"colno":14,"in_app":false},{"filename":"/Users/abhijeetprasad/workspace/sentry-javascript/dev-packages/node-integration-tests/suites/contextLines/memory-leak/scenario.ts","module":"scenario.ts","function":"Object.?","lineno":14,"colno":10,"in_app":true,"pre_context":[" dsn: 'https://public@dsn.ingest.sentry.io/1337',"," release: '1.0',"," transport: loggingTransport,","});","","import { runSentry } from './other-file';",""],"context_line":"runSentry();","post_context":["","console.log(execSync(`lsof -p ${process.pid}`, { stdio: 'inherit', cwd: process.cwd() }));"]},{"filename":"/Users/abhijeetprasad/workspace/sentry-javascript/dev-packages/node-integration-tests/suites/contextLines/memory-leak/other-file.ts","module":"other-file.ts","function":"runSentry","lineno":5,"colno":29,"in_app":true,"pre_context":["import * as Sentry from '@sentry/node';","","export function runSentry(): void {"," for (let i = 0; i < 10; i++) {"],"context_line":" Sentry.captureException(new Error(`error in loop ${i}`));","post_context":[" }","}"]}]},"mechanism":{"type":"generic","handled":true}}]},"event_id":"d4d1b66dc41b44b98df2d2ff5d5370a2","level":"error","platform":"node","contexts":{"trace":{"trace_id":"efdb9350effb47959d48bd0aaf395824","span_id":"82d56f443d3f01f9"},"runtime":{"name":"node","version":"v18.20.5"},"app":{"app_start_time":"2025-01-13T21:47:46.327Z","app_memory":270073856},"os":{"kernel_version":"23.6.0","name":"macOS","version":"14.7","build":"23H124"},"device":{"boot_time":"2024-12-23T16:56:50.637Z","arch":"arm64","memory_size":34359738368,"free_memory":355794944,"processor_count":10,"cpu_description":"Apple M1 Pro","processor_frequency":24},"culture":{"locale":"en-CA","timezone":"America/Toronto"},"cloud_resource":{}},"server_name":"GT9RQ02WW5.local","timestamp":1736804867.532,"environment":"production","release":"1.0","sdk":{"integrations":["InboundFilters","FunctionToString","LinkedErrors","RequestData","Console","Http","NodeFetch","OnUncaughtException","OnUnhandledRejection","ContextLines","LocalVariables","Context","ChildProcess","Modules"],"name":"sentry.javascript.node","version":"8.45.0","packages":[{"name":"npm:@sentry/node","version":"8.45.0"}]},"modules":{"ts-node":"10.9.1","make-error":"1.3.6","yn":"3.1.1","arg":"4.1.3","v8-compile-cache-lib":"3.0.1","typescript":"5.0.4","tslib":"2.7.0","semver":"7.6.3","shimmer":"1.2.1","require-in-the-middle":"7.2.0","resolve":"1.22.1","is-core-module":"2.11.0","has":"1.0.3","function-bind":"1.1.1","debug":"4.3.4","supports-color":"7.2.0","has-flag":"4.0.0","module-details-from-path":"1.0.3","import-in-the-middle":"1.12.0","forwarded-parse":"2.1.2"}}]]] + + at log (utils/runner.ts:462:11) + + console.log + line [{"event_id":"293d7c8c731c48eca30735b41efd40ba","sent_at":"2025-01-13T21:47:47.665Z","sdk":{"name":"sentry.javascript.node","version":"8.45.0"},"trace":{"environment":"production","release":"1.0","public_key":"public","trace_id":"efdb9350effb47959d48bd0aaf395824"}},[[{"type":"event"},{"exception":{"values":[{"type":"Error","value":"error in loop 3","stacktrace":{"frames":[{"filename":"node:internal/main/run_main_module","module":"run_main_module","function":"?","lineno":28,"colno":49,"in_app":false},{"filename":"node:internal/modules/run_main","module":"run_main","function":"Function.executeUserEntryPoint [as runMain]","lineno":128,"colno":12,"in_app":false},{"filename":"node:internal/modules/cjs/loader","module":"loader","function":"Module._load","lineno":1019,"colno":12,"in_app":false},{"filename":"node:internal/modules/cjs/loader","module":"loader","function":"Module.load","lineno":1203,"colno":32,"in_app":false},{"filename":"/Users/abhijeetprasad/workspace/sentry-javascript/node_modules/ts-node/src/index.ts","module":"ts-node.src:index.ts","function":"Object.require.extensions. [as .ts]","lineno":1621,"colno":12,"in_app":false},{"filename":"node:internal/modules/cjs/loader","module":"loader","function":"Module._extensions..js","lineno":1422,"colno":10,"in_app":false},{"filename":"/Users/abhijeetprasad/workspace/sentry-javascript/node_modules/ts-node/src/index.ts","module":"ts-node.src:index.ts","function":"Module.m._compile","lineno":1618,"colno":23,"in_app":false},{"filename":"node:internal/modules/cjs/loader","module":"loader","function":"Module._compile","lineno":1364,"colno":14,"in_app":false},{"filename":"/Users/abhijeetprasad/workspace/sentry-javascript/dev-packages/node-integration-tests/suites/contextLines/memory-leak/scenario.ts","module":"scenario.ts","function":"Object.?","lineno":14,"colno":10,"in_app":true,"pre_context":[" dsn: 'https://public@dsn.ingest.sentry.io/1337',"," release: '1.0',"," transport: loggingTransport,","});","","import { runSentry } from './other-file';",""],"context_line":"runSentry();","post_context":["","console.log(execSync(`lsof -p ${process.pid}`, { stdio: 'inherit', cwd: process.cwd() }));"]},{"filename":"/Users/abhijeetprasad/workspace/sentry-javascript/dev-packages/node-integration-tests/suites/contextLines/memory-leak/other-file.ts","module":"other-file.ts","function":"runSentry","lineno":5,"colno":29,"in_app":true,"pre_context":["import * as Sentry from '@sentry/node';","","export function runSentry(): void {"," for (let i = 0; i < 10; i++) {"],"context_line":" Sentry.captureException(new Error(`error in loop ${i}`));","post_context":[" }","}"]}]},"mechanism":{"type":"generic","handled":true}}]},"event_id":"293d7c8c731c48eca30735b41efd40ba","level":"error","platform":"node","contexts":{"trace":{"trace_id":"efdb9350effb47959d48bd0aaf395824","span_id":"8be46494d3555ddb"},"runtime":{"name":"node","version":"v18.20.5"},"app":{"app_start_time":"2025-01-13T21:47:46.327Z","app_memory":270073856},"os":{"kernel_version":"23.6.0","name":"macOS","version":"14.7","build":"23H124"},"device":{"boot_time":"2024-12-23T16:56:50.637Z","arch":"arm64","memory_size":34359738368,"free_memory":355794944,"processor_count":10,"cpu_description":"Apple M1 Pro","processor_frequency":24},"culture":{"locale":"en-CA","timezone":"America/Toronto"},"cloud_resource":{}},"server_name":"GT9RQ02WW5.local","timestamp":1736804867.533,"environment":"production","release":"1.0","sdk":{"integrations":["InboundFilters","FunctionToString","LinkedErrors","RequestData","Console","Http","NodeFetch","OnUncaughtException","OnUnhandledRejection","ContextLines","LocalVariables","Context","ChildProcess","Modules"],"name":"sentry.javascript.node","version":"8.45.0","packages":[{"name":"npm:@sentry/node","version":"8.45.0"}]},"modules":{"ts-node":"10.9.1","make-error":"1.3.6","yn":"3.1.1","arg":"4.1.3","v8-compile-cache-lib":"3.0.1","typescript":"5.0.4","tslib":"2.7.0","semver":"7.6.3","shimmer":"1.2.1","require-in-the-middle":"7.2.0","resolve":"1.22.1","is-core-module":"2.11.0","has":"1.0.3","function-bind":"1.1.1","debug":"4.3.4","supports-color":"7.2.0","has-flag":"4.0.0","module-details-from-path":"1.0.3","import-in-the-middle":"1.12.0","forwarded-parse":"2.1.2"}}]]] + + at log (utils/runner.ts:462:11) + + console.log + line [{"event_id":"e9273b56624d4261b00f5431852da167","sent_at":"2025-01-13T21:47:47.666Z","sdk":{"name":"sentry.javascript.node","version":"8.45.0"},"trace":{"environment":"production","release":"1.0","public_key":"public","trace_id":"efdb9350effb47959d48bd0aaf395824"}},[[{"type":"event"},{"exception":{"values":[{"type":"Error","value":"error in loop 4","stacktrace":{"frames":[{"filename":"node:internal/main/run_main_module","module":"run_main_module","function":"?","lineno":28,"colno":49,"in_app":false},{"filename":"node:internal/modules/run_main","module":"run_main","function":"Function.executeUserEntryPoint [as runMain]","lineno":128,"colno":12,"in_app":false},{"filename":"node:internal/modules/cjs/loader","module":"loader","function":"Module._load","lineno":1019,"colno":12,"in_app":false},{"filename":"node:internal/modules/cjs/loader","module":"loader","function":"Module.load","lineno":1203,"colno":32,"in_app":false},{"filename":"/Users/abhijeetprasad/workspace/sentry-javascript/node_modules/ts-node/src/index.ts","module":"ts-node.src:index.ts","function":"Object.require.extensions. [as .ts]","lineno":1621,"colno":12,"in_app":false},{"filename":"node:internal/modules/cjs/loader","module":"loader","function":"Module._extensions..js","lineno":1422,"colno":10,"in_app":false},{"filename":"/Users/abhijeetprasad/workspace/sentry-javascript/node_modules/ts-node/src/index.ts","module":"ts-node.src:index.ts","function":"Module.m._compile","lineno":1618,"colno":23,"in_app":false},{"filename":"node:internal/modules/cjs/loader","module":"loader","function":"Module._compile","lineno":1364,"colno":14,"in_app":false},{"filename":"/Users/abhijeetprasad/workspace/sentry-javascript/dev-packages/node-integration-tests/suites/contextLines/memory-leak/scenario.ts","module":"scenario.ts","function":"Object.?","lineno":14,"colno":10,"in_app":true,"pre_context":[" dsn: 'https://public@dsn.ingest.sentry.io/1337',"," release: '1.0',"," transport: loggingTransport,","});","","import { runSentry } from './other-file';",""],"context_line":"runSentry();","post_context":["","console.log(execSync(`lsof -p ${process.pid}`, { stdio: 'inherit', cwd: process.cwd() }));"]},{"filename":"/Users/abhijeetprasad/workspace/sentry-javascript/dev-packages/node-integration-tests/suites/contextLines/memory-leak/other-file.ts","module":"other-file.ts","function":"runSentry","lineno":5,"colno":29,"in_app":true,"pre_context":["import * as Sentry from '@sentry/node';","","export function runSentry(): void {"," for (let i = 0; i < 10; i++) {"],"context_line":" Sentry.captureException(new Error(`error in loop ${i}`));","post_context":[" }","}"]}]},"mechanism":{"type":"generic","handled":true}}]},"event_id":"e9273b56624d4261b00f5431852da167","level":"error","platform":"node","contexts":{"trace":{"trace_id":"efdb9350effb47959d48bd0aaf395824","span_id":"9a067a8906c8c147"},"runtime":{"name":"node","version":"v18.20.5"},"app":{"app_start_time":"2025-01-13T21:47:46.327Z","app_memory":270073856},"os":{"kernel_version":"23.6.0","name":"macOS","version":"14.7","build":"23H124"},"device":{"boot_time":"2024-12-23T16:56:50.637Z","arch":"arm64","memory_size":34359738368,"free_memory":355794944,"processor_count":10,"cpu_description":"Apple M1 Pro","processor_frequency":24},"culture":{"locale":"en-CA","timezone":"America/Toronto"},"cloud_resource":{}},"server_name":"GT9RQ02WW5.local","timestamp":1736804867.533,"environment":"production","release":"1.0","sdk":{"integrations":["InboundFilters","FunctionToString","LinkedErrors","RequestData","Console","Http","NodeFetch","OnUncaughtException","OnUnhandledRejection","ContextLines","LocalVariables","Context","ChildProcess","Modules"],"name":"sentry.javascript.node","version":"8.45.0","packages":[{"name":"npm:@sentry/node","version":"8.45.0"}]},"modules":{"ts-node":"10.9.1","make-error":"1.3.6","yn":"3.1.1","arg":"4.1.3","v8-compile-cache-lib":"3.0.1","typescript":"5.0.4","tslib":"2.7.0","semver":"7.6.3","shimmer":"1.2.1","require-in-the-middle":"7.2.0","resolve":"1.22.1","is-core-module":"2.11.0","has":"1.0.3","function-bind":"1.1.1","debug":"4.3.4","supports-color":"7.2.0","has-flag":"4.0.0","module-details-from-path":"1.0.3","import-in-the-middle":"1.12.0","forwarded-parse":"2.1.2"}}]]] + + at log (utils/runner.ts:462:11) + + console.log + line [{"event_id":"cf92173285aa49b8bdb3fe31a5de6c90","sent_at":"2025-01-13T21:47:47.667Z","sdk":{"name":"sentry.javascript.node","version":"8.45.0"},"trace":{"environment":"production","release":"1.0","public_key":"public","trace_id":"efdb9350effb47959d48bd0aaf395824"}},[[{"type":"event"},{"exception":{"values":[{"type":"Error","value":"error in loop 5","stacktrace":{"frames":[{"filename":"node:internal/main/run_main_module","module":"run_main_module","function":"?","lineno":28,"colno":49,"in_app":false},{"filename":"node:internal/modules/run_main","module":"run_main","function":"Function.executeUserEntryPoint [as runMain]","lineno":128,"colno":12,"in_app":false},{"filename":"node:internal/modules/cjs/loader","module":"loader","function":"Module._load","lineno":1019,"colno":12,"in_app":false},{"filename":"node:internal/modules/cjs/loader","module":"loader","function":"Module.load","lineno":1203,"colno":32,"in_app":false},{"filename":"/Users/abhijeetprasad/workspace/sentry-javascript/node_modules/ts-node/src/index.ts","module":"ts-node.src:index.ts","function":"Object.require.extensions. [as .ts]","lineno":1621,"colno":12,"in_app":false},{"filename":"node:internal/modules/cjs/loader","module":"loader","function":"Module._extensions..js","lineno":1422,"colno":10,"in_app":false},{"filename":"/Users/abhijeetprasad/workspace/sentry-javascript/node_modules/ts-node/src/index.ts","module":"ts-node.src:index.ts","function":"Module.m._compile","lineno":1618,"colno":23,"in_app":false},{"filename":"node:internal/modules/cjs/loader","module":"loader","function":"Module._compile","lineno":1364,"colno":14,"in_app":false},{"filename":"/Users/abhijeetprasad/workspace/sentry-javascript/dev-packages/node-integration-tests/suites/contextLines/memory-leak/scenario.ts","module":"scenario.ts","function":"Object.?","lineno":14,"colno":10,"in_app":true,"pre_context":[" dsn: 'https://public@dsn.ingest.sentry.io/1337',"," release: '1.0',"," transport: loggingTransport,","});","","import { runSentry } from './other-file';",""],"context_line":"runSentry();","post_context":["","console.log(execSync(`lsof -p ${process.pid}`, { stdio: 'inherit', cwd: process.cwd() }));"]},{"filename":"/Users/abhijeetprasad/workspace/sentry-javascript/dev-packages/node-integration-tests/suites/contextLines/memory-leak/other-file.ts","module":"other-file.ts","function":"runSentry","lineno":5,"colno":29,"in_app":true,"pre_context":["import * as Sentry from '@sentry/node';","","export function runSentry(): void {"," for (let i = 0; i < 10; i++) {"],"context_line":" Sentry.captureException(new Error(`error in loop ${i}`));","post_context":[" }","}"]}]},"mechanism":{"type":"generic","handled":true}}]},"event_id":"cf92173285aa49b8bdb3fe31a5de6c90","level":"error","platform":"node","contexts":{"trace":{"trace_id":"efdb9350effb47959d48bd0aaf395824","span_id":"ac2ad9041812f9d9"},"runtime":{"name":"node","version":"v18.20.5"},"app":{"app_start_time":"2025-01-13T21:47:46.327Z","app_memory":270073856},"os":{"kernel_version":"23.6.0","name":"macOS","version":"14.7","build":"23H124"},"device":{"boot_time":"2024-12-23T16:56:50.637Z","arch":"arm64","memory_size":34359738368,"free_memory":355794944,"processor_count":10,"cpu_description":"Apple M1 Pro","processor_frequency":24},"culture":{"locale":"en-CA","timezone":"America/Toronto"},"cloud_resource":{}},"server_name":"GT9RQ02WW5.local","timestamp":1736804867.534,"environment":"production","release":"1.0","sdk":{"integrations":["InboundFilters","FunctionToString","LinkedErrors","RequestData","Console","Http","NodeFetch","OnUncaughtException","OnUnhandledRejection","ContextLines","LocalVariables","Context","ChildProcess","Modules"],"name":"sentry.javascript.node","version":"8.45.0","packages":[{"name":"npm:@sentry/node","version":"8.45.0"}]},"modules":{"ts-node":"10.9.1","make-error":"1.3.6","yn":"3.1.1","arg":"4.1.3","v8-compile-cache-lib":"3.0.1","typescript":"5.0.4","tslib":"2.7.0","semver":"7.6.3","shimmer":"1.2.1","require-in-the-middle":"7.2.0","resolve":"1.22.1","is-core-module":"2.11.0","has":"1.0.3","function-bind":"1.1.1","debug":"4.3.4","supports-color":"7.2.0","has-flag":"4.0.0","module-details-from-path":"1.0.3","import-in-the-middle":"1.12.0","forwarded-parse":"2.1.2"}}]]] + + at log (utils/runner.ts:462:11) + + console.log + line [{"event_id":"65224267e02049daadbc577de86960f3","sent_at":"2025-01-13T21:47:47.667Z","sdk":{"name":"sentry.javascript.node","version":"8.45.0"},"trace":{"environment":"production","release":"1.0","public_key":"public","trace_id":"efdb9350effb47959d48bd0aaf395824"}},[[{"type":"event"},{"exception":{"values":[{"type":"Error","value":"error in loop 6","stacktrace":{"frames":[{"filename":"node:internal/main/run_main_module","module":"run_main_module","function":"?","lineno":28,"colno":49,"in_app":false},{"filename":"node:internal/modules/run_main","module":"run_main","function":"Function.executeUserEntryPoint [as runMain]","lineno":128,"colno":12,"in_app":false},{"filename":"node:internal/modules/cjs/loader","module":"loader","function":"Module._load","lineno":1019,"colno":12,"in_app":false},{"filename":"node:internal/modules/cjs/loader","module":"loader","function":"Module.load","lineno":1203,"colno":32,"in_app":false},{"filename":"/Users/abhijeetprasad/workspace/sentry-javascript/node_modules/ts-node/src/index.ts","module":"ts-node.src:index.ts","function":"Object.require.extensions. [as .ts]","lineno":1621,"colno":12,"in_app":false},{"filename":"node:internal/modules/cjs/loader","module":"loader","function":"Module._extensions..js","lineno":1422,"colno":10,"in_app":false},{"filename":"/Users/abhijeetprasad/workspace/sentry-javascript/node_modules/ts-node/src/index.ts","module":"ts-node.src:index.ts","function":"Module.m._compile","lineno":1618,"colno":23,"in_app":false},{"filename":"node:internal/modules/cjs/loader","module":"loader","function":"Module._compile","lineno":1364,"colno":14,"in_app":false},{"filename":"/Users/abhijeetprasad/workspace/sentry-javascript/dev-packages/node-integration-tests/suites/contextLines/memory-leak/scenario.ts","module":"scenario.ts","function":"Object.?","lineno":14,"colno":10,"in_app":true,"pre_context":[" dsn: 'https://public@dsn.ingest.sentry.io/1337',"," release: '1.0',"," transport: loggingTransport,","});","","import { runSentry } from './other-file';",""],"context_line":"runSentry();","post_context":["","console.log(execSync(`lsof -p ${process.pid}`, { stdio: 'inherit', cwd: process.cwd() }));"]},{"filename":"/Users/abhijeetprasad/workspace/sentry-javascript/dev-packages/node-integration-tests/suites/contextLines/memory-leak/other-file.ts","module":"other-file.ts","function":"runSentry","lineno":5,"colno":29,"in_app":true,"pre_context":["import * as Sentry from '@sentry/node';","","export function runSentry(): void {"," for (let i = 0; i < 10; i++) {"],"context_line":" Sentry.captureException(new Error(`error in loop ${i}`));","post_context":[" }","}"]}]},"mechanism":{"type":"generic","handled":true}}]},"event_id":"65224267e02049daadbc577de86960f3","level":"error","platform":"node","contexts":{"trace":{"trace_id":"efdb9350effb47959d48bd0aaf395824","span_id":"b12818330e05cd2f"},"runtime":{"name":"node","version":"v18.20.5"},"app":{"app_start_time":"2025-01-13T21:47:46.327Z","app_memory":270073856},"os":{"kernel_version":"23.6.0","name":"macOS","version":"14.7","build":"23H124"},"device":{"boot_time":"2024-12-23T16:56:50.637Z","arch":"arm64","memory_size":34359738368,"free_memory":355794944,"processor_count":10,"cpu_description":"Apple M1 Pro","processor_frequency":24},"culture":{"locale":"en-CA","timezone":"America/Toronto"},"cloud_resource":{}},"server_name":"GT9RQ02WW5.local","timestamp":1736804867.535,"environment":"production","release":"1.0","sdk":{"integrations":["InboundFilters","FunctionToString","LinkedErrors","RequestData","Console","Http","NodeFetch","OnUncaughtException","OnUnhandledRejection","ContextLines","LocalVariables","Context","ChildProcess","Modules"],"name":"sentry.javascript.node","version":"8.45.0","packages":[{"name":"npm:@sentry/node","version":"8.45.0"}]},"modules":{"ts-node":"10.9.1","make-error":"1.3.6","yn":"3.1.1","arg":"4.1.3","v8-compile-cache-lib":"3.0.1","typescript":"5.0.4","tslib":"2.7.0","semver":"7.6.3","shimmer":"1.2.1","require-in-the-middle":"7.2.0","resolve":"1.22.1","is-core-module":"2.11.0","has":"1.0.3","function-bind":"1.1.1","debug":"4.3.4","supports-color":"7.2.0","has-flag":"4.0.0","module-details-from-path":"1.0.3","import-in-the-middle":"1.12.0","forwarded-parse":"2.1.2"}}]]] + + at log (utils/runner.ts:462:11) + + console.log + line [{"event_id":"b9e96b480e1a4e74a2ecebde9f0400a9","sent_at":"2025-01-13T21:47:47.668Z","sdk":{"name":"sentry.javascript.node","version":"8.45.0"},"trace":{"environment":"production","release":"1.0","public_key":"public","trace_id":"efdb9350effb47959d48bd0aaf395824"}},[[{"type":"event"},{"exception":{"values":[{"type":"Error","value":"error in loop 7","stacktrace":{"frames":[{"filename":"node:internal/main/run_main_module","module":"run_main_module","function":"?","lineno":28,"colno":49,"in_app":false},{"filename":"node:internal/modules/run_main","module":"run_main","function":"Function.executeUserEntryPoint [as runMain]","lineno":128,"colno":12,"in_app":false},{"filename":"node:internal/modules/cjs/loader","module":"loader","function":"Module._load","lineno":1019,"colno":12,"in_app":false},{"filename":"node:internal/modules/cjs/loader","module":"loader","function":"Module.load","lineno":1203,"colno":32,"in_app":false},{"filename":"/Users/abhijeetprasad/workspace/sentry-javascript/node_modules/ts-node/src/index.ts","module":"ts-node.src:index.ts","function":"Object.require.extensions. [as .ts]","lineno":1621,"colno":12,"in_app":false},{"filename":"node:internal/modules/cjs/loader","module":"loader","function":"Module._extensions..js","lineno":1422,"colno":10,"in_app":false},{"filename":"/Users/abhijeetprasad/workspace/sentry-javascript/node_modules/ts-node/src/index.ts","module":"ts-node.src:index.ts","function":"Module.m._compile","lineno":1618,"colno":23,"in_app":false},{"filename":"node:internal/modules/cjs/loader","module":"loader","function":"Module._compile","lineno":1364,"colno":14,"in_app":false},{"filename":"/Users/abhijeetprasad/workspace/sentry-javascript/dev-packages/node-integration-tests/suites/contextLines/memory-leak/scenario.ts","module":"scenario.ts","function":"Object.?","lineno":14,"colno":10,"in_app":true,"pre_context":[" dsn: 'https://public@dsn.ingest.sentry.io/1337',"," release: '1.0',"," transport: loggingTransport,","});","","import { runSentry } from './other-file';",""],"context_line":"runSentry();","post_context":["","console.log(execSync(`lsof -p ${process.pid}`, { stdio: 'inherit', cwd: process.cwd() }));"]},{"filename":"/Users/abhijeetprasad/workspace/sentry-javascript/dev-packages/node-integration-tests/suites/contextLines/memory-leak/other-file.ts","module":"other-file.ts","function":"runSentry","lineno":5,"colno":29,"in_app":true,"pre_context":["import * as Sentry from '@sentry/node';","","export function runSentry(): void {"," for (let i = 0; i < 10; i++) {"],"context_line":" Sentry.captureException(new Error(`error in loop ${i}`));","post_context":[" }","}"]}]},"mechanism":{"type":"generic","handled":true}}]},"event_id":"b9e96b480e1a4e74a2ecebde9f0400a9","level":"error","platform":"node","contexts":{"trace":{"trace_id":"efdb9350effb47959d48bd0aaf395824","span_id":"83cb86896d96bbf6"},"runtime":{"name":"node","version":"v18.20.5"},"app":{"app_start_time":"2025-01-13T21:47:46.327Z","app_memory":270073856},"os":{"kernel_version":"23.6.0","name":"macOS","version":"14.7","build":"23H124"},"device":{"boot_time":"2024-12-23T16:56:50.637Z","arch":"arm64","memory_size":34359738368,"free_memory":355794944,"processor_count":10,"cpu_description":"Apple M1 Pro","processor_frequency":24},"culture":{"locale":"en-CA","timezone":"America/Toronto"},"cloud_resource":{}},"server_name":"GT9RQ02WW5.local","timestamp":1736804867.536,"environment":"production","release":"1.0","sdk":{"integrations":["InboundFilters","FunctionToString","LinkedErrors","RequestData","Console","Http","NodeFetch","OnUncaughtException","OnUnhandledRejection","ContextLines","LocalVariables","Context","ChildProcess","Modules"],"name":"sentry.javascript.node","version":"8.45.0","packages":[{"name":"npm:@sentry/node","version":"8.45.0"}]},"modules":{"ts-node":"10.9.1","make-error":"1.3.6","yn":"3.1.1","arg":"4.1.3","v8-compile-cache-lib":"3.0.1","typescript":"5.0.4","tslib":"2.7.0","semver":"7.6.3","shimmer":"1.2.1","require-in-the-middle":"7.2.0","resolve":"1.22.1","is-core-module":"2.11.0","has":"1.0.3","function-bind":"1.1.1","debug":"4.3.4","supports-color":"7.2.0","has-flag":"4.0.0","module-details-from-path":"1.0.3","import-in-the-middle":"1.12.0","forwarded-parse":"2.1.2"}}]]] + + at log (utils/runner.ts:462:11) + + console.log + line [{"event_id":"c541f2c0a31345b78f93f69ffe5e0fc6","sent_at":"2025-01-13T21:47:47.668Z","sdk":{"name":"sentry.javascript.node","version":"8.45.0"},"trace":{"environment":"production","release":"1.0","public_key":"public","trace_id":"efdb9350effb47959d48bd0aaf395824"}},[[{"type":"event"},{"exception":{"values":[{"type":"Error","value":"error in loop 8","stacktrace":{"frames":[{"filename":"node:internal/main/run_main_module","module":"run_main_module","function":"?","lineno":28,"colno":49,"in_app":false},{"filename":"node:internal/modules/run_main","module":"run_main","function":"Function.executeUserEntryPoint [as runMain]","lineno":128,"colno":12,"in_app":false},{"filename":"node:internal/modules/cjs/loader","module":"loader","function":"Module._load","lineno":1019,"colno":12,"in_app":false},{"filename":"node:internal/modules/cjs/loader","module":"loader","function":"Module.load","lineno":1203,"colno":32,"in_app":false},{"filename":"/Users/abhijeetprasad/workspace/sentry-javascript/node_modules/ts-node/src/index.ts","module":"ts-node.src:index.ts","function":"Object.require.extensions. [as .ts]","lineno":1621,"colno":12,"in_app":false},{"filename":"node:internal/modules/cjs/loader","module":"loader","function":"Module._extensions..js","lineno":1422,"colno":10,"in_app":false},{"filename":"/Users/abhijeetprasad/workspace/sentry-javascript/node_modules/ts-node/src/index.ts","module":"ts-node.src:index.ts","function":"Module.m._compile","lineno":1618,"colno":23,"in_app":false},{"filename":"node:internal/modules/cjs/loader","module":"loader","function":"Module._compile","lineno":1364,"colno":14,"in_app":false},{"filename":"/Users/abhijeetprasad/workspace/sentry-javascript/dev-packages/node-integration-tests/suites/contextLines/memory-leak/scenario.ts","module":"scenario.ts","function":"Object.?","lineno":14,"colno":10,"in_app":true,"pre_context":[" dsn: 'https://public@dsn.ingest.sentry.io/1337',"," release: '1.0',"," transport: loggingTransport,","});","","import { runSentry } from './other-file';",""],"context_line":"runSentry();","post_context":["","console.log(execSync(`lsof -p ${process.pid}`, { stdio: 'inherit', cwd: process.cwd() }));"]},{"filename":"/Users/abhijeetprasad/workspace/sentry-javascript/dev-packages/node-integration-tests/suites/contextLines/memory-leak/other-file.ts","module":"other-file.ts","function":"runSentry","lineno":5,"colno":29,"in_app":true,"pre_context":["import * as Sentry from '@sentry/node';","","export function runSentry(): void {"," for (let i = 0; i < 10; i++) {"],"context_line":" Sentry.captureException(new Error(`error in loop ${i}`));","post_context":[" }","}"]}]},"mechanism":{"type":"generic","handled":true}}]},"event_id":"c541f2c0a31345b78f93f69ffe5e0fc6","level":"error","platform":"node","contexts":{"trace":{"trace_id":"efdb9350effb47959d48bd0aaf395824","span_id":"a0e8e199fcf05714"},"runtime":{"name":"node","version":"v18.20.5"},"app":{"app_start_time":"2025-01-13T21:47:46.327Z","app_memory":270073856},"os":{"kernel_version":"23.6.0","name":"macOS","version":"14.7","build":"23H124"},"device":{"boot_time":"2024-12-23T16:56:50.637Z","arch":"arm64","memory_size":34359738368,"free_memory":355794944,"processor_count":10,"cpu_description":"Apple M1 Pro","processor_frequency":24},"culture":{"locale":"en-CA","timezone":"America/Toronto"},"cloud_resource":{}},"server_name":"GT9RQ02WW5.local","timestamp":1736804867.536,"environment":"production","release":"1.0","sdk":{"integrations":["InboundFilters","FunctionToString","LinkedErrors","RequestData","Console","Http","NodeFetch","OnUncaughtException","OnUnhandledRejection","ContextLines","LocalVariables","Context","ChildProcess","Modules"],"name":"sentry.javascript.node","version":"8.45.0","packages":[{"name":"npm:@sentry/node","version":"8.45.0"}]},"modules":{"ts-node":"10.9.1","make-error":"1.3.6","yn":"3.1.1","arg":"4.1.3","v8-compile-cache-lib":"3.0.1","typescript":"5.0.4","tslib":"2.7.0","semver":"7.6.3","shimmer":"1.2.1","require-in-the-middle":"7.2.0","resolve":"1.22.1","is-core-module":"2.11.0","has":"1.0.3","function-bind":"1.1.1","debug":"4.3.4","supports-color":"7.2.0","has-flag":"4.0.0","module-details-from-path":"1.0.3","import-in-the-middle":"1.12.0","forwarded-parse":"2.1.2"}}]]] + + at log (utils/runner.ts:462:11) + + console.log + line [{"event_id":"dc08b3fe26e94759817c7b5e95469727","sent_at":"2025-01-13T21:47:47.669Z","sdk":{"name":"sentry.javascript.node","version":"8.45.0"},"trace":{"environment":"production","release":"1.0","public_key":"public","trace_id":"efdb9350effb47959d48bd0aaf395824"}},[[{"type":"event"},{"exception":{"values":[{"type":"Error","value":"error in loop 9","stacktrace":{"frames":[{"filename":"node:internal/main/run_main_module","module":"run_main_module","function":"?","lineno":28,"colno":49,"in_app":false},{"filename":"node:internal/modules/run_main","module":"run_main","function":"Function.executeUserEntryPoint [as runMain]","lineno":128,"colno":12,"in_app":false},{"filename":"node:internal/modules/cjs/loader","module":"loader","function":"Module._load","lineno":1019,"colno":12,"in_app":false},{"filename":"node:internal/modules/cjs/loader","module":"loader","function":"Module.load","lineno":1203,"colno":32,"in_app":false},{"filename":"/Users/abhijeetprasad/workspace/sentry-javascript/node_modules/ts-node/src/index.ts","module":"ts-node.src:index.ts","function":"Object.require.extensions. [as .ts]","lineno":1621,"colno":12,"in_app":false},{"filename":"node:internal/modules/cjs/loader","module":"loader","function":"Module._extensions..js","lineno":1422,"colno":10,"in_app":false},{"filename":"/Users/abhijeetprasad/workspace/sentry-javascript/node_modules/ts-node/src/index.ts","module":"ts-node.src:index.ts","function":"Module.m._compile","lineno":1618,"colno":23,"in_app":false},{"filename":"node:internal/modules/cjs/loader","module":"loader","function":"Module._compile","lineno":1364,"colno":14,"in_app":false},{"filename":"/Users/abhijeetprasad/workspace/sentry-javascript/dev-packages/node-integration-tests/suites/contextLines/memory-leak/scenario.ts","module":"scenario.ts","function":"Object.?","lineno":14,"colno":10,"in_app":true,"pre_context":[" dsn: 'https://public@dsn.ingest.sentry.io/1337',"," release: '1.0',"," transport: loggingTransport,","});","","import { runSentry } from './other-file';",""],"context_line":"runSentry();","post_context":["","console.log(execSync(`lsof -p ${process.pid}`, { stdio: 'inherit', cwd: process.cwd() }));"]},{"filename":"/Users/abhijeetprasad/workspace/sentry-javascript/dev-packages/node-integration-tests/suites/contextLines/memory-leak/other-file.ts","module":"other-file.ts","function":"runSentry","lineno":5,"colno":29,"in_app":true,"pre_context":["import * as Sentry from '@sentry/node';","","export function runSentry(): void {"," for (let i = 0; i < 10; i++) {"],"context_line":" Sentry.captureException(new Error(`error in loop ${i}`));","post_context":[" }","}"]}]},"mechanism":{"type":"generic","handled":true}}]},"event_id":"dc08b3fe26e94759817c7b5e95469727","level":"error","platform":"node","contexts":{"trace":{"trace_id":"efdb9350effb47959d48bd0aaf395824","span_id":"8ec7d145c5362df0"},"runtime":{"name":"node","version":"v18.20.5"},"app":{"app_start_time":"2025-01-13T21:47:46.327Z","app_memory":270106624},"os":{"kernel_version":"23.6.0","name":"macOS","version":"14.7","build":"23H124"},"device":{"boot_time":"2024-12-23T16:56:50.637Z","arch":"arm64","memory_size":34359738368,"free_memory":355794944,"processor_count":10,"cpu_description":"Apple M1 Pro","processor_frequency":24},"culture":{"locale":"en-CA","timezone":"America/Toronto"},"cloud_resource":{}},"server_name":"GT9RQ02WW5.local","timestamp":1736804867.537,"environment":"production","release":"1.0","sdk":{"integrations":["InboundFilters","FunctionToString","LinkedErrors","RequestData","Console","Http","NodeFetch","OnUncaughtException","OnUnhandledRejection","ContextLines","LocalVariables","Context","ChildProcess","Modules"],"name":"sentry.javascript.node","version":"8.45.0","packages":[{"name":"npm:@sentry/node","version":"8.45.0"}]},"modules":{"ts-node":"10.9.1","make-error":"1.3.6","yn":"3.1.1","arg":"4.1.3","v8-compile-cache-lib":"3.0.1","typescript":"5.0.4","tslib":"2.7.0","semver":"7.6.3","shimmer":"1.2.1","require-in-the-middle":"7.2.0","resolve":"1.22.1","is-core-module":"2.11.0","has":"1.0.3","function-bind":"1.1.1","debug":"4.3.4","supports-color":"7.2.0","has-flag":"4.0.0","module-details-from-path":"1.0.3","import-in-the-middle":"1.12.0","forwarded-parse":"2.1.2"}}]]] + + at log (utils/runner.ts:462:11) + +Done in 4.21s. diff --git a/dev-packages/node-core-integration-tests/tsconfig.json b/dev-packages/node-core-integration-tests/tsconfig.json new file mode 100644 index 000000000000..1cd6c0aec734 --- /dev/null +++ b/dev-packages/node-core-integration-tests/tsconfig.json @@ -0,0 +1,14 @@ +{ + "extends": "../../tsconfig.json", + + "include": ["utils/**/*.ts", "src/**/*.ts"], + + "compilerOptions": { + // Although this seems wrong to include `DOM` here, it's necessary to make + // global fetch available in tests in lower Node versions. + "lib": ["DOM", "ES2018"], + // package-specific options + "esModuleInterop": true, + "types": ["node"] + } +} diff --git a/dev-packages/node-core-integration-tests/tsconfig.test.json b/dev-packages/node-core-integration-tests/tsconfig.test.json new file mode 100644 index 000000000000..45a6e39b0054 --- /dev/null +++ b/dev-packages/node-core-integration-tests/tsconfig.test.json @@ -0,0 +1,15 @@ +{ + "extends": "./tsconfig.json", + + "include": ["suites/**/*.ts", "vite.config.ts"], + + "compilerOptions": { + // Although this seems wrong to include `DOM` here, it's necessary to make + // global fetch available in tests in lower Node versions. + "lib": ["DOM", "ES2018"], + // should include all types from `./tsconfig.json` plus types for all test frameworks used + "types": ["node"] + + // other package-specific, test-specific options + } +} diff --git a/dev-packages/node-core-integration-tests/tsconfig.types.json b/dev-packages/node-core-integration-tests/tsconfig.types.json new file mode 100644 index 000000000000..65455f66bd75 --- /dev/null +++ b/dev-packages/node-core-integration-tests/tsconfig.types.json @@ -0,0 +1,10 @@ +{ + "extends": "./tsconfig.json", + + "compilerOptions": { + "declaration": true, + "declarationMap": true, + "emitDeclarationOnly": true, + "outDir": "build/types" + } +} diff --git a/dev-packages/node-core-integration-tests/utils/assertions.ts b/dev-packages/node-core-integration-tests/utils/assertions.ts new file mode 100644 index 000000000000..296bdc608bb4 --- /dev/null +++ b/dev-packages/node-core-integration-tests/utils/assertions.ts @@ -0,0 +1,89 @@ +import type { + ClientReport, + Envelope, + Event, + SerializedCheckIn, + SerializedLogContainer, + SerializedSession, + SessionAggregates, + TransactionEvent, +} from '@sentry/core'; +import { SDK_VERSION } from '@sentry/core'; +import { expect } from 'vitest'; + +/** + * Asserts against a Sentry Event ignoring non-deterministic properties + * + * @param {Record} actual + * @param {Record} expected + */ +export const assertSentryEvent = (actual: Event, expected: Record): void => { + expect(actual).toMatchObject({ + event_id: expect.any(String), + ...expected, + }); +}; + +/** + * Asserts against a Sentry Transaction ignoring non-deterministic properties + * + * @param {Record} actual + * @param {Record} expected + */ +export const assertSentryTransaction = (actual: TransactionEvent, expected: Record): void => { + expect(actual).toMatchObject({ + event_id: expect.any(String), + timestamp: expect.anything(), + start_timestamp: expect.anything(), + spans: expect.any(Array), + type: 'transaction', + ...expected, + }); +}; + +export function assertSentrySession(actual: SerializedSession, expected: Partial): void { + expect(actual).toMatchObject({ + sid: expect.any(String), + ...expected, + }); +} + +export function assertSentrySessions(actual: SessionAggregates, expected: Partial): void { + expect(actual).toMatchObject({ + ...expected, + }); +} + +export function assertSentryCheckIn(actual: SerializedCheckIn, expected: Partial): void { + expect(actual).toMatchObject({ + check_in_id: expect.any(String), + ...expected, + }); +} + +export function assertSentryClientReport(actual: ClientReport, expected: Partial): void { + expect(actual).toMatchObject({ + ...expected, + }); +} + +export function assertSentryLogContainer( + actual: SerializedLogContainer, + expected: Partial, +): void { + expect(actual).toMatchObject({ + ...expected, + }); +} + +export function assertEnvelopeHeader(actual: Envelope[0], expected: Partial): void { + expect(actual).toEqual({ + event_id: expect.any(String), + sent_at: expect.any(String), + sdk: { + name: 'sentry.javascript.node', + version: SDK_VERSION, + }, + ...expected, + }); +} diff --git a/dev-packages/node-core-integration-tests/utils/index.ts b/dev-packages/node-core-integration-tests/utils/index.ts new file mode 100644 index 000000000000..e08d89a92131 --- /dev/null +++ b/dev-packages/node-core-integration-tests/utils/index.ts @@ -0,0 +1,57 @@ +import type { EnvelopeItemType } from '@sentry/core'; +import { parseSemver } from '@sentry/core'; +import type * as http from 'http'; +import { describe } from 'vitest'; + +const NODE_VERSION = parseSemver(process.versions.node).major; + +export type TestServerConfig = { + url: string; + server: http.Server; +}; + +export type DataCollectorOptions = { + // Optional custom URL + url?: string; + + // The expected amount of requests to the envelope endpoint. + // If the amount of sent requests is lower than `count`, this function will not resolve. + count?: number; + + // The method of the request. + method?: 'get' | 'post'; + + // Whether to stop the server after the requests have been intercepted + endServer?: boolean; + + // Type(s) of the envelopes to capture + envelopeType?: EnvelopeItemType | EnvelopeItemType[]; +}; + +/** + * Returns`describe` or `describe.skip` depending on allowed major versions of Node. + * + * @param {{ min?: number; max?: number }} allowedVersion + */ +export function conditionalTest(allowedVersion: { + min?: number; + max?: number; +}): typeof describe | typeof describe.skip { + if (!NODE_VERSION) { + return describe.skip; + } + + return NODE_VERSION < (allowedVersion.min || -Infinity) || NODE_VERSION > (allowedVersion.max || Infinity) + ? describe.skip + : describe; +} + +/** + * Parses response body containing an Envelope + * + * @param {string} body + * @return {*} {Array>} + */ +export const parseEnvelope = (body: string): Array> => { + return body.split('\n').map(e => JSON.parse(e)); +}; diff --git a/dev-packages/node-core-integration-tests/utils/runner.ts b/dev-packages/node-core-integration-tests/utils/runner.ts new file mode 100644 index 000000000000..97b1efa2dbb4 --- /dev/null +++ b/dev-packages/node-core-integration-tests/utils/runner.ts @@ -0,0 +1,683 @@ +/* eslint-disable max-lines */ +import type { + ClientReport, + Envelope, + EnvelopeItemType, + Event, + EventEnvelope, + SerializedCheckIn, + SerializedLogContainer, + SerializedSession, + SessionAggregates, + TransactionEvent, +} from '@sentry/core'; +import { normalize } from '@sentry/core'; +import { execSync, spawn, spawnSync } from 'child_process'; +import { existsSync, readFileSync, unlinkSync, writeFileSync } from 'fs'; +import { join } from 'path'; +import { afterAll, beforeAll, describe, test } from 'vitest'; +import { + assertEnvelopeHeader, + assertSentryCheckIn, + assertSentryClientReport, + assertSentryEvent, + assertSentryLogContainer, + assertSentrySession, + assertSentrySessions, + assertSentryTransaction, +} from './assertions'; +import { createBasicSentryServer } from './server'; + +const CLEANUP_STEPS = new Set(); + +export function cleanupChildProcesses(): void { + for (const step of CLEANUP_STEPS) { + step(); + } + CLEANUP_STEPS.clear(); +} + +process.on('exit', cleanupChildProcesses); + +/** Promise only resolves when fn returns true */ +async function waitFor(fn: () => boolean, timeout = 10_000, message = 'Timed out waiting'): Promise { + let remaining = timeout; + while (fn() === false) { + await new Promise(resolve => setTimeout(resolve, 100)); + remaining -= 100; + if (remaining < 0) { + throw new Error(message); + } + } +} + +type VoidFunction = () => void; + +interface DockerOptions { + /** + * The working directory to run docker compose in + */ + workingDirectory: string[]; + /** + * The strings to look for in the output to know that the docker compose is ready for the test to be run + */ + readyMatches: string[]; + /** + * The command to run after docker compose is up + */ + setupCommand?: string; +} + +/** + * Runs docker compose up and waits for the readyMatches to appear in the output + * + * Returns a function that can be called to docker compose down + */ +async function runDockerCompose(options: DockerOptions): Promise { + return new Promise((resolve, reject) => { + const cwd = join(...options.workingDirectory); + const close = (): void => { + spawnSync('docker', ['compose', 'down', '--volumes'], { + cwd, + stdio: process.env.DEBUG ? 'inherit' : undefined, + }); + }; + + // ensure we're starting fresh + close(); + + const child = spawn('docker', ['compose', 'up'], { cwd }); + + const timeout = setTimeout(() => { + close(); + reject(new Error('Timed out waiting for docker-compose')); + }, 75_000); + + function newData(data: Buffer): void { + const text = data.toString('utf8'); + + if (process.env.DEBUG) log(text); + + for (const match of options.readyMatches) { + if (text.includes(match)) { + child.stdout.removeAllListeners(); + clearTimeout(timeout); + if (options.setupCommand) { + execSync(options.setupCommand, { cwd, stdio: 'inherit' }); + } + resolve(close); + } + } + } + + child.stdout.on('data', newData); + child.stderr.on('data', newData); + }); +} + +type ExpectedEvent = Partial | ((event: Event) => void); +type ExpectedTransaction = Partial | ((event: TransactionEvent) => void); +type ExpectedSession = Partial | ((event: SerializedSession) => void); +type ExpectedSessions = Partial | ((event: SessionAggregates) => void); +type ExpectedCheckIn = Partial | ((event: SerializedCheckIn) => void); +type ExpectedClientReport = Partial | ((event: ClientReport) => void); +type ExpectedLogContainer = Partial | ((event: SerializedLogContainer) => void); + +type Expected = + | { + event: ExpectedEvent; + } + | { + transaction: ExpectedTransaction; + } + | { + session: ExpectedSession; + } + | { + sessions: ExpectedSessions; + } + | { + check_in: ExpectedCheckIn; + } + | { + client_report: ExpectedClientReport; + } + | { + log: ExpectedLogContainer; + }; + +type ExpectedEnvelopeHeader = + | { event: Partial } + | { transaction: Partial } + | { session: Partial } + | { sessions: Partial } + | { log: Partial }; + +type StartResult = { + completed(): Promise; + childHasExited(): boolean; + getLogs(): string[]; + makeRequest( + method: 'get' | 'post', + path: string, + options?: { headers?: Record; data?: BodyInit; expectError?: boolean }, + ): Promise; +}; + +export function createEsmAndCjsTests( + cwd: string, + scenarioPath: string, + instrumentPath: string, + callback: ( + createTestRunner: () => ReturnType, + testFn: typeof test | typeof test.fails, + mode: 'esm' | 'cjs', + ) => void, + options?: { failsOnCjs?: boolean; failsOnEsm?: boolean }, +): void { + const mjsScenarioPath = join(cwd, scenarioPath); + const mjsInstrumentPath = join(cwd, instrumentPath); + + if (!mjsScenarioPath.endsWith('.mjs')) { + throw new Error(`Scenario path must end with .mjs: ${scenarioPath}`); + } + + if (!existsSync(mjsInstrumentPath)) { + throw new Error(`Instrument file not found: ${mjsInstrumentPath}`); + } + + const cjsScenarioPath = join(cwd, `tmp_${scenarioPath.replace('.mjs', '.cjs')}`); + const cjsInstrumentPath = join(cwd, `tmp_${instrumentPath.replace('.mjs', '.cjs')}`); + + describe('esm', () => { + const testFn = options?.failsOnEsm ? test.fails : test; + callback(() => createRunner(mjsScenarioPath).withFlags('--import', mjsInstrumentPath), testFn, 'esm'); + }); + + describe('cjs', () => { + beforeAll(() => { + // For the CJS runner, we create some temporary files... + convertEsmFileToCjs(mjsScenarioPath, cjsScenarioPath); + convertEsmFileToCjs(mjsInstrumentPath, cjsInstrumentPath); + }); + + afterAll(() => { + try { + unlinkSync(cjsInstrumentPath); + } catch { + // Ignore errors here + } + try { + unlinkSync(cjsScenarioPath); + } catch { + // Ignore errors here + } + }); + + const testFn = options?.failsOnCjs ? test.fails : test; + callback(() => createRunner(cjsScenarioPath).withFlags('--require', cjsInstrumentPath), testFn, 'cjs'); + }); +} + +function convertEsmFileToCjs(inputPath: string, outputPath: string): void { + const cjsFileContent = readFileSync(inputPath, 'utf8'); + const cjsFileContentConverted = convertEsmToCjs(cjsFileContent); + writeFileSync(outputPath, cjsFileContentConverted); +} + +/** Creates a test runner */ +// eslint-disable-next-line @typescript-eslint/explicit-function-return-type +export function createRunner(...paths: string[]) { + const testPath = join(...paths); + + if (!existsSync(testPath)) { + throw new Error(`Test scenario not found: ${testPath}`); + } + + const expectedEnvelopes: Expected[] = []; + let expectedEnvelopeHeaders: ExpectedEnvelopeHeader[] | undefined = undefined; + const flags: string[] = []; + // By default, we ignore session & sessions + const ignored: Set = new Set(['session', 'sessions', 'client_report']); + let withEnv: Record = {}; + let withSentryServer = false; + let dockerOptions: DockerOptions | undefined; + let ensureNoErrorOutput = false; + const logs: string[] = []; + + if (testPath.endsWith('.ts')) { + flags.push('-r', 'ts-node/register'); + } + + return { + expect: function (expected: Expected) { + if (ensureNoErrorOutput) { + throw new Error('You should not use `ensureNoErrorOutput` when using `expect`!'); + } + expectedEnvelopes.push(expected); + return this; + }, + expectN: function (n: number, expected: Expected) { + for (let i = 0; i < n; i++) { + expectedEnvelopes.push(expected); + } + return this; + }, + expectHeader: function (expected: ExpectedEnvelopeHeader) { + if (!expectedEnvelopeHeaders) { + expectedEnvelopeHeaders = []; + } + + expectedEnvelopeHeaders.push(expected); + return this; + }, + withEnv: function (env: Record) { + withEnv = env; + return this; + }, + withFlags: function (...args: string[]) { + flags.push(...args); + return this; + }, + withInstrument: function (instrumentPath: string) { + flags.push('--import', instrumentPath); + return this; + }, + withMockSentryServer: function () { + withSentryServer = true; + return this; + }, + ignore: function (...types: EnvelopeItemType[]) { + types.forEach(t => ignored.add(t)); + return this; + }, + unignore: function (...types: EnvelopeItemType[]) { + for (const t of types) { + ignored.delete(t); + } + return this; + }, + withDockerCompose: function (options: DockerOptions) { + dockerOptions = options; + return this; + }, + ensureNoErrorOutput: function () { + if (expectedEnvelopes.length > 0) { + throw new Error('You should not use `ensureNoErrorOutput` when using `expect`!'); + } + ensureNoErrorOutput = true; + return this; + }, + start: function (): StartResult { + let isComplete = false; + let completeError: Error | undefined; + + const expectedEnvelopeCount = Math.max(expectedEnvelopes.length, (expectedEnvelopeHeaders || []).length); + + let envelopeCount = 0; + let scenarioServerPort: number | undefined; + let hasExited = false; + let child: ReturnType | undefined; + + function complete(error?: Error): void { + if (isComplete) { + return; + } + + isComplete = true; + completeError = error || undefined; + child?.kill(); + } + + /** Called after each expect callback to check if we're complete */ + function expectCallbackCalled(): void { + envelopeCount++; + if (envelopeCount === expectedEnvelopeCount) { + complete(); + } + } + + function newEnvelope(envelope: Envelope): void { + for (const item of envelope[1]) { + const envelopeItemType = item[0].type; + + if (ignored.has(envelopeItemType)) { + continue; + } + + if (expectedEnvelopeHeaders) { + const header = envelope[0]; + const expected = expectedEnvelopeHeaders.shift()?.[envelopeItemType as keyof ExpectedEnvelopeHeader]; + + try { + if (!expected) { + return; + } + + assertEnvelopeHeader(header, expected); + + expectCallbackCalled(); + } catch (e) { + complete(e as Error); + } + + return; + } + + const expected = expectedEnvelopes.shift(); + + // Catch any error or failed assertions and pass them to done to end the test quickly + try { + if (!expected) { + return; + } + + const expectedType = Object.keys(expected)[0]; + + if (expectedType !== envelopeItemType) { + throw new Error( + `Expected envelope item type '${expectedType}' but got '${envelopeItemType}'. \nItem: ${JSON.stringify( + item, + )}`, + ); + } + + if ('event' in expected) { + expectErrorEvent(item[1] as Event, expected.event); + expectCallbackCalled(); + } else if ('transaction' in expected) { + expectTransactionEvent(item[1] as TransactionEvent, expected.transaction); + expectCallbackCalled(); + } else if ('session' in expected) { + expectSessionEvent(item[1] as SerializedSession, expected.session); + expectCallbackCalled(); + } else if ('sessions' in expected) { + expectSessionsEvent(item[1] as SessionAggregates, expected.sessions); + expectCallbackCalled(); + } else if ('check_in' in expected) { + expectCheckInEvent(item[1] as SerializedCheckIn, expected.check_in); + expectCallbackCalled(); + } else if ('client_report' in expected) { + expectClientReport(item[1] as ClientReport, expected.client_report); + expectCallbackCalled(); + } else if ('log' in expected) { + expectLog(item[1] as SerializedLogContainer, expected.log); + expectCallbackCalled(); + } else { + throw new Error( + `Unhandled expected envelope item type: ${JSON.stringify(expected)}\nItem: ${JSON.stringify(item)}`, + ); + } + } catch (e) { + complete(e as Error); + } + } + } + + // We need to properly define & pass these types around for TS 3.8, + // which otherwise fails to infer these correctly :( + type ServerStartup = [number | undefined, (() => void) | undefined]; + type DockerStartup = VoidFunction | undefined; + + const serverStartup: Promise = withSentryServer + ? createBasicSentryServer(newEnvelope) + : Promise.resolve([undefined, undefined]); + + const dockerStartup: Promise = dockerOptions + ? runDockerCompose(dockerOptions) + : Promise.resolve(undefined); + + const startup = Promise.all([dockerStartup, serverStartup]) as Promise<[DockerStartup, ServerStartup]>; + + startup + .then(([dockerChild, [mockServerPort, mockServerClose]]) => { + if (mockServerClose) { + CLEANUP_STEPS.add(() => { + mockServerClose(); + }); + } + + if (dockerChild) { + CLEANUP_STEPS.add(dockerChild); + } + + const env = mockServerPort + ? { ...process.env, ...withEnv, SENTRY_DSN: `http://public@localhost:${mockServerPort}/1337` } + : { ...process.env, ...withEnv }; + + if (process.env.DEBUG) log('starting scenario', testPath, flags, env.SENTRY_DSN); + + child = spawn('node', [...flags, testPath], { env }); + + CLEANUP_STEPS.add(() => { + child?.kill(); + }); + + child.stderr?.on('data', (data: Buffer) => { + const output = data.toString(); + logs.push(output.trim()); + + if (process.env.DEBUG) log('stderr line', output); + + if (ensureNoErrorOutput) { + complete(new Error(`Expected no error output but got: '${output}'`)); + } + }); + + child.on('close', () => { + hasExited = true; + + if (ensureNoErrorOutput) { + complete(); + } + }); + + // Pass error to done to end the test quickly + child.on('error', e => { + if (process.env.DEBUG) log('scenario error', e); + complete(e); + }); + + function tryParseEnvelopeFromStdoutLine(line: string): void { + // Lines can have leading '[something] [{' which we need to remove + const cleanedLine = line.replace(/^.*?] \[{"/, '[{"'); + + // See if we have a port message + if (cleanedLine.startsWith('{"port":')) { + const { port } = JSON.parse(cleanedLine) as { port: number }; + scenarioServerPort = port; + return; + } + + // Skip any lines that don't start with envelope JSON + if (!cleanedLine.startsWith('[{')) { + return; + } + + try { + const envelope = JSON.parse(cleanedLine) as Envelope; + newEnvelope(envelope); + } catch (_) { + // + } + } + + let buffer = Buffer.alloc(0); + child.stdout?.on('data', (data: Buffer) => { + // This is horribly memory inefficient but it's only for tests + buffer = Buffer.concat([buffer, data]); + + let splitIndex = -1; + while ((splitIndex = buffer.indexOf(0xa)) >= 0) { + const line = buffer.subarray(0, splitIndex).toString(); + logs.push(line.trim()); + + buffer = Buffer.from(buffer.subarray(splitIndex + 1)); + if (process.env.DEBUG) log('line', line); + tryParseEnvelopeFromStdoutLine(line); + } + }); + }) + .catch(e => complete(e)); + + return { + completed: async function (): Promise { + await waitFor(() => isComplete, 120_000, 'Timed out waiting for test to complete'); + + if (completeError) { + throw completeError; + } + }, + childHasExited: function (): boolean { + return hasExited; + }, + getLogs(): string[] { + return logs; + }, + makeRequest: async function ( + method: 'get' | 'post', + path: string, + options: { headers?: Record; data?: BodyInit; expectError?: boolean } = {}, + ): Promise { + try { + await waitFor(() => scenarioServerPort !== undefined, 10_000, 'Timed out waiting for server port'); + } catch (e) { + complete(e as Error); + return; + } + + const url = `http://localhost:${scenarioServerPort}${path}`; + const body = options.data; + const headers = options.headers || {}; + const expectError = options.expectError || false; + + if (process.env.DEBUG) log('making request', method, url, headers, body); + + try { + const res = await fetch(url, { headers, method, body }); + + if (!res.ok) { + if (!expectError) { + complete(new Error(`Expected request to "${path}" to succeed, but got a ${res.status} response`)); + } + + return; + } + + if (expectError) { + complete(new Error(`Expected request to "${path}" to fail, but got a ${res.status} response`)); + return; + } + + if (res.headers.get('content-type')?.includes('application/json')) { + return await res.json(); + } + + return (await res.text()) as T; + } catch (e) { + if (expectError) { + return; + } + + complete(e as Error); + return; + } + }, + }; + }, + }; +} + +function log(...args: unknown[]): void { + // eslint-disable-next-line no-console + console.log(...args.map(arg => normalize(arg))); +} + +function expectErrorEvent(item: Event, expected: ExpectedEvent): void { + if (typeof expected === 'function') { + expected(item); + } else { + assertSentryEvent(item, expected); + } +} + +function expectTransactionEvent(item: TransactionEvent, expected: ExpectedTransaction): void { + if (typeof expected === 'function') { + expected(item); + } else { + assertSentryTransaction(item, expected); + } +} + +function expectSessionEvent(item: SerializedSession, expected: ExpectedSession): void { + if (typeof expected === 'function') { + expected(item); + } else { + assertSentrySession(item, expected); + } +} + +function expectSessionsEvent(item: SessionAggregates, expected: ExpectedSessions): void { + if (typeof expected === 'function') { + expected(item); + } else { + assertSentrySessions(item, expected); + } +} + +function expectCheckInEvent(item: SerializedCheckIn, expected: ExpectedCheckIn): void { + if (typeof expected === 'function') { + expected(item); + } else { + assertSentryCheckIn(item, expected); + } +} + +function expectClientReport(item: ClientReport, expected: ExpectedClientReport): void { + if (typeof expected === 'function') { + expected(item); + } else { + assertSentryClientReport(item, expected); + } +} + +function expectLog(item: SerializedLogContainer, expected: ExpectedLogContainer): void { + if (typeof expected === 'function') { + expected(item); + } else { + assertSentryLogContainer(item, expected); + } +} + +/** + * Converts ESM import statements to CommonJS require statements + * @param content The content of an ESM file + * @returns The content with require statements instead of imports + */ +function convertEsmToCjs(content: string): string { + let newContent = content; + + // Handle default imports: import x from 'y' -> const x = require('y') + newContent = newContent.replace( + /import\s+([\w*{}\s,]+)\s+from\s+['"]([^'"]+)['"]/g, + (_, imports: string, module: string) => { + if (imports.includes('* as')) { + // Handle namespace imports: import * as x from 'y' -> const x = require('y') + return `const ${imports.replace('* as', '').trim()} = require('${module}')`; + } else if (imports.includes('{')) { + // Handle named imports: import {x, y} from 'z' -> const {x, y} = require('z') + return `const ${imports} = require('${module}')`; + } else { + // Handle default imports: import x from 'y' -> const x = require('y') + return `const ${imports} = require('${module}')`; + } + }, + ); + + // Handle side-effect imports: import 'x' -> require('x') + newContent = newContent.replace(/import\s+['"]([^'"]+)['"]/g, (_, module) => { + return `require('${module}')`; + }); + + return newContent; +} diff --git a/dev-packages/node-core-integration-tests/utils/server.ts b/dev-packages/node-core-integration-tests/utils/server.ts new file mode 100644 index 000000000000..92e0477c845c --- /dev/null +++ b/dev-packages/node-core-integration-tests/utils/server.ts @@ -0,0 +1,85 @@ +import type { Envelope } from '@sentry/core'; +import { parseEnvelope } from '@sentry/core'; +import express from 'express'; +import type { AddressInfo } from 'net'; + +/** + * Creates a basic Sentry server that accepts POST to the envelope endpoint + * + * This does no checks on the envelope, it just calls the callback if it managed to parse an envelope from the raw POST + * body data. + */ +export function createBasicSentryServer(onEnvelope: (env: Envelope) => void): Promise<[number, () => void]> { + const app = express(); + + app.use(express.raw({ type: () => true, inflate: true, limit: '100mb' })); + app.post('/api/:id/envelope/', (req, res) => { + try { + const env = parseEnvelope(req.body as Buffer); + onEnvelope(env); + } catch (e) { + // eslint-disable-next-line no-console + console.error(e); + } + + res.status(200).send(); + }); + + return new Promise(resolve => { + const server = app.listen(0, () => { + const address = server.address() as AddressInfo; + resolve([ + address.port, + () => { + server.close(); + }, + ]); + }); + }); +} + +type HeaderAssertCallback = (headers: Record) => void; + +/** Creates a test server that can be used to check headers */ +// eslint-disable-next-line @typescript-eslint/explicit-function-return-type +export function createTestServer() { + const gets: Array<[string, HeaderAssertCallback, number]> = []; + let error: unknown | undefined; + + return { + get: function (path: string, callback: HeaderAssertCallback, result = 200) { + gets.push([path, callback, result]); + return this; + }, + start: async (): Promise<[string, () => void]> => { + const app = express(); + + for (const [path, callback, result] of gets) { + app.get(path, (req, res) => { + try { + callback(req.headers); + } catch (e) { + error = e; + } + + res.status(result).send(); + }); + } + + return new Promise(resolve => { + const server = app.listen(0, () => { + const address = server.address() as AddressInfo; + resolve([ + `http://localhost:${address.port}`, + () => { + server.close(); + if (error) { + throw error; + } + }, + ]); + }); + }); + }, + }; +} diff --git a/dev-packages/node-core-integration-tests/utils/setup-tests.ts b/dev-packages/node-core-integration-tests/utils/setup-tests.ts new file mode 100644 index 000000000000..6f7bb2bec369 --- /dev/null +++ b/dev-packages/node-core-integration-tests/utils/setup-tests.ts @@ -0,0 +1,12 @@ +import EventEmitter from 'events'; + +const setup = async (): Promise => { + // Node warns about a potential memory leak + // when more than 10 event listeners are assigned inside a single thread. + // Initializing Sentry for each test triggers these warnings after 10th test inside Jest thread. + // As we know that it's not a memory leak and number of listeners are limited to the number of tests, + // removing the limit on listener count here. + EventEmitter.defaultMaxListeners = 0; +}; + +export default setup; diff --git a/dev-packages/node-core-integration-tests/utils/setupOtel.js b/dev-packages/node-core-integration-tests/utils/setupOtel.js new file mode 100644 index 000000000000..bcbf874ac7f0 --- /dev/null +++ b/dev-packages/node-core-integration-tests/utils/setupOtel.js @@ -0,0 +1,17 @@ +const { trace, propagation, context } = require('@opentelemetry/api'); +const { BasicTracerProvider } = require('@opentelemetry/sdk-trace-base'); +const Sentry = require('@sentry/node-core'); +const { SentryPropagator, SentrySampler, SentrySpanProcessor } = require('@sentry/opentelemetry'); + +exports.setupOtel = function setupOtel(client) { + const provider = new BasicTracerProvider({ + sampler: client ? new SentrySampler(client) : undefined, + spanProcessors: [new SentrySpanProcessor()], + }); + + trace.setGlobalTracerProvider(provider); + propagation.setGlobalPropagator(new SentryPropagator()); + context.setGlobalContextManager(new Sentry.SentryContextManager()); + + Sentry.validateOpenTelemetrySetup(); +}; diff --git a/dev-packages/node-core-integration-tests/utils/setupOtel.ts b/dev-packages/node-core-integration-tests/utils/setupOtel.ts new file mode 100644 index 000000000000..2c7488b9c64f --- /dev/null +++ b/dev-packages/node-core-integration-tests/utils/setupOtel.ts @@ -0,0 +1,38 @@ +import { context, propagation, trace } from '@opentelemetry/api'; +import { BasicTracerProvider } from '@opentelemetry/sdk-trace-base'; +import type { Client } from '@sentry/core'; +import * as Sentry from '@sentry/node-core'; +import { SentryPropagator, SentrySampler, SentrySpanProcessor } from '@sentry/opentelemetry'; + +export function setupOtel(client: Client | undefined): BasicTracerProvider | undefined { + if (!client) { + return undefined; + } + + const provider = new BasicTracerProvider({ + sampler: new SentrySampler(client), + spanProcessors: [new SentrySpanProcessor()], + }); + + trace.setGlobalTracerProvider(provider); + propagation.setGlobalPropagator(new SentryPropagator()); + context.setGlobalContextManager(new Sentry.SentryContextManager()); + + Sentry.validateOpenTelemetrySetup(); + + return provider; +} + +export function cleanupOtel(provider: BasicTracerProvider): void { + void provider.forceFlush().catch(() => { + // no-op + }); + void provider.shutdown().catch(() => { + // no-op + }); + + // Disable all globally registered APIs + trace.disable(); + context.disable(); + propagation.disable(); +} diff --git a/dev-packages/node-core-integration-tests/vite.config.ts b/dev-packages/node-core-integration-tests/vite.config.ts new file mode 100644 index 000000000000..4b2c3b2a0a74 --- /dev/null +++ b/dev-packages/node-core-integration-tests/vite.config.ts @@ -0,0 +1,31 @@ +import { defineConfig } from 'vitest/config'; +import baseConfig from '../../vite/vite.config'; + +export default defineConfig({ + ...baseConfig, + test: { + ...baseConfig.test, + isolate: false, + coverage: { + enabled: false, + }, + include: ['./**/test.ts'], + testTimeout: 15000, + // Ensure we can see debug output when DEBUG=true + ...(process.env.DEBUG + ? { + disableConsoleIntercept: true, + silent: false, + } + : {}), + // By default Vitest uses child processes to run tests but all our tests + // already run in their own processes. We use threads instead because the + // overhead is significantly less. + pool: 'threads', + reporters: process.env.DEBUG + ? ['default', { summary: false }] + : process.env.GITHUB_ACTIONS + ? ['dot', 'github-actions'] + : ['verbose'], + }, +}); diff --git a/dev-packages/rollup-utils/npmHelpers.mjs b/dev-packages/rollup-utils/npmHelpers.mjs index 7cd41344e596..6ef09192eef1 100644 --- a/dev-packages/rollup-utils/npmHelpers.mjs +++ b/dev-packages/rollup-utils/npmHelpers.mjs @@ -152,9 +152,14 @@ export function makeOtelLoaders(outputFolder, hookVariant) { } const requiredDep = hookVariant === 'otel' ? '@opentelemetry/instrumentation' : '@sentry/node'; - const foundImportInTheMiddleDep = Object.keys(packageDotJSON.dependencies ?? {}).some(key => { - return key === requiredDep; - }); + const foundImportInTheMiddleDep = + Object.keys(packageDotJSON.dependencies ?? {}).some(key => { + return key === requiredDep; + }) || + Object.keys(packageDotJSON.devDependencies ?? {}).some(key => { + return key === requiredDep; + }); + if (!foundImportInTheMiddleDep) { throw new Error( `You used the makeOtelLoaders() rollup utility but didn't specify the "${requiredDep}" dependency in ${path.resolve( diff --git a/package.json b/package.json index bdcadb520b22..e6488580dc45 100644 --- a/package.json +++ b/package.json @@ -32,8 +32,8 @@ "dedupe-deps:check": "yarn-deduplicate yarn.lock --list --fail", "dedupe-deps:fix": "yarn-deduplicate yarn.lock", "postpublish": "lerna run --stream --concurrency 1 postpublish", - "test": "lerna run --ignore \"@sentry-internal/{browser-integration-tests,e2e-tests,integration-shims,node-integration-tests}\" test", - "test:unit": "lerna run --ignore \"@sentry-internal/{browser-integration-tests,e2e-tests,integration-shims,node-integration-tests}\" test:unit", + "test": "lerna run --ignore \"@sentry-internal/{browser-integration-tests,e2e-tests,integration-shims,node-integration-tests,node-core-integration-tests}\" test", + "test:unit": "lerna run --ignore \"@sentry-internal/{browser-integration-tests,e2e-tests,integration-shims,node-integration-tests,node-core-integration-tests}\" test:unit", "test:update-snapshots": "lerna run test:update-snapshots", "test:pr": "nx affected -t test --exclude \"@sentry-internal/{browser-integration-tests,e2e-tests,integration-shims,node-integration-tests}\"", "test:pr:browser": "UNIT_TEST_ENV=browser ts-node ./scripts/ci-unit-tests.ts --affected", @@ -95,6 +95,7 @@ "dev-packages/bundle-analyzer-scenarios", "dev-packages/e2e-tests", "dev-packages/node-integration-tests", + "dev-packages/node-core-integration-tests", "dev-packages/test-utils", "dev-packages/size-limit-gh-action", "dev-packages/clear-cache-gh-action", From fea070720c79a9d8b31ba7c60d3bfcb66ffe0cd4 Mon Sep 17 00:00:00 2001 From: Andrei Borza Date: Wed, 2 Jul 2025 23:30:24 +0200 Subject: [PATCH 04/13] Add node-core to Verdaccio config --- dev-packages/e2e-tests/verdaccio-config/config.yaml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/dev-packages/e2e-tests/verdaccio-config/config.yaml b/dev-packages/e2e-tests/verdaccio-config/config.yaml index d96c3cc6905e..1d565dbecda2 100644 --- a/dev-packages/e2e-tests/verdaccio-config/config.yaml +++ b/dev-packages/e2e-tests/verdaccio-config/config.yaml @@ -104,6 +104,12 @@ packages: unpublish: $all # proxy: npmjs # Don't proxy for E2E tests! + '@sentry/node-core': + access: $all + publish: $all + unpublish: $all + # proxy: npmjs # Don't proxy for E2E tests! + '@sentry/node-native': access: $all publish: $all From 1c8d2260ec16e103164f1672562989f73d43d54c Mon Sep 17 00:00:00 2001 From: Andrei Borza Date: Wed, 2 Jul 2025 23:31:25 +0200 Subject: [PATCH 05/13] Add node-core e2e test app with OTel v1 deps --- .../node-core-express-otel-v1/.gitignore | 1 + .../node-core-express-otel-v1/.npmrc | 2 + .../node-core-express-otel-v1/package.json | 39 +++++++++ .../playwright.config.mjs | 7 ++ .../node-core-express-otel-v1/src/app.ts | 57 +++++++++++++ .../src/instrument.ts | 46 +++++++++++ .../start-event-proxy.mjs | 6 ++ .../tests/errors.test.ts | 42 ++++++++++ .../tests/transactions.test.ts | 80 +++++++++++++++++++ .../node-core-express-otel-v1/tsconfig.json | 11 +++ 10 files changed, 291 insertions(+) create mode 100644 dev-packages/e2e-tests/test-applications/node-core-express-otel-v1/.gitignore create mode 100644 dev-packages/e2e-tests/test-applications/node-core-express-otel-v1/.npmrc create mode 100644 dev-packages/e2e-tests/test-applications/node-core-express-otel-v1/package.json create mode 100644 dev-packages/e2e-tests/test-applications/node-core-express-otel-v1/playwright.config.mjs create mode 100644 dev-packages/e2e-tests/test-applications/node-core-express-otel-v1/src/app.ts create mode 100644 dev-packages/e2e-tests/test-applications/node-core-express-otel-v1/src/instrument.ts create mode 100644 dev-packages/e2e-tests/test-applications/node-core-express-otel-v1/start-event-proxy.mjs create mode 100644 dev-packages/e2e-tests/test-applications/node-core-express-otel-v1/tests/errors.test.ts create mode 100644 dev-packages/e2e-tests/test-applications/node-core-express-otel-v1/tests/transactions.test.ts create mode 100644 dev-packages/e2e-tests/test-applications/node-core-express-otel-v1/tsconfig.json diff --git a/dev-packages/e2e-tests/test-applications/node-core-express-otel-v1/.gitignore b/dev-packages/e2e-tests/test-applications/node-core-express-otel-v1/.gitignore new file mode 100644 index 000000000000..1521c8b7652b --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/node-core-express-otel-v1/.gitignore @@ -0,0 +1 @@ +dist diff --git a/dev-packages/e2e-tests/test-applications/node-core-express-otel-v1/.npmrc b/dev-packages/e2e-tests/test-applications/node-core-express-otel-v1/.npmrc new file mode 100644 index 000000000000..070f80f05092 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/node-core-express-otel-v1/.npmrc @@ -0,0 +1,2 @@ +@sentry:registry=http://127.0.0.1:4873 +@sentry-internal:registry=http://127.0.0.1:4873 diff --git a/dev-packages/e2e-tests/test-applications/node-core-express-otel-v1/package.json b/dev-packages/e2e-tests/test-applications/node-core-express-otel-v1/package.json new file mode 100644 index 000000000000..67351f3ab187 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/node-core-express-otel-v1/package.json @@ -0,0 +1,39 @@ +{ + "name": "node-core-express-app", + "version": "1.0.0", + "private": true, + "scripts": { + "build": "tsc", + "start": "node dist/app.js", + "test": "playwright test", + "clean": "npx rimraf node_modules pnpm-lock.yaml", + "test:build": "pnpm install && pnpm build", + "test:assert": "pnpm test" + }, + "dependencies": { + "@sentry/node-core": "latest || *", + "@sentry/opentelemetry": "latest || *", + "@opentelemetry/api": "^1.9.0", + "@opentelemetry/context-async-hooks": "^1.30.1", + "@opentelemetry/core": "^1.30.1", + "@opentelemetry/instrumentation": "^0.57.1", + "@opentelemetry/instrumentation-http": "^0.57.1", + "@opentelemetry/resources": "^1.30.1", + "@opentelemetry/sdk-trace-node": "^1.30.1", + "@opentelemetry/semantic-conventions": "^1.30.0", + "@types/express": "^4.17.21", + "@types/node": "^18.19.1", + "express": "^4.21.2", + "typescript": "~5.0.0" + }, + "devDependencies": { + "@playwright/test": "~1.50.0", + "@sentry-internal/test-utils": "link:../../../test-utils" + }, + "resolutions": { + "@types/qs": "6.9.17" + }, + "volta": { + "extends": "../../package.json" + } +} diff --git a/dev-packages/e2e-tests/test-applications/node-core-express-otel-v1/playwright.config.mjs b/dev-packages/e2e-tests/test-applications/node-core-express-otel-v1/playwright.config.mjs new file mode 100644 index 000000000000..31f2b913b58b --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/node-core-express-otel-v1/playwright.config.mjs @@ -0,0 +1,7 @@ +import { getPlaywrightConfig } from '@sentry-internal/test-utils'; + +const config = getPlaywrightConfig({ + startCommand: `pnpm start`, +}); + +export default config; diff --git a/dev-packages/e2e-tests/test-applications/node-core-express-otel-v1/src/app.ts b/dev-packages/e2e-tests/test-applications/node-core-express-otel-v1/src/app.ts new file mode 100644 index 000000000000..d5bf40067de0 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/node-core-express-otel-v1/src/app.ts @@ -0,0 +1,57 @@ +// Import this first! +import './instrument'; + +// Now import other modules +import * as Sentry from '@sentry/node-core'; +import express from 'express'; + +const app = express(); +const port = 3030; + +app.get('/test-transaction', function (req, res) { + Sentry.withActiveSpan(null, async () => { + Sentry.startSpan({ name: 'test-transaction', op: 'e2e-test' }, () => { + Sentry.startSpan({ name: 'test-span' }, () => undefined); + }); + + await Sentry.flush(); + + res.send({ + transactionIds: global.transactionIds || [], + }); + }); +}); + +app.get('/test-exception/:id', function (req, _res) { + try { + throw new Error(`This is an exception with id ${req.params.id}`); + } catch (e) { + Sentry.captureException(e); + throw e; + } +}); + +app.get('/test-local-variables-caught', function (req, res) { + const randomVariableToRecord = Math.random(); + + let exceptionId: string; + try { + throw new Error('Local Variable Error'); + } catch (e) { + exceptionId = Sentry.captureException(e); + } + + res.send({ exceptionId, randomVariableToRecord }); +}); + +// @ts-ignore +app.use(function onError(err, req, res, next) { + // The error id is attached to `res.sentry` to be returned + // and optionally displayed to the user for support. + res.statusCode = 500; + res.end(res.sentry + '\n'); +}); + +app.listen(port, () => { + console.log(`Example app listening on port ${port}`); +}); diff --git a/dev-packages/e2e-tests/test-applications/node-core-express-otel-v1/src/instrument.ts b/dev-packages/e2e-tests/test-applications/node-core-express-otel-v1/src/instrument.ts new file mode 100644 index 000000000000..a3969933ea64 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/node-core-express-otel-v1/src/instrument.ts @@ -0,0 +1,46 @@ +import { NodeTracerProvider } from '@opentelemetry/sdk-trace-node'; +import * as Sentry from '@sentry/node-core'; +import { SentrySpanProcessor, SentryPropagator, SentrySampler } from '@sentry/opentelemetry'; +import { HttpInstrumentation } from '@opentelemetry/instrumentation-http'; + +declare global { + namespace globalThis { + var transactionIds: string[]; + } +} + +const sentryClient = Sentry.init({ + environment: 'qa', // dynamic sampling bias to keep transactions + dsn: process.env.E2E_TEST_DSN, + includeLocalVariables: true, + debug: !!process.env.DEBUG, + tunnel: `http://localhost:3031/`, // proxy server + tracesSampleRate: 1, + openTelemetryInstrumentations: [new HttpInstrumentation()], +}); + +const provider = new NodeTracerProvider({ + sampler: sentryClient ? new SentrySampler(sentryClient) : undefined, + spanProcessors: [new SentrySpanProcessor()], +}); + +provider.register({ + propagator: new SentryPropagator(), + contextManager: new Sentry.SentryContextManager(), +}); + +Sentry.validateOpenTelemetrySetup(); + +Sentry.addEventProcessor(event => { + global.transactionIds = global.transactionIds || []; + + if (event.type === 'transaction') { + const eventId = event.event_id; + + if (eventId) { + global.transactionIds.push(eventId); + } + } + + return event; +}); diff --git a/dev-packages/e2e-tests/test-applications/node-core-express-otel-v1/start-event-proxy.mjs b/dev-packages/e2e-tests/test-applications/node-core-express-otel-v1/start-event-proxy.mjs new file mode 100644 index 000000000000..161017eab5ee --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/node-core-express-otel-v1/start-event-proxy.mjs @@ -0,0 +1,6 @@ +import { startEventProxyServer } from '@sentry-internal/test-utils'; + +startEventProxyServer({ + port: 3031, + proxyServerName: 'node-core-express-otel-v1', +}); diff --git a/dev-packages/e2e-tests/test-applications/node-core-express-otel-v1/tests/errors.test.ts b/dev-packages/e2e-tests/test-applications/node-core-express-otel-v1/tests/errors.test.ts new file mode 100644 index 000000000000..013c622f125a --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/node-core-express-otel-v1/tests/errors.test.ts @@ -0,0 +1,42 @@ +import { expect, test } from '@playwright/test'; +import { waitForError } from '@sentry-internal/test-utils'; + +test('Sends correct error event', async ({ baseURL }) => { + const errorEventPromise = waitForError('node-core-express-otel-v1', event => { + return !event.type && event.exception?.values?.[0]?.value === 'This is an exception with id 123'; + }); + + await fetch(`${baseURL}/test-exception/123`); + + const errorEvent = await errorEventPromise; + + expect(errorEvent.exception?.values).toHaveLength(1); + expect(errorEvent.exception?.values?.[0]?.value).toBe('This is an exception with id 123'); + + expect(errorEvent.request).toEqual({ + method: 'GET', + cookies: {}, + headers: expect.any(Object), + url: 'http://localhost:3030/test-exception/123', + }); + + expect(errorEvent.transaction).toEqual('GET /test-exception/123'); + + expect(errorEvent.contexts?.trace).toEqual({ + trace_id: expect.stringMatching(/[a-f0-9]{32}/), + span_id: expect.stringMatching(/[a-f0-9]{16}/), + }); +}); + +test('Should record caught exceptions with local variable', async ({ baseURL }) => { + const errorEventPromise = waitForError('node-core-express-otel-v1', event => { + return event.transaction === 'GET /test-local-variables-caught'; + }); + + await fetch(`${baseURL}/test-local-variables-caught`); + + const errorEvent = await errorEventPromise; + + const frames = errorEvent.exception?.values?.[0].stacktrace?.frames; + expect(frames?.[frames.length - 1].vars?.randomVariableToRecord).toBeDefined(); +}); diff --git a/dev-packages/e2e-tests/test-applications/node-core-express-otel-v1/tests/transactions.test.ts b/dev-packages/e2e-tests/test-applications/node-core-express-otel-v1/tests/transactions.test.ts new file mode 100644 index 000000000000..1628a9a03ada --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/node-core-express-otel-v1/tests/transactions.test.ts @@ -0,0 +1,80 @@ +import { expect, test } from '@playwright/test'; +import { waitForTransaction } from '@sentry-internal/test-utils'; + +test('Sends an API route transaction', async ({ baseURL }) => { + const pageloadTransactionEventPromise = waitForTransaction('node-core-express-otel-v1', transactionEvent => { + return ( + transactionEvent?.contexts?.trace?.op === 'http.server' && + transactionEvent?.transaction === 'GET /test-transaction' + ); + }); + + await fetch(`${baseURL}/test-transaction`); + + const transactionEvent = await pageloadTransactionEventPromise; + + expect(transactionEvent.contexts?.trace).toEqual({ + data: { + 'sentry.source': 'url', + 'sentry.origin': 'manual', + 'sentry.op': 'http.server', + 'sentry.sample_rate': 1, + url: 'http://localhost:3030/test-transaction', + 'otel.kind': 'SERVER', + 'http.response.status_code': 200, + 'http.url': 'http://localhost:3030/test-transaction', + 'http.host': 'localhost:3030', + 'net.host.name': 'localhost', + 'http.method': 'GET', + 'http.scheme': 'http', + 'http.target': '/test-transaction', + 'http.user_agent': 'node', + 'http.flavor': '1.1', + 'net.transport': 'ip_tcp', + 'net.host.ip': expect.any(String), + 'net.host.port': expect.any(Number), + 'net.peer.ip': expect.any(String), + 'net.peer.port': expect.any(Number), + 'http.status_code': 200, + 'http.status_text': 'OK', + }, + op: 'http.server', + span_id: expect.stringMatching(/[a-f0-9]{16}/), + status: 'ok', + trace_id: expect.stringMatching(/[a-f0-9]{32}/), + origin: 'manual', + }); + + expect(transactionEvent.contexts?.response).toEqual({ + status_code: 200, + }); + + expect(transactionEvent).toEqual( + expect.objectContaining({ + transaction: 'GET /test-transaction', + type: 'transaction', + transaction_info: { + source: 'url', + }, + }), + ); +}); + +test('Sends an API route transaction for an errored route', async ({ baseURL }) => { + const transactionEventPromise = waitForTransaction('node-core-express-otel-v1', transactionEvent => { + return ( + transactionEvent.contexts?.trace?.op === 'http.server' && + transactionEvent.transaction === 'GET /test-exception/777' && + transactionEvent.request?.url === 'http://localhost:3030/test-exception/777' + ); + }); + + await fetch(`${baseURL}/test-exception/777`); + + const transactionEvent = await transactionEventPromise; + + expect(transactionEvent.contexts?.trace?.op).toEqual('http.server'); + expect(transactionEvent.transaction).toEqual('GET /test-exception/777'); + expect(transactionEvent.contexts?.trace?.status).toEqual('internal_error'); + expect(transactionEvent.contexts?.trace?.data?.['http.status_code']).toEqual(500); +}); diff --git a/dev-packages/e2e-tests/test-applications/node-core-express-otel-v1/tsconfig.json b/dev-packages/e2e-tests/test-applications/node-core-express-otel-v1/tsconfig.json new file mode 100644 index 000000000000..0060abd94682 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/node-core-express-otel-v1/tsconfig.json @@ -0,0 +1,11 @@ +{ + "compilerOptions": { + "types": ["node"], + "esModuleInterop": true, + "lib": ["es2020"], + "strict": true, + "outDir": "dist", + "skipLibCheck": true + }, + "include": ["src/**/*.ts"] +} From 0d217d43b439d86becca20ca0e9efff4c112aa28 Mon Sep 17 00:00:00 2001 From: Andrei Borza Date: Wed, 2 Jul 2025 23:31:58 +0200 Subject: [PATCH 06/13] Add node-core e2e test app with OTel v2 deps --- .../node-core-express-otel-v2/.gitignore | 1 + .../node-core-express-otel-v2/.npmrc | 2 + .../node-core-express-otel-v2/package.json | 39 +++++++++ .../playwright.config.mjs | 7 ++ .../node-core-express-otel-v2/src/app.ts | 57 +++++++++++++ .../src/instrument.ts | 46 +++++++++++ .../start-event-proxy.mjs | 6 ++ .../tests/errors.test.ts | 42 ++++++++++ .../tests/transactions.test.ts | 80 +++++++++++++++++++ .../node-core-express-otel-v2/tsconfig.json | 11 +++ 10 files changed, 291 insertions(+) create mode 100644 dev-packages/e2e-tests/test-applications/node-core-express-otel-v2/.gitignore create mode 100644 dev-packages/e2e-tests/test-applications/node-core-express-otel-v2/.npmrc create mode 100644 dev-packages/e2e-tests/test-applications/node-core-express-otel-v2/package.json create mode 100644 dev-packages/e2e-tests/test-applications/node-core-express-otel-v2/playwright.config.mjs create mode 100644 dev-packages/e2e-tests/test-applications/node-core-express-otel-v2/src/app.ts create mode 100644 dev-packages/e2e-tests/test-applications/node-core-express-otel-v2/src/instrument.ts create mode 100644 dev-packages/e2e-tests/test-applications/node-core-express-otel-v2/start-event-proxy.mjs create mode 100644 dev-packages/e2e-tests/test-applications/node-core-express-otel-v2/tests/errors.test.ts create mode 100644 dev-packages/e2e-tests/test-applications/node-core-express-otel-v2/tests/transactions.test.ts create mode 100644 dev-packages/e2e-tests/test-applications/node-core-express-otel-v2/tsconfig.json diff --git a/dev-packages/e2e-tests/test-applications/node-core-express-otel-v2/.gitignore b/dev-packages/e2e-tests/test-applications/node-core-express-otel-v2/.gitignore new file mode 100644 index 000000000000..1521c8b7652b --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/node-core-express-otel-v2/.gitignore @@ -0,0 +1 @@ +dist diff --git a/dev-packages/e2e-tests/test-applications/node-core-express-otel-v2/.npmrc b/dev-packages/e2e-tests/test-applications/node-core-express-otel-v2/.npmrc new file mode 100644 index 000000000000..070f80f05092 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/node-core-express-otel-v2/.npmrc @@ -0,0 +1,2 @@ +@sentry:registry=http://127.0.0.1:4873 +@sentry-internal:registry=http://127.0.0.1:4873 diff --git a/dev-packages/e2e-tests/test-applications/node-core-express-otel-v2/package.json b/dev-packages/e2e-tests/test-applications/node-core-express-otel-v2/package.json new file mode 100644 index 000000000000..58dcd720e5b2 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/node-core-express-otel-v2/package.json @@ -0,0 +1,39 @@ +{ + "name": "node-core-express-otel-v2-app", + "version": "1.0.0", + "private": true, + "scripts": { + "build": "tsc", + "start": "node dist/app.js", + "test": "playwright test", + "clean": "npx rimraf node_modules pnpm-lock.yaml", + "test:build": "pnpm install && pnpm build", + "test:assert": "pnpm test" + }, + "dependencies": { + "@sentry/node-core": "latest || *", + "@sentry/opentelemetry": "latest || *", + "@opentelemetry/api": "^1.9.0", + "@opentelemetry/context-async-hooks": "^2.0.0", + "@opentelemetry/core": "^2.0.0", + "@opentelemetry/instrumentation": "^0.200.0", + "@opentelemetry/instrumentation-http": "^0.200.0", + "@opentelemetry/resources": "^2.0.0", + "@opentelemetry/sdk-trace-node": "^2.0.0", + "@opentelemetry/semantic-conventions": "^1.30.0", + "@types/express": "^4.17.21", + "@types/node": "^18.19.1", + "express": "^4.21.2", + "typescript": "~5.0.0" + }, + "devDependencies": { + "@playwright/test": "~1.50.0", + "@sentry-internal/test-utils": "link:../../../test-utils" + }, + "resolutions": { + "@types/qs": "6.9.17" + }, + "volta": { + "extends": "../../package.json" + } +} diff --git a/dev-packages/e2e-tests/test-applications/node-core-express-otel-v2/playwright.config.mjs b/dev-packages/e2e-tests/test-applications/node-core-express-otel-v2/playwright.config.mjs new file mode 100644 index 000000000000..31f2b913b58b --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/node-core-express-otel-v2/playwright.config.mjs @@ -0,0 +1,7 @@ +import { getPlaywrightConfig } from '@sentry-internal/test-utils'; + +const config = getPlaywrightConfig({ + startCommand: `pnpm start`, +}); + +export default config; diff --git a/dev-packages/e2e-tests/test-applications/node-core-express-otel-v2/src/app.ts b/dev-packages/e2e-tests/test-applications/node-core-express-otel-v2/src/app.ts new file mode 100644 index 000000000000..d5bf40067de0 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/node-core-express-otel-v2/src/app.ts @@ -0,0 +1,57 @@ +// Import this first! +import './instrument'; + +// Now import other modules +import * as Sentry from '@sentry/node-core'; +import express from 'express'; + +const app = express(); +const port = 3030; + +app.get('/test-transaction', function (req, res) { + Sentry.withActiveSpan(null, async () => { + Sentry.startSpan({ name: 'test-transaction', op: 'e2e-test' }, () => { + Sentry.startSpan({ name: 'test-span' }, () => undefined); + }); + + await Sentry.flush(); + + res.send({ + transactionIds: global.transactionIds || [], + }); + }); +}); + +app.get('/test-exception/:id', function (req, _res) { + try { + throw new Error(`This is an exception with id ${req.params.id}`); + } catch (e) { + Sentry.captureException(e); + throw e; + } +}); + +app.get('/test-local-variables-caught', function (req, res) { + const randomVariableToRecord = Math.random(); + + let exceptionId: string; + try { + throw new Error('Local Variable Error'); + } catch (e) { + exceptionId = Sentry.captureException(e); + } + + res.send({ exceptionId, randomVariableToRecord }); +}); + +// @ts-ignore +app.use(function onError(err, req, res, next) { + // The error id is attached to `res.sentry` to be returned + // and optionally displayed to the user for support. + res.statusCode = 500; + res.end(res.sentry + '\n'); +}); + +app.listen(port, () => { + console.log(`Example app listening on port ${port}`); +}); diff --git a/dev-packages/e2e-tests/test-applications/node-core-express-otel-v2/src/instrument.ts b/dev-packages/e2e-tests/test-applications/node-core-express-otel-v2/src/instrument.ts new file mode 100644 index 000000000000..a3969933ea64 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/node-core-express-otel-v2/src/instrument.ts @@ -0,0 +1,46 @@ +import { NodeTracerProvider } from '@opentelemetry/sdk-trace-node'; +import * as Sentry from '@sentry/node-core'; +import { SentrySpanProcessor, SentryPropagator, SentrySampler } from '@sentry/opentelemetry'; +import { HttpInstrumentation } from '@opentelemetry/instrumentation-http'; + +declare global { + namespace globalThis { + var transactionIds: string[]; + } +} + +const sentryClient = Sentry.init({ + environment: 'qa', // dynamic sampling bias to keep transactions + dsn: process.env.E2E_TEST_DSN, + includeLocalVariables: true, + debug: !!process.env.DEBUG, + tunnel: `http://localhost:3031/`, // proxy server + tracesSampleRate: 1, + openTelemetryInstrumentations: [new HttpInstrumentation()], +}); + +const provider = new NodeTracerProvider({ + sampler: sentryClient ? new SentrySampler(sentryClient) : undefined, + spanProcessors: [new SentrySpanProcessor()], +}); + +provider.register({ + propagator: new SentryPropagator(), + contextManager: new Sentry.SentryContextManager(), +}); + +Sentry.validateOpenTelemetrySetup(); + +Sentry.addEventProcessor(event => { + global.transactionIds = global.transactionIds || []; + + if (event.type === 'transaction') { + const eventId = event.event_id; + + if (eventId) { + global.transactionIds.push(eventId); + } + } + + return event; +}); diff --git a/dev-packages/e2e-tests/test-applications/node-core-express-otel-v2/start-event-proxy.mjs b/dev-packages/e2e-tests/test-applications/node-core-express-otel-v2/start-event-proxy.mjs new file mode 100644 index 000000000000..23a724872457 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/node-core-express-otel-v2/start-event-proxy.mjs @@ -0,0 +1,6 @@ +import { startEventProxyServer } from '@sentry-internal/test-utils'; + +startEventProxyServer({ + port: 3031, + proxyServerName: 'node-core-express-otel-v2', +}); diff --git a/dev-packages/e2e-tests/test-applications/node-core-express-otel-v2/tests/errors.test.ts b/dev-packages/e2e-tests/test-applications/node-core-express-otel-v2/tests/errors.test.ts new file mode 100644 index 000000000000..f4832729b899 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/node-core-express-otel-v2/tests/errors.test.ts @@ -0,0 +1,42 @@ +import { expect, test } from '@playwright/test'; +import { waitForError } from '@sentry-internal/test-utils'; + +test('Sends correct error event', async ({ baseURL }) => { + const errorEventPromise = waitForError('node-core-express-otel-v2', event => { + return !event.type && event.exception?.values?.[0]?.value === 'This is an exception with id 123'; + }); + + await fetch(`${baseURL}/test-exception/123`); + + const errorEvent = await errorEventPromise; + + expect(errorEvent.exception?.values).toHaveLength(1); + expect(errorEvent.exception?.values?.[0]?.value).toBe('This is an exception with id 123'); + + expect(errorEvent.request).toEqual({ + method: 'GET', + cookies: {}, + headers: expect.any(Object), + url: 'http://localhost:3030/test-exception/123', + }); + + expect(errorEvent.transaction).toEqual('GET /test-exception/123'); + + expect(errorEvent.contexts?.trace).toEqual({ + trace_id: expect.stringMatching(/[a-f0-9]{32}/), + span_id: expect.stringMatching(/[a-f0-9]{16}/), + }); +}); + +test('Should record caught exceptions with local variable', async ({ baseURL }) => { + const errorEventPromise = waitForError('node-core-express-otel-v2', event => { + return event.transaction === 'GET /test-local-variables-caught'; + }); + + await fetch(`${baseURL}/test-local-variables-caught`); + + const errorEvent = await errorEventPromise; + + const frames = errorEvent.exception?.values?.[0].stacktrace?.frames; + expect(frames?.[frames.length - 1].vars?.randomVariableToRecord).toBeDefined(); +}); diff --git a/dev-packages/e2e-tests/test-applications/node-core-express-otel-v2/tests/transactions.test.ts b/dev-packages/e2e-tests/test-applications/node-core-express-otel-v2/tests/transactions.test.ts new file mode 100644 index 000000000000..f3b1b680f2e9 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/node-core-express-otel-v2/tests/transactions.test.ts @@ -0,0 +1,80 @@ +import { expect, test } from '@playwright/test'; +import { waitForTransaction } from '@sentry-internal/test-utils'; + +test('Sends an API route transaction', async ({ baseURL }) => { + const pageloadTransactionEventPromise = waitForTransaction('node-core-express-otel-v2', transactionEvent => { + return ( + transactionEvent?.contexts?.trace?.op === 'http.server' && + transactionEvent?.transaction === 'GET /test-transaction' + ); + }); + + await fetch(`${baseURL}/test-transaction`); + + const transactionEvent = await pageloadTransactionEventPromise; + + expect(transactionEvent.contexts?.trace).toEqual({ + data: { + 'sentry.source': 'url', + 'sentry.origin': 'manual', + 'sentry.op': 'http.server', + 'sentry.sample_rate': 1, + url: 'http://localhost:3030/test-transaction', + 'otel.kind': 'SERVER', + 'http.response.status_code': 200, + 'http.url': 'http://localhost:3030/test-transaction', + 'http.host': 'localhost:3030', + 'net.host.name': 'localhost', + 'http.method': 'GET', + 'http.scheme': 'http', + 'http.target': '/test-transaction', + 'http.user_agent': 'node', + 'http.flavor': '1.1', + 'net.transport': 'ip_tcp', + 'net.host.ip': expect.any(String), + 'net.host.port': expect.any(Number), + 'net.peer.ip': expect.any(String), + 'net.peer.port': expect.any(Number), + 'http.status_code': 200, + 'http.status_text': 'OK', + }, + op: 'http.server', + span_id: expect.stringMatching(/[a-f0-9]{16}/), + status: 'ok', + trace_id: expect.stringMatching(/[a-f0-9]{32}/), + origin: 'manual', + }); + + expect(transactionEvent.contexts?.response).toEqual({ + status_code: 200, + }); + + expect(transactionEvent).toEqual( + expect.objectContaining({ + transaction: 'GET /test-transaction', + type: 'transaction', + transaction_info: { + source: 'url', + }, + }), + ); +}); + +test('Sends an API route transaction for an errored route', async ({ baseURL }) => { + const transactionEventPromise = waitForTransaction('node-core-express-otel-v2', transactionEvent => { + return ( + transactionEvent.contexts?.trace?.op === 'http.server' && + transactionEvent.transaction === 'GET /test-exception/777' && + transactionEvent.request?.url === 'http://localhost:3030/test-exception/777' + ); + }); + + await fetch(`${baseURL}/test-exception/777`); + + const transactionEvent = await transactionEventPromise; + + expect(transactionEvent.contexts?.trace?.op).toEqual('http.server'); + expect(transactionEvent.transaction).toEqual('GET /test-exception/777'); + expect(transactionEvent.contexts?.trace?.status).toEqual('internal_error'); + expect(transactionEvent.contexts?.trace?.data?.['http.status_code']).toEqual(500); +}); diff --git a/dev-packages/e2e-tests/test-applications/node-core-express-otel-v2/tsconfig.json b/dev-packages/e2e-tests/test-applications/node-core-express-otel-v2/tsconfig.json new file mode 100644 index 000000000000..0060abd94682 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/node-core-express-otel-v2/tsconfig.json @@ -0,0 +1,11 @@ +{ + "compilerOptions": { + "types": ["node"], + "esModuleInterop": true, + "lib": ["es2020"], + "strict": true, + "outDir": "dist", + "skipLibCheck": true + }, + "include": ["src/**/*.ts"] +} From b783c5e28c54330413ba61e0090d039b2976be4e Mon Sep 17 00:00:00 2001 From: Andrei Borza Date: Wed, 2 Jul 2025 23:32:08 +0200 Subject: [PATCH 07/13] Add node-core to .craft.yml --- .craft.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.craft.yml b/.craft.yml index deb38bf0c40d..efb18669ac9f 100644 --- a/.craft.yml +++ b/.craft.yml @@ -8,10 +8,13 @@ targets: - name: npm id: '@sentry/types' includeNames: /^sentry-types-\d.*\.tgz$/ - ## 1.2 Core SDK + ## 1.2 Core SDKs - name: npm id: '@sentry/core' includeNames: /^sentry-core-\d.*\.tgz$/ + - name: npm + id: '@sentry/node-core' + includeNames: /^sentry-node-core-\d.*\.tgz$/ ## 1.3 Browser Utils package - name: npm id: '@sentry-internal/browser-utils' From 515e0280d60e6d2b8a0253a3e5c343d2b23e4cae Mon Sep 17 00:00:00 2001 From: Andrei <168741329+andreiborza@users.noreply.github.com> Date: Thu, 3 Jul 2025 16:00:58 +0200 Subject: [PATCH 08/13] Update packages/node-core/LICENSE Co-authored-by: Abhijeet Prasad --- packages/node-core/LICENSE | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/node-core/LICENSE b/packages/node-core/LICENSE index b3c4b18a6317..0da96cd2f885 100644 --- a/packages/node-core/LICENSE +++ b/packages/node-core/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2023 Functional Software, Inc. dba Sentry +Copyright (c) 2025 Functional Software, Inc. dba Sentry 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 From 7ece7eaf72d210bb8326047aa535f9998e3d60e1 Mon Sep 17 00:00:00 2001 From: Andrei Borza Date: Thu, 3 Jul 2025 23:34:54 +0200 Subject: [PATCH 09/13] Export `isWrapped` from `@opentelemetry/instrumentation` to ensure v1/v2 interop --- packages/node-core/src/utils/ensureIsWrapped.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/node-core/src/utils/ensureIsWrapped.ts b/packages/node-core/src/utils/ensureIsWrapped.ts index 3a6518e7ec14..70253d9debb7 100644 --- a/packages/node-core/src/utils/ensureIsWrapped.ts +++ b/packages/node-core/src/utils/ensureIsWrapped.ts @@ -1,4 +1,4 @@ -import { isWrapped } from '@opentelemetry/core'; +import { isWrapped } from '@opentelemetry/instrumentation'; import { consoleSandbox, getClient, getGlobalScope, hasSpansEnabled, isEnabled } from '@sentry/core'; import type { NodeClient } from '../sdk/client'; import { isCjs } from './commonjs'; From 0c455ad630ef885a345001c5d9b818b8c943e82f Mon Sep 17 00:00:00 2001 From: Andrei Borza Date: Fri, 4 Jul 2025 00:15:18 +0200 Subject: [PATCH 10/13] Use SDK_VERSION for http and fetch instrumentation version --- .../src/integrations/http/SentryHttpInstrumentation.ts | 5 +++-- .../node-fetch/SentryNodeFetchInstrumentation.ts | 6 ++---- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/packages/node-core/src/integrations/http/SentryHttpInstrumentation.ts b/packages/node-core/src/integrations/http/SentryHttpInstrumentation.ts index b8c339b30185..d1cfc3b1ea0c 100644 --- a/packages/node-core/src/integrations/http/SentryHttpInstrumentation.ts +++ b/packages/node-core/src/integrations/http/SentryHttpInstrumentation.ts @@ -5,7 +5,7 @@ import type * as http from 'node:http'; import type * as https from 'node:https'; import type { EventEmitter } from 'node:stream'; import { context, propagation } from '@opentelemetry/api'; -import { isTracingSuppressed, VERSION } from '@opentelemetry/core'; +import { isTracingSuppressed } from '@opentelemetry/core'; import type { InstrumentationConfig } from '@opentelemetry/instrumentation'; import { InstrumentationBase, InstrumentationNodeModuleDefinition } from '@opentelemetry/instrumentation'; import type { AggregationCounts, Client, SanitizedRequestData, Scope } from '@sentry/core'; @@ -24,6 +24,7 @@ import { logger, LRUMap, parseUrl, + SDK_VERSION, stripUrlQueryAndFragment, withIsolationScope, } from '@sentry/core'; @@ -136,7 +137,7 @@ export class SentryHttpInstrumentation extends InstrumentationBase; public constructor(config: SentryHttpInstrumentationOptions = {}) { - super(INSTRUMENTATION_NAME, VERSION, config); + super(INSTRUMENTATION_NAME, SDK_VERSION, config); this._propagationDecisionMap = new LRUMap(100); this._ignoreOutgoingRequestsMap = new WeakMap(); diff --git a/packages/node-core/src/integrations/node-fetch/SentryNodeFetchInstrumentation.ts b/packages/node-core/src/integrations/node-fetch/SentryNodeFetchInstrumentation.ts index e17bbe58454a..3b7b745077be 100644 --- a/packages/node-core/src/integrations/node-fetch/SentryNodeFetchInstrumentation.ts +++ b/packages/node-core/src/integrations/node-fetch/SentryNodeFetchInstrumentation.ts @@ -11,6 +11,7 @@ import { getTraceData, LRUMap, parseUrl, + SDK_VERSION, } from '@sentry/core'; import { shouldPropagateTraceForUrl } from '@sentry/opentelemetry'; import * as diagch from 'diagnostics_channel'; @@ -24,9 +25,6 @@ const SENTRY_BAGGAGE_HEADER = 'baggage'; // For baggage, we make sure to merge this into a possibly existing header const BAGGAGE_HEADER_REGEX = /baggage: (.*)\r\n/; -// Bump this whenever we make changes -const VERSION = '1.0.0'; - export type SentryNodeFetchInstrumentationOptions = InstrumentationConfig & { /** * Whether breadcrumbs should be recorded for requests. @@ -68,7 +66,7 @@ export class SentryNodeFetchInstrumentation extends InstrumentationBase; public constructor(config: SentryNodeFetchInstrumentationOptions = {}) { - super('@sentry/instrumentation-node-fetch', VERSION, config); + super('@sentry/instrumentation-node-fetch', SDK_VERSION, config); this._channelSubs = []; this._propagationDecisionMap = new LRUMap(100); this._ignoreOutgoingRequestsMap = new WeakMap(); From 241cacacb95d8f4395122720c6ccbfb73711c1e8 Mon Sep 17 00:00:00 2001 From: Andrei Borza Date: Fri, 4 Jul 2025 00:16:22 +0200 Subject: [PATCH 11/13] Bump @opentelemetry/instrumentation version to latest --- .../test-applications/node-core-express-otel-v2/package.json | 4 ++-- packages/node-core/package.json | 2 +- packages/opentelemetry/package.json | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/dev-packages/e2e-tests/test-applications/node-core-express-otel-v2/package.json b/dev-packages/e2e-tests/test-applications/node-core-express-otel-v2/package.json index 58dcd720e5b2..e7f854cb7943 100644 --- a/dev-packages/e2e-tests/test-applications/node-core-express-otel-v2/package.json +++ b/dev-packages/e2e-tests/test-applications/node-core-express-otel-v2/package.json @@ -16,8 +16,8 @@ "@opentelemetry/api": "^1.9.0", "@opentelemetry/context-async-hooks": "^2.0.0", "@opentelemetry/core": "^2.0.0", - "@opentelemetry/instrumentation": "^0.200.0", - "@opentelemetry/instrumentation-http": "^0.200.0", + "@opentelemetry/instrumentation": "^0.202.0", + "@opentelemetry/instrumentation-http": "^0.202.0", "@opentelemetry/resources": "^2.0.0", "@opentelemetry/sdk-trace-node": "^2.0.0", "@opentelemetry/semantic-conventions": "^1.30.0", diff --git a/packages/node-core/package.json b/packages/node-core/package.json index 531485ede044..75adea395dc5 100644 --- a/packages/node-core/package.json +++ b/packages/node-core/package.json @@ -60,7 +60,7 @@ "@opentelemetry/api": "^1.9.0", "@opentelemetry/context-async-hooks": "^1.30.1 || ^2.0.0", "@opentelemetry/core": "^1.30.1 || ^2.0.0", - "@opentelemetry/instrumentation": "^0.57.1 || ^0.200.0", + "@opentelemetry/instrumentation": "^0.57.1 || ^0.202.0", "@opentelemetry/resources": "^1.30.1 || ^2.0.0", "@opentelemetry/sdk-trace-base": "^1.30.1 || ^2.0.0", "@opentelemetry/semantic-conventions": "^1.34.0" diff --git a/packages/opentelemetry/package.json b/packages/opentelemetry/package.json index 94506eefdd9b..e9f22208e5f6 100644 --- a/packages/opentelemetry/package.json +++ b/packages/opentelemetry/package.json @@ -45,7 +45,7 @@ "@opentelemetry/api": "^1.9.0", "@opentelemetry/context-async-hooks": "^1.30.1 || ^2.0.0", "@opentelemetry/core": "^1.30.1 || ^2.0.0", - "@opentelemetry/instrumentation": "^0.57.1 || ^0.200.0", + "@opentelemetry/instrumentation": "^0.57.1 || ^0.202.0", "@opentelemetry/sdk-trace-base": "^1.30.1 || ^2.0.0", "@opentelemetry/semantic-conventions": "^1.34.0" }, From 5a2a8111c03d2e1e7974035dd3b90ec3f85a4169 Mon Sep 17 00:00:00 2001 From: Andrei Borza Date: Fri, 4 Jul 2025 00:48:48 +0200 Subject: [PATCH 12/13] Export `setupOpenTelemetryLogger`from node-core and use in node --- packages/node-core/README.md | 44 ++++++++++++++++----------- packages/node-core/src/index.ts | 1 + packages/node-core/src/otel/logger.ts | 18 +++++++++++ packages/node-core/src/sdk/index.ts | 21 ------------- packages/node/src/sdk/initOtel.ts | 7 +++-- 5 files changed, 50 insertions(+), 41 deletions(-) create mode 100644 packages/node-core/src/otel/logger.ts diff --git a/packages/node-core/README.md b/packages/node-core/README.md index 72ab816b21aa..570957a394ee 100644 --- a/packages/node-core/README.md +++ b/packages/node-core/README.md @@ -36,7 +36,9 @@ yarn add @sentry/node-core @sentry/opentelemetry @opentelemetry/api @opentelemet Sentry should be initialized as early in your app as possible. It is essential that you call `Sentry.init` before you require any other modules in your application, otherwise any auto-instrumentation will **not** work. -You also need to set up OpenTelemetry, if you prefer not to, consider using the `@sentry/node` SDK instead. + +You also **have to** set up OpenTelemetry, if you prefer not to, consider using the `@sentry/node` SDK instead. +Without setting up OpenTelemetry, you only get basic error tracking out of the box without proper scope isolation. You need to create a file named `instrument.js` that imports and initializes Sentry: @@ -57,23 +59,29 @@ const sentryClient = Sentry.init({ // ... }); -// Note: This could be BasicTracerProvider or any other provider depending on how you want to use the -// OpenTelemetry SDK -const provider = new NodeTracerProvider({ - // Ensure the correct subset of traces is sent to Sentry - // This also ensures trace propagation works as expected - sampler: sentryClient ? new SentrySampler(sentryClient) : undefined, - spanProcessors: [ - // Ensure spans are correctly linked & sent to Sentry - new SentrySpanProcessor(), - // Add additional processors here - ], -}); - -trace.setGlobalTracerProvider(provider); -propagation.setGlobalPropagator(new SentryPropagator()); -context.setGlobalContextManager(new Sentry.SentryContextManager()); - +if (sentryClient) { + // Note: This could be BasicTracerProvider or any other provider depending on how you want to use the + // OpenTelemetry SDK + const provider = new NodeTracerProvider({ + // Ensure the correct subset of traces is sent to Sentry + // This also ensures trace propagation works as expected + sampler: new SentrySampler(sentryClient), + spanProcessors: [ + // Ensure spans are correctly linked & sent to Sentry + new SentrySpanProcessor(), + // Add additional processors here + ], + }); + + trace.setGlobalTracerProvider(provider); + propagation.setGlobalPropagator(new SentryPropagator()); + context.setGlobalContextManager(new Sentry.SentryContextManager()); +} + +// Set up the OpenTelemetry logger to use Sentry's logger +Sentry.setupOpenTelemetryLogger(); + +// validate your setup Sentry.validateOpenTelemetrySetup(); ``` diff --git a/packages/node-core/src/index.ts b/packages/node-core/src/index.ts index 2e8e16daa04c..399d3441e84b 100644 --- a/packages/node-core/src/index.ts +++ b/packages/node-core/src/index.ts @@ -24,6 +24,7 @@ export { childProcessIntegration } from './integrations/childProcess'; export { createSentryWinstonTransport } from './integrations/winston'; export { SentryContextManager } from './otel/contextManager'; +export { setupOpenTelemetryLogger } from './otel/logger'; export { generateInstrumentOnce, instrumentWhenWrapped, INSTRUMENTED } from './otel/instrument'; export { init, getDefaultIntegrations, initWithoutDefaultIntegrations, validateOpenTelemetrySetup } from './sdk'; diff --git a/packages/node-core/src/otel/logger.ts b/packages/node-core/src/otel/logger.ts new file mode 100644 index 000000000000..53cbdc63c3ee --- /dev/null +++ b/packages/node-core/src/otel/logger.ts @@ -0,0 +1,18 @@ +import { diag, DiagLogLevel } from '@opentelemetry/api'; +import { logger } from '@sentry/core'; + +/** + * Setup the OTEL logger to use our own logger. + */ +export function setupOpenTelemetryLogger(): void { + const otelLogger = new Proxy(logger as typeof logger & { verbose: (typeof logger)['debug'] }, { + get(target, prop, receiver) { + const actualProp = prop === 'verbose' ? 'debug' : prop; + return Reflect.get(target, actualProp, receiver); + }, + }); + + // Disable diag, to ensure this works even if called multiple times + diag.disable(); + diag.setLogger(otelLogger, DiagLogLevel.DEBUG); +} diff --git a/packages/node-core/src/sdk/index.ts b/packages/node-core/src/sdk/index.ts index c2e5d2c6e5c9..47c4256c5c2f 100644 --- a/packages/node-core/src/sdk/index.ts +++ b/packages/node-core/src/sdk/index.ts @@ -1,4 +1,3 @@ -import { diag, DiagLogLevel } from '@opentelemetry/api'; import type { Integration, Options } from '@sentry/core'; import { consoleIntegration, @@ -133,10 +132,6 @@ function _init( updateScopeFromEnvVariables(); - if (options.debug) { - setupOpenTelemetryLogger(); - } - enhanceDscWithOpenTelemetryRootSpanName(client); setupEventContextTrace(client); @@ -249,19 +244,3 @@ function updateScopeFromEnvVariables(): void { getCurrentScope().setPropagationContext(propagationContext); } } - -/** - * Setup the OTEL logger to use our own logger. - */ -function setupOpenTelemetryLogger(): void { - const otelLogger = new Proxy(logger as typeof logger & { verbose: (typeof logger)['debug'] }, { - get(target, prop, receiver) { - const actualProp = prop === 'verbose' ? 'debug' : prop; - return Reflect.get(target, actualProp, receiver); - }, - }); - - // Disable diag, to ensure this works even if called multiple times - diag.disable(); - diag.setLogger(otelLogger, DiagLogLevel.DEBUG); -} diff --git a/packages/node/src/sdk/initOtel.ts b/packages/node/src/sdk/initOtel.ts index e6f2b8d15786..3c35a1fecc3a 100644 --- a/packages/node/src/sdk/initOtel.ts +++ b/packages/node/src/sdk/initOtel.ts @@ -8,8 +8,7 @@ import { SEMRESATTRS_SERVICE_NAMESPACE, } from '@opentelemetry/semantic-conventions'; import { consoleSandbox, GLOBAL_OBJ, logger, SDK_VERSION } from '@sentry/core'; -import type { NodeClient } from '@sentry/node-core'; -import { isCjs, SentryContextManager } from '@sentry/node-core'; +import { type NodeClient, isCjs, SentryContextManager, setupOpenTelemetryLogger } from '@sentry/node-core'; import { SentryPropagator, SentrySampler, SentrySpanProcessor } from '@sentry/opentelemetry'; import { createAddHookMessageChannel } from 'import-in-the-middle'; import moduleModule from 'module'; @@ -28,6 +27,10 @@ interface AdditionalOpenTelemetryOptions { * Initialize OpenTelemetry for Node. */ export function initOpenTelemetry(client: NodeClient, options: AdditionalOpenTelemetryOptions = {}): void { + if (client.getOptions().debug) { + setupOpenTelemetryLogger(); + } + const provider = setupOtel(client, options); client.traceProvider = provider; } From 0c52be783c7769ec09ebda7c073265ede160dbcc Mon Sep 17 00:00:00 2001 From: Andrei Borza Date: Fri, 4 Jul 2025 13:40:47 +0200 Subject: [PATCH 13/13] Bump versions --- dev-packages/node-core-integration-tests/package.json | 6 +++--- packages/node-core/package.json | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/dev-packages/node-core-integration-tests/package.json b/dev-packages/node-core-integration-tests/package.json index 5e34fb9aacc3..d21894f1debc 100644 --- a/dev-packages/node-core-integration-tests/package.json +++ b/dev-packages/node-core-integration-tests/package.json @@ -1,6 +1,6 @@ { "name": "@sentry-internal/node-core-integration-tests", - "version": "9.34.0", + "version": "9.35.0", "license": "MIT", "engines": { "node": ">=18" @@ -34,8 +34,8 @@ "@opentelemetry/resources": "^1.30.1", "@opentelemetry/sdk-trace-base": "^1.30.1", "@opentelemetry/semantic-conventions": "^1.34.0", - "@sentry/core": "9.34.0", - "@sentry/node-core": "9.34.0", + "@sentry/core": "9.35.0", + "@sentry/node-core": "9.35.0", "body-parser": "^1.20.3", "cors": "^2.8.5", "cron": "^3.1.6", diff --git a/packages/node-core/package.json b/packages/node-core/package.json index 75adea395dc5..80f951f8ae4b 100644 --- a/packages/node-core/package.json +++ b/packages/node-core/package.json @@ -1,6 +1,6 @@ { "name": "@sentry/node-core", - "version": "9.34.0", + "version": "9.35.0", "description": "Sentry Node-Core SDK", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/node-core", @@ -66,8 +66,8 @@ "@opentelemetry/semantic-conventions": "^1.34.0" }, "dependencies": { - "@sentry/core": "9.34.0", - "@sentry/opentelemetry": "9.34.0", + "@sentry/core": "9.35.0", + "@sentry/opentelemetry": "9.35.0", "import-in-the-middle": "^1.14.2" }, "devDependencies": {