From b433821cdb071cf6d56ec0a197212d9a67c13caf Mon Sep 17 00:00:00 2001 From: Tim Fish Date: Thu, 13 Feb 2025 13:33:55 +0000 Subject: [PATCH 01/32] test(node): Run Prisma docker containers via test runner (#15402) Migrates the prisma tests to use the `withDockerCompose` option on the test runner. This means: - Docker is only required if you're running tests that actually use docker - Containers are automatically cleaned up after tests complete --- dev-packages/node-integration-tests/package.json | 3 --- .../suites/tracing/prisma-orm-v5/package.json | 3 +-- .../suites/tracing/prisma-orm-v5/test.ts | 13 +++++++++++-- .../suites/tracing/prisma-orm-v6/package.json | 3 +-- .../suites/tracing/prisma-orm-v6/test.ts | 13 +++++++++++-- dev-packages/node-integration-tests/utils/runner.ts | 9 ++++++++- 6 files changed, 32 insertions(+), 12 deletions(-) diff --git a/dev-packages/node-integration-tests/package.json b/dev-packages/node-integration-tests/package.json index c98722e16a0b..01deaadcc7de 100644 --- a/dev-packages/node-integration-tests/package.json +++ b/dev-packages/node-integration-tests/package.json @@ -16,12 +16,9 @@ "build:types": "tsc -p tsconfig.types.json", "clean": "rimraf -g **/node_modules && run-p clean:script", "clean:script": "node scripts/clean.js", - "prisma-v5:init": "cd suites/tracing/prisma-orm-v5 && yarn && yarn setup", - "prisma-v6:init": "cd suites/tracing/prisma-orm-v6 && yarn && yarn setup", "lint": "eslint . --format stylish", "fix": "eslint . --format stylish --fix", "type-check": "tsc", - "pretest": "run-s --silent prisma-v5:init prisma-v6:init", "test": "jest --config ./jest.config.js", "test:watch": "yarn test --watch" }, diff --git a/dev-packages/node-integration-tests/suites/tracing/prisma-orm-v5/package.json b/dev-packages/node-integration-tests/suites/tracing/prisma-orm-v5/package.json index b8721038c83b..d3dcaf9d1328 100644 --- a/dev-packages/node-integration-tests/suites/tracing/prisma-orm-v5/package.json +++ b/dev-packages/node-integration-tests/suites/tracing/prisma-orm-v5/package.json @@ -7,10 +7,9 @@ "node": ">=18" }, "scripts": { - "db-up": "docker compose up -d", "generate": "prisma generate", "migrate": "prisma migrate dev -n sentry-test", - "setup": "run-s --silent db-up generate migrate" + "setup": "run-s --silent generate migrate" }, "keywords": [], "author": "", diff --git a/dev-packages/node-integration-tests/suites/tracing/prisma-orm-v5/test.ts b/dev-packages/node-integration-tests/suites/tracing/prisma-orm-v5/test.ts index 0ece02f2f1cb..6f51fe329bbc 100644 --- a/dev-packages/node-integration-tests/suites/tracing/prisma-orm-v5/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/prisma-orm-v5/test.ts @@ -1,8 +1,17 @@ -import { createRunner } from '../../../utils/runner'; +import { cleanupChildProcesses, createRunner } from '../../../utils/runner'; -describe('Prisma ORM Tests', () => { +afterAll(() => { + cleanupChildProcesses(); +}); + +describe('Prisma ORM v5 Tests', () => { test('CJS - should instrument PostgreSQL queries from Prisma ORM', done => { createRunner(__dirname, 'scenario.js') + .withDockerCompose({ + workingDirectory: [__dirname], + readyMatches: ['port 5432'], + setupCommand: 'yarn && yarn setup', + }) .expect({ transaction: transaction => { expect(transaction.transaction).toBe('Test Transaction'); diff --git a/dev-packages/node-integration-tests/suites/tracing/prisma-orm-v6/package.json b/dev-packages/node-integration-tests/suites/tracing/prisma-orm-v6/package.json index a0b24c52e319..dc062f1b9e3b 100644 --- a/dev-packages/node-integration-tests/suites/tracing/prisma-orm-v6/package.json +++ b/dev-packages/node-integration-tests/suites/tracing/prisma-orm-v6/package.json @@ -7,10 +7,9 @@ "node": ">=18" }, "scripts": { - "db-up": "docker compose up -d", "generate": "prisma generate", "migrate": "prisma migrate dev -n sentry-test", - "setup": "run-s --silent db-up generate migrate" + "setup": "run-s --silent generate migrate" }, "keywords": [], "author": "", diff --git a/dev-packages/node-integration-tests/suites/tracing/prisma-orm-v6/test.ts b/dev-packages/node-integration-tests/suites/tracing/prisma-orm-v6/test.ts index 70d2fda9cbe0..34214858d8ff 100644 --- a/dev-packages/node-integration-tests/suites/tracing/prisma-orm-v6/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/prisma-orm-v6/test.ts @@ -1,9 +1,18 @@ import type { SpanJSON } from '@sentry/core'; -import { createRunner } from '../../../utils/runner'; +import { cleanupChildProcesses, createRunner } from '../../../utils/runner'; -describe('Prisma ORM Tests', () => { +afterAll(() => { + cleanupChildProcesses(); +}); + +describe('Prisma ORM v6 Tests', () => { test('CJS - should instrument PostgreSQL queries from Prisma ORM', done => { createRunner(__dirname, 'scenario.js') + .withDockerCompose({ + workingDirectory: [__dirname], + readyMatches: ['port 5432'], + setupCommand: 'yarn && yarn setup', + }) .expect({ transaction: transaction => { expect(transaction.transaction).toBe('Test Transaction'); diff --git a/dev-packages/node-integration-tests/utils/runner.ts b/dev-packages/node-integration-tests/utils/runner.ts index a5fc8df38825..152160f7118b 100644 --- a/dev-packages/node-integration-tests/utils/runner.ts +++ b/dev-packages/node-integration-tests/utils/runner.ts @@ -1,5 +1,5 @@ /* eslint-disable max-lines */ -import { spawn, spawnSync } from 'child_process'; +import { execSync, spawn, spawnSync } from 'child_process'; import { existsSync } from 'fs'; import { join } from 'path'; import { normalize } from '@sentry/core'; @@ -60,6 +60,10 @@ interface DockerOptions { * 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; } /** @@ -96,6 +100,9 @@ async function runDockerCompose(options: DockerOptions): Promise { if (text.includes(match)) { child.stdout.removeAllListeners(); clearTimeout(timeout); + if (options.setupCommand) { + execSync(options.setupCommand, { cwd, stdio: 'inherit' }); + } resolve(close); } } From 50f89f1c2bf36f59721cef50843b08114a65cb6c Mon Sep 17 00:00:00 2001 From: Ryan Albrecht Date: Thu, 13 Feb 2025 15:59:55 -0800 Subject: [PATCH 02/32] feat(feedback): Disable Feedback submit & cancel buttons while submitting (#15408) While submitting feedback the Submit and Cancel buttons previously remained active. So you could smash that submit many times and submit many duplicate feedbacks. This disabled the buttons while the network request is being fired off. The disabled buttons are slightly grayed out by the browser. So it depends on what custom color you've picked: ![SCR-20250213-kmpm](https://github.com/user-attachments/assets/883d4098-3f7f-4167-9506-4ff84b4e7e25) --- .../feedback/src/modal/components/Dialog.css.ts | 6 ++++++ packages/feedback/src/modal/components/Form.tsx | 15 +++++++++------ 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/packages/feedback/src/modal/components/Dialog.css.ts b/packages/feedback/src/modal/components/Dialog.css.ts index 8459108d1fc2..b67cae1de13c 100644 --- a/packages/feedback/src/modal/components/Dialog.css.ts +++ b/packages/feedback/src/modal/components/Dialog.css.ts @@ -115,6 +115,12 @@ const FORM = ` flex: 1 0; } +.form fieldset { + border: none; + margin: 0; + padding: 0; +} + .form__right { flex: 0 0 auto; display: flex; diff --git a/packages/feedback/src/modal/components/Form.tsx b/packages/feedback/src/modal/components/Form.tsx index a79e4d94194c..3d508f895429 100644 --- a/packages/feedback/src/modal/components/Form.tsx +++ b/packages/feedback/src/modal/components/Form.tsx @@ -61,6 +61,7 @@ export function Form({ submitButtonLabel, isRequiredLabel, } = options; + const [isSubmitting, setIsSubmitting] = useState(false); // TODO: set a ref on the form, and whenever an input changes call processForm() and setError() const [error, setError] = useState(null); @@ -97,6 +98,7 @@ export function Form({ const handleSubmit = useCallback( async (e: JSX.TargetedSubmitEvent) => { + setIsSubmitting(true); try { e.preventDefault(); if (!(e.target instanceof HTMLFormElement)) { @@ -133,8 +135,8 @@ export function Form({ setError(error as string); onSubmitError(error as Error); } - } catch { - // pass + } finally { + setIsSubmitting(false); } }, [screenshotInput && showScreenshotInput, onSubmitSuccess, onSubmitError], @@ -146,7 +148,7 @@ export function Form({ ) : null} -
+
{error ?
{error}
: null} @@ -201,6 +203,7 @@ export function Form({
- -
-
+ ); } From 7d4e1e9cae931959c9c15d6fbe1440efc93ec1be Mon Sep 17 00:00:00 2001 From: Tim Fish Date: Fri, 14 Feb 2025 12:25:43 +0100 Subject: [PATCH 03/32] test(node): Extend timeouts for Prisma tests (#15413) After moving to setup docker via the test runner, the tests became flakey. This PR extends the timeouts. --- .../suites/tracing/prisma-orm-v5/test.ts | 3 +++ .../suites/tracing/prisma-orm-v6/test.ts | 3 +++ 2 files changed, 6 insertions(+) diff --git a/dev-packages/node-integration-tests/suites/tracing/prisma-orm-v5/test.ts b/dev-packages/node-integration-tests/suites/tracing/prisma-orm-v5/test.ts index 6f51fe329bbc..da47ef8bc415 100644 --- a/dev-packages/node-integration-tests/suites/tracing/prisma-orm-v5/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/prisma-orm-v5/test.ts @@ -1,5 +1,8 @@ import { cleanupChildProcesses, createRunner } from '../../../utils/runner'; +// When running docker compose, we need a larger timeout, as this takes some time... +jest.setTimeout(75000); + afterAll(() => { cleanupChildProcesses(); }); diff --git a/dev-packages/node-integration-tests/suites/tracing/prisma-orm-v6/test.ts b/dev-packages/node-integration-tests/suites/tracing/prisma-orm-v6/test.ts index 34214858d8ff..10cb5b15c95c 100644 --- a/dev-packages/node-integration-tests/suites/tracing/prisma-orm-v6/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/prisma-orm-v6/test.ts @@ -1,6 +1,9 @@ import type { SpanJSON } from '@sentry/core'; import { cleanupChildProcesses, createRunner } from '../../../utils/runner'; +// When running docker compose, we need a larger timeout, as this takes some time... +jest.setTimeout(75000); + afterAll(() => { cleanupChildProcesses(); }); From f67fa79f9a006efdc12a46c66eb99fe75764209f Mon Sep 17 00:00:00 2001 From: Luca Forstner Date: Fri, 14 Feb 2025 15:31:37 +0100 Subject: [PATCH 04/32] fix(browser): Ensure that `performance.measure` spans have a positive duration (#15415) --- .../src/metrics/browserMetrics.ts | 17 +++++++------- .../test/browser/browserMetrics.test.ts | 23 +++++++++++++++++++ 2 files changed, 32 insertions(+), 8 deletions(-) diff --git a/packages/browser-utils/src/metrics/browserMetrics.ts b/packages/browser-utils/src/metrics/browserMetrics.ts index 794faa197ad5..62201a72ceab 100644 --- a/packages/browser-utils/src/metrics/browserMetrics.ts +++ b/packages/browser-utils/src/metrics/browserMetrics.ts @@ -425,7 +425,7 @@ export function _addMeasureSpans( startTime: number, duration: number, timeOrigin: number, -): number { +): void { const navEntry = getNavigationEntry(false); const requestTime = msToSec(navEntry ? navEntry.requestStart : 0); // Because performance.measure accepts arbitrary timestamps it can produce @@ -450,13 +450,14 @@ export function _addMeasureSpans( attributes['sentry.browser.measure_start_time'] = measureStartTimestamp; } - startAndEndSpan(span, measureStartTimestamp, measureEndTimestamp, { - name: entry.name as string, - op: entry.entryType as string, - attributes, - }); - - return measureStartTimestamp; + // Measurements from third parties can be off, which would create invalid spans, dropping transactions in the process. + if (measureStartTimestamp <= measureEndTimestamp) { + startAndEndSpan(span, measureStartTimestamp, measureEndTimestamp, { + name: entry.name as string, + op: entry.entryType as string, + attributes, + }); + } } /** Instrument navigation entries */ diff --git a/packages/browser-utils/test/browser/browserMetrics.test.ts b/packages/browser-utils/test/browser/browserMetrics.test.ts index 98a3bb375c00..27d489eae140 100644 --- a/packages/browser-utils/test/browser/browserMetrics.test.ts +++ b/packages/browser-utils/test/browser/browserMetrics.test.ts @@ -92,6 +92,29 @@ describe('_addMeasureSpans', () => { }), ); }); + + it('drops measurement spans with negative duration', () => { + const spans: Span[] = []; + + getClient()?.on('spanEnd', span => { + spans.push(span); + }); + + const entry = { + entryType: 'measure', + name: 'measure-1', + duration: 10, + startTime: 12, + } as PerformanceEntry; + + const timeOrigin = 100; + const startTime = 23; + const duration = -50; + + _addMeasureSpans(span, entry, startTime, duration, timeOrigin); + + expect(spans).toHaveLength(0); + }); }); describe('_addResourceSpans', () => { From 793109742d94936632df3ade5a4d88a58348e782 Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Fri, 14 Feb 2025 17:27:56 +0100 Subject: [PATCH 05/32] fix(nestjs): Pin dependency on @opentelemetry/instrumentation (#15419) --- packages/nestjs/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/nestjs/package.json b/packages/nestjs/package.json index bf2184205688..eaa0b6140ee7 100644 --- a/packages/nestjs/package.json +++ b/packages/nestjs/package.json @@ -46,7 +46,7 @@ "dependencies": { "@opentelemetry/api": "^1.9.0", "@opentelemetry/core": "^1.30.1", - "@opentelemetry/instrumentation": "^0.57.1", + "@opentelemetry/instrumentation": "0.57.1", "@opentelemetry/instrumentation-nestjs-core": "0.44.0", "@opentelemetry/semantic-conventions": "^1.27.0", "@sentry/core": "9.1.0", From f92f39b2c73fa58aff51baed80c4c3974401b97a Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Sat, 15 Feb 2025 13:37:04 +0100 Subject: [PATCH 06/32] chore(docs): Remove references to typedocs (#15412) Removes all references to typedocs (http://getsentry.github.io/sentry-javascript/) as this page has been deleted. --- packages/angular/README.md | 1 - packages/aws-serverless/README.md | 1 - packages/browser-utils/README.md | 1 - packages/browser/README.md | 1 - packages/bun/README.md | 1 - packages/cloudflare/README.md | 1 - packages/core/README.md | 1 - packages/deno/README.md | 1 - packages/ember/README.md | 1 - packages/eslint-config-sdk/README.md | 1 - packages/gatsby/README.md | 1 - packages/google-cloud-serverless/README.md | 1 - packages/react/README.md | 1 - packages/types/README.md | 1 - packages/typescript/README.md | 1 - packages/vue/README.md | 1 - packages/wasm/README.md | 1 - 17 files changed, 17 deletions(-) diff --git a/packages/angular/README.md b/packages/angular/README.md index 95e0379480d7..384c4c2d48c8 100644 --- a/packages/angular/README.md +++ b/packages/angular/README.md @@ -13,7 +13,6 @@ ## Links - [Official SDK Docs](https://docs.sentry.io/platforms/javascript/angular/) -- [TypeDoc](http://getsentry.github.io/sentry-javascript/) ## Angular Version Compatibility diff --git a/packages/aws-serverless/README.md b/packages/aws-serverless/README.md index 5a1ea8a1cc00..9109b8e059b0 100644 --- a/packages/aws-serverless/README.md +++ b/packages/aws-serverless/README.md @@ -9,7 +9,6 @@ ## Links - [Official SDK Docs](https://docs.sentry.io/) -- [TypeDoc](http://getsentry.github.io/sentry-javascript/) ## General diff --git a/packages/browser-utils/README.md b/packages/browser-utils/README.md index 108f3f3613c7..442228c4ddc4 100644 --- a/packages/browser-utils/README.md +++ b/packages/browser-utils/README.md @@ -13,7 +13,6 @@ ## Links - [Official SDK Docs](https://docs.sentry.io/quickstart/) -- [TypeDoc](http://getsentry.github.io/sentry-javascript/) ## General diff --git a/packages/browser/README.md b/packages/browser/README.md index 98bbf0cabca2..91f3cde774d9 100644 --- a/packages/browser/README.md +++ b/packages/browser/README.md @@ -14,7 +14,6 @@ ## Links - [Official SDK Docs](https://docs.sentry.io/quickstart/) -- [TypeDoc](http://getsentry.github.io/sentry-javascript/) ## Usage diff --git a/packages/bun/README.md b/packages/bun/README.md index 43a80713e45b..0f6f37bd6384 100644 --- a/packages/bun/README.md +++ b/packages/bun/README.md @@ -13,7 +13,6 @@ ## Links - [Official SDK Docs](https://docs.sentry.io/quickstart/) -- [TypeDoc](http://getsentry.github.io/sentry-javascript/) The Sentry Bun SDK is in beta. Please help us improve the SDK by [reporting any issues or giving us feedback](https://github.com/getsentry/sentry-javascript/issues). diff --git a/packages/cloudflare/README.md b/packages/cloudflare/README.md index 8fc88a578808..de54f5351332 100644 --- a/packages/cloudflare/README.md +++ b/packages/cloudflare/README.md @@ -13,7 +13,6 @@ ## Links - [Official SDK Docs](https://docs.sentry.io/quickstart/) -- [TypeDoc](http://getsentry.github.io/sentry-javascript/) ## Install diff --git a/packages/core/README.md b/packages/core/README.md index 3e44a1dfc7fa..c4c26b591b3e 100644 --- a/packages/core/README.md +++ b/packages/core/README.md @@ -13,7 +13,6 @@ ## Links - [Official SDK Docs](https://docs.sentry.io/quickstart/) -- [TypeDoc](http://getsentry.github.io/sentry-javascript/) ## General diff --git a/packages/deno/README.md b/packages/deno/README.md index 8986cdf85c8d..42e643c05e4e 100644 --- a/packages/deno/README.md +++ b/packages/deno/README.md @@ -13,7 +13,6 @@ ## Links - [Official SDK Docs](https://docs.sentry.io/quickstart/) -- [TypeDoc](http://getsentry.github.io/sentry-javascript/) The Sentry Deno SDK is in beta. Please help us improve the SDK by [reporting any issues or giving us feedback](https://github.com/getsentry/sentry-javascript/issues). diff --git a/packages/ember/README.md b/packages/ember/README.md index 2376869d107f..e0c9694d7d49 100644 --- a/packages/ember/README.md +++ b/packages/ember/README.md @@ -9,7 +9,6 @@ ## Links - [Official SDK Docs](https://docs.sentry.io/quickstart/) -- [TypeDoc](http://getsentry.github.io/sentry-javascript/) ## General diff --git a/packages/eslint-config-sdk/README.md b/packages/eslint-config-sdk/README.md index c3180d237bdf..4de7b40d6218 100644 --- a/packages/eslint-config-sdk/README.md +++ b/packages/eslint-config-sdk/README.md @@ -13,7 +13,6 @@ ## Links - [Official SDK Docs](https://docs.sentry.io/quickstart/) -- [TypeDoc](http://getsentry.github.io/sentry-javascript/) ## General diff --git a/packages/gatsby/README.md b/packages/gatsby/README.md index cf5eadf7045b..0473030865da 100644 --- a/packages/gatsby/README.md +++ b/packages/gatsby/README.md @@ -86,4 +86,3 @@ module.exports = { ## Links - [Official SDK Docs](https://docs.sentry.io/quickstart/) -- [TypeDoc](http://getsentry.github.io/sentry-javascript/) diff --git a/packages/google-cloud-serverless/README.md b/packages/google-cloud-serverless/README.md index 124ca28d6c16..833d47a95f56 100644 --- a/packages/google-cloud-serverless/README.md +++ b/packages/google-cloud-serverless/README.md @@ -9,7 +9,6 @@ ## Links - [Official SDK Docs](https://docs.sentry.io/) -- [TypeDoc](http://getsentry.github.io/sentry-javascript/) ## General diff --git a/packages/react/README.md b/packages/react/README.md index 066ab7f7c828..b8d9879aa231 100644 --- a/packages/react/README.md +++ b/packages/react/README.md @@ -9,7 +9,6 @@ ## Links - [Official SDK Docs](https://docs.sentry.io/platforms/javascript/guides/react/) -- [TypeDoc](http://getsentry.github.io/sentry-javascript/) ## General diff --git a/packages/types/README.md b/packages/types/README.md index a7fa71c4421b..0b52be537522 100644 --- a/packages/types/README.md +++ b/packages/types/README.md @@ -17,7 +17,6 @@ ## Links - [Official SDK Docs](https://docs.sentry.io/quickstart/) -- [TypeDoc](http://getsentry.github.io/sentry-javascript/) ## General diff --git a/packages/typescript/README.md b/packages/typescript/README.md index 94eb1cbba7e6..1ff1406c3344 100644 --- a/packages/typescript/README.md +++ b/packages/typescript/README.md @@ -13,7 +13,6 @@ ## Links - [Official SDK Docs](https://docs.sentry.io/quickstart/) -- [TypeDoc](http://getsentry.github.io/sentry-javascript/) ## General diff --git a/packages/vue/README.md b/packages/vue/README.md index 21f980cfb2c8..b2b0b349b15f 100644 --- a/packages/vue/README.md +++ b/packages/vue/README.md @@ -9,7 +9,6 @@ ## Links - [Official SDK Docs](https://docs.sentry.io/platforms/javascript/guides/vue/) -- [TypeDoc](http://getsentry.github.io/sentry-javascript/) ## General diff --git a/packages/wasm/README.md b/packages/wasm/README.md index ab5b84482f7b..3343f6b9730c 100644 --- a/packages/wasm/README.md +++ b/packages/wasm/README.md @@ -13,7 +13,6 @@ ## Links - [Official SDK Docs](https://docs.sentry.io/quickstart/) -- [TypeDoc](http://getsentry.github.io/sentry-javascript/) ## General From 5e6b8527b55da0677fe31a3a5803a6204f354ed2 Mon Sep 17 00:00:00 2001 From: Sigrid Huemer <32902192+s1gr1d@users.noreply.github.com> Date: Mon, 17 Feb 2025 12:38:36 +0100 Subject: [PATCH 07/32] feat(opentelemetry): Add `addLink(s)` to span (#15387) Link spans which are related. Example: ```javascript const span1 = startInactiveSpan({ name: 'span1' }); startSpan({ name: 'span2' }, span2 => { span2.addLink({ context: span1.spanContext(), attributes: { 'sentry.link.type': 'previous_trace' }, }); ``` --- .size-limit.js | 2 +- .../linking/scenario-addLink-nested.ts | 33 ++++ .../tracing/linking/scenario-addLink.ts | 20 +++ .../linking/scenario-addLinks-nested.ts | 31 ++++ .../tracing/linking/scenario-addLinks.ts | 26 +++ .../suites/tracing/linking/test.ts | 157 ++++++++++++++++++ packages/core/src/types-hoist/context.ts | 2 + packages/core/src/utils/spanUtils.ts | 4 +- packages/opentelemetry/src/spanExporter.ts | 6 +- .../opentelemetry/test/spanExporter.test.ts | 29 +++- packages/opentelemetry/test/trace.test.ts | 68 ++++++++ 11 files changed, 374 insertions(+), 4 deletions(-) create mode 100644 dev-packages/node-integration-tests/suites/tracing/linking/scenario-addLink-nested.ts create mode 100644 dev-packages/node-integration-tests/suites/tracing/linking/scenario-addLink.ts create mode 100644 dev-packages/node-integration-tests/suites/tracing/linking/scenario-addLinks-nested.ts create mode 100644 dev-packages/node-integration-tests/suites/tracing/linking/scenario-addLinks.ts create mode 100644 dev-packages/node-integration-tests/suites/tracing/linking/test.ts diff --git a/.size-limit.js b/.size-limit.js index c6e86836fd4c..157c1243021e 100644 --- a/.size-limit.js +++ b/.size-limit.js @@ -54,7 +54,7 @@ module.exports = [ path: 'packages/browser/build/npm/esm/index.js', import: createImport('init', 'browserTracingIntegration', 'replayIntegration'), gzip: true, - limit: '68 KB', + limit: '70 KB', modifyWebpackConfig: function (config) { const webpack = require('webpack'); const TerserPlugin = require('terser-webpack-plugin'); diff --git a/dev-packages/node-integration-tests/suites/tracing/linking/scenario-addLink-nested.ts b/dev-packages/node-integration-tests/suites/tracing/linking/scenario-addLink-nested.ts new file mode 100644 index 000000000000..27282ffb2867 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/tracing/linking/scenario-addLink-nested.ts @@ -0,0 +1,33 @@ +import { loggingTransport } from '@sentry-internal/node-integration-tests'; +import * as Sentry from '@sentry/node'; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', + tracesSampleRate: 1.0, + integrations: [], + transport: loggingTransport, +}); + +// 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-integration-tests/suites/tracing/linking/scenario-addLink.ts b/dev-packages/node-integration-tests/suites/tracing/linking/scenario-addLink.ts new file mode 100644 index 000000000000..d00ae669dbd7 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/tracing/linking/scenario-addLink.ts @@ -0,0 +1,20 @@ +import { loggingTransport } from '@sentry-internal/node-integration-tests'; +import * as Sentry from '@sentry/node'; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', + tracesSampleRate: 1.0, + integrations: [], + transport: loggingTransport, +}); + +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-integration-tests/suites/tracing/linking/scenario-addLinks-nested.ts b/dev-packages/node-integration-tests/suites/tracing/linking/scenario-addLinks-nested.ts new file mode 100644 index 000000000000..216beff5c87e --- /dev/null +++ b/dev-packages/node-integration-tests/suites/tracing/linking/scenario-addLinks-nested.ts @@ -0,0 +1,31 @@ +import { loggingTransport } from '@sentry-internal/node-integration-tests'; +import * as Sentry from '@sentry/node'; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', + tracesSampleRate: 1.0, + integrations: [], + transport: loggingTransport, +}); + +// 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-integration-tests/suites/tracing/linking/scenario-addLinks.ts b/dev-packages/node-integration-tests/suites/tracing/linking/scenario-addLinks.ts new file mode 100644 index 000000000000..1ce8a8a34a8f --- /dev/null +++ b/dev-packages/node-integration-tests/suites/tracing/linking/scenario-addLinks.ts @@ -0,0 +1,26 @@ +import { loggingTransport } from '@sentry-internal/node-integration-tests'; +import * as Sentry from '@sentry/node'; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', + tracesSampleRate: 1.0, + integrations: [], + transport: loggingTransport, +}); + +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-integration-tests/suites/tracing/linking/test.ts b/dev-packages/node-integration-tests/suites/tracing/linking/test.ts new file mode 100644 index 000000000000..1c4e518a4f74 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/tracing/linking/test.ts @@ -0,0 +1,157 @@ +import { createRunner } from '../../../utils/runner'; + +describe('span links', () => { + test('should link spans with addLink() in trace context', done => { + let span1_traceId: string, span1_spanId: string; + + 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(done); + }); + + test('should link spans with addLinks() in trace context', done => { + let span1_traceId: string, span1_spanId: string, span2_traceId: string, span2_spanId: string; + + 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(done); + }); + + test('should link spans with addLink() in nested startSpan() calls', done => { + 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(done); + }); + + test('should link spans with addLinks() in nested startSpan() calls', done => { + 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(done); + }); +}); diff --git a/packages/core/src/types-hoist/context.ts b/packages/core/src/types-hoist/context.ts index 60aa60b38868..0ad6eebf6ac3 100644 --- a/packages/core/src/types-hoist/context.ts +++ b/packages/core/src/types-hoist/context.ts @@ -1,4 +1,5 @@ import type { FeatureFlag } from '../featureFlags'; +import type { SpanLinkJSON } from './link'; import type { Primitive } from './misc'; import type { SpanOrigin } from './span'; @@ -106,6 +107,7 @@ export interface TraceContext extends Record { tags?: { [key: string]: Primitive }; trace_id: string; origin?: SpanOrigin; + links?: SpanLinkJSON[]; } export interface CloudResourceContext extends Record { diff --git a/packages/core/src/utils/spanUtils.ts b/packages/core/src/utils/spanUtils.ts index fcf4aa1857e3..d23a08a96808 100644 --- a/packages/core/src/utils/spanUtils.ts +++ b/packages/core/src/utils/spanUtils.ts @@ -144,7 +144,7 @@ export function spanToJSON(span: Span): SpanJSON { // Handle a span from @opentelemetry/sdk-base-trace's `Span` class if (spanIsOpenTelemetrySdkTraceBaseSpan(span)) { - const { attributes, startTime, name, endTime, parentSpanId, status } = span; + const { attributes, startTime, name, endTime, parentSpanId, status, links } = span; return dropUndefinedKeys({ span_id, @@ -158,6 +158,7 @@ export function spanToJSON(span: Span): SpanJSON { status: getStatusMessage(status), op: attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP], origin: attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN] as SpanOrigin | undefined, + links: convertSpanLinksForEnvelope(links), }); } @@ -184,6 +185,7 @@ export interface OpenTelemetrySdkTraceBaseSpan extends Span { status: SpanStatus; endTime: SpanTimeInput; parentSpanId?: string; + links?: SpanLink[]; } /** diff --git a/packages/opentelemetry/src/spanExporter.ts b/packages/opentelemetry/src/spanExporter.ts index c6a838a5574f..1c88afea0f51 100644 --- a/packages/opentelemetry/src/spanExporter.ts +++ b/packages/opentelemetry/src/spanExporter.ts @@ -11,6 +11,7 @@ import type { TransactionEvent, TransactionSource, } from '@sentry/core'; +import { convertSpanLinksForEnvelope } from '@sentry/core'; import { SEMANTIC_ATTRIBUTE_SENTRY_CUSTOM_SPAN_NAME, SEMANTIC_ATTRIBUTE_SENTRY_OP, @@ -247,6 +248,7 @@ export function createTransactionForOtelSpan(span: ReadableSpan): TransactionEve ...removeSentryAttributes(span.attributes), }); + const { links } = span; const { traceId: trace_id, spanId: span_id } = span.spanContext(); // If parentSpanIdFromTraceState is defined at all, we want it to take precedence @@ -266,6 +268,7 @@ export function createTransactionForOtelSpan(span: ReadableSpan): TransactionEve origin, op, status: getStatusMessage(status), // As per protocol, span status is allowed to be undefined + links: convertSpanLinksForEnvelope(links), }); const statusCode = attributes[ATTR_HTTP_RESPONSE_STATUS_CODE]; @@ -322,7 +325,7 @@ function createAndFinishSpanForOtelSpan(node: SpanNode, spans: SpanJSON[], sentS const span_id = span.spanContext().spanId; const trace_id = span.spanContext().traceId; - const { attributes, startTime, endTime, parentSpanId } = span; + const { attributes, startTime, endTime, parentSpanId, links } = span; const { op, description, data, origin = 'manual' } = getSpanData(span); const allData = dropUndefinedKeys({ @@ -347,6 +350,7 @@ function createAndFinishSpanForOtelSpan(node: SpanNode, spans: SpanJSON[], sentS op, origin, measurements: timedEventsToMeasurements(span.events), + links: convertSpanLinksForEnvelope(links), }); spans.push(spanJSON); diff --git a/packages/opentelemetry/test/spanExporter.test.ts b/packages/opentelemetry/test/spanExporter.test.ts index 48ab8da060de..19714c2b172f 100644 --- a/packages/opentelemetry/test/spanExporter.test.ts +++ b/packages/opentelemetry/test/spanExporter.test.ts @@ -1,5 +1,5 @@ import { ATTR_HTTP_RESPONSE_STATUS_CODE } from '@opentelemetry/semantic-conventions'; -import { SDK_VERSION, SEMANTIC_ATTRIBUTE_SENTRY_OP, startInactiveSpan } from '@sentry/core'; +import { SDK_VERSION, SEMANTIC_ATTRIBUTE_SENTRY_OP, startInactiveSpan, startSpanManual } from '@sentry/core'; import { createTransactionForOtelSpan } from '../src/spanExporter'; import { cleanupOtel, mockSdkInit } from './helpers/mockSdkInit'; @@ -108,4 +108,31 @@ describe('createTransactionForOtelSpan', () => { transaction_info: { source: 'custom' }, }); }); + + it('adds span link to the trace context when adding with addLink()', () => { + const span = startInactiveSpan({ name: 'parent1' }); + span.end(); + + startSpanManual({ name: 'rootSpan' }, rootSpan => { + rootSpan.addLink({ context: span.spanContext(), attributes: { 'sentry.link.type': 'previous_trace' } }); + rootSpan.end(); + + const prevTraceId = span.spanContext().traceId; + const prevSpanId = span.spanContext().spanId; + const event = createTransactionForOtelSpan(rootSpan as any); + + expect(event.contexts?.trace).toEqual( + expect.objectContaining({ + links: [ + expect.objectContaining({ + attributes: { 'sentry.link.type': 'previous_trace' }, + sampled: true, + trace_id: expect.stringMatching(prevTraceId), + span_id: expect.stringMatching(prevSpanId), + }), + ], + }), + ); + }); + }); }); diff --git a/packages/opentelemetry/test/trace.test.ts b/packages/opentelemetry/test/trace.test.ts index 184b93b1e71b..ba1adbb74031 100644 --- a/packages/opentelemetry/test/trace.test.ts +++ b/packages/opentelemetry/test/trace.test.ts @@ -356,6 +356,40 @@ describe('trace', () => { }); }); + it('allows to pass span links', () => { + const rawSpan1 = startInactiveSpan({ name: 'pageload_span' }); + + // @ts-expect-error links exists on span + expect(rawSpan1?.links).toEqual([]); + + const span1JSON = spanToJSON(rawSpan1); + + startSpan({ name: '/users/:id' }, rawSpan2 => { + rawSpan2.addLink({ + context: rawSpan1.spanContext(), + attributes: { + 'sentry.link.type': 'previous_trace', + }, + }); + + const span2LinkJSON = spanToJSON(rawSpan2).links?.[0]; + + expect(span2LinkJSON?.attributes?.['sentry.link.type']).toBe('previous_trace'); + + // @ts-expect-error links and _spanContext exist on span + expect(rawSpan2?.links?.[0].context.traceId).toEqual(rawSpan1._spanContext.traceId); + // @ts-expect-error links and _spanContext exist on span + expect(rawSpan2?.links?.[0].context.traceId).toEqual(span1JSON.trace_id); + expect(span2LinkJSON?.trace_id).toBe(span1JSON.trace_id); + + // @ts-expect-error links and _spanContext exist on span + expect(rawSpan2?.links?.[0].context.spanId).toEqual(rawSpan1?._spanContext.spanId); + // @ts-expect-error links and _spanContext exist on span + expect(rawSpan2?.links?.[0].context.spanId).toEqual(span1JSON.span_id); + expect(span2LinkJSON?.span_id).toBe(span1JSON.span_id); + }); + }); + it('allows to force a transaction with forceTransaction=true', async () => { const client = getClient()!; const transactionEvents: Event[] = []; @@ -906,6 +940,40 @@ describe('trace', () => { }); }); + it('allows to pass span links', () => { + const rawSpan1 = startInactiveSpan({ name: 'pageload_span' }); + + // @ts-expect-error links exists on span + expect(rawSpan1?.links).toEqual([]); + + const span1JSON = spanToJSON(rawSpan1); + + startSpanManual({ name: '/users/:id' }, rawSpan2 => { + rawSpan2.addLink({ + context: rawSpan1.spanContext(), + attributes: { + 'sentry.link.type': 'previous_trace', + }, + }); + + const span2LinkJSON = spanToJSON(rawSpan2).links?.[0]; + + expect(span2LinkJSON?.attributes?.['sentry.link.type']).toBe('previous_trace'); + + // @ts-expect-error links and _spanContext exist on span + expect(rawSpan2?.links?.[0].context.traceId).toEqual(rawSpan1._spanContext.traceId); + // @ts-expect-error links and _spanContext exist on span + expect(rawSpan2?.links?.[0].context.traceId).toEqual(span1JSON.trace_id); + expect(span2LinkJSON?.trace_id).toBe(span1JSON.trace_id); + + // @ts-expect-error links and _spanContext exist on span + expect(rawSpan2?.links?.[0].context.spanId).toEqual(rawSpan1?._spanContext.spanId); + // @ts-expect-error links and _spanContext exist on span + expect(rawSpan2?.links?.[0].context.spanId).toEqual(span1JSON.span_id); + expect(span2LinkJSON?.span_id).toBe(span1JSON.span_id); + }); + }); + it('allows to force a transaction with forceTransaction=true', async () => { const client = getClient()!; const transactionEvents: Event[] = []; From 6e6f85b4f57f304bb9cbc5bdea129471fdc24db1 Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Mon, 17 Feb 2025 15:42:52 +0100 Subject: [PATCH 08/32] fix(core): Ensure `http.client` span descriptions don't contain query params or fragments (#15404) This patch fixes an oversight with our `fetch` instrumentation in the core package and the browser XHR instrumentation. We didn't strip query params and URL hash fragments from the span name (description) of `http.client` spans. With this fix, the span description now only contains the URL protocol, host and path as defined in our [develop specification](https://develop.sentry.dev/sdk/expected-features/data-handling/#spans). --- .../fetch-strip-query-and-fragment/init.js | 11 ++ .../fetch-strip-query-and-fragment/subject.js | 19 ++ .../template.html | 12 ++ .../fetch-strip-query-and-fragment/test.ts | 176 ++++++++++++++++++ .../xhr-strip-query-and-fragment/init.js | 11 ++ .../xhr-strip-query-and-fragment/subject.js | 29 +++ .../template.html | 12 ++ .../xhr-strip-query-and-fragment/test.ts | 172 +++++++++++++++++ .../node-integration-tests/package.json | 1 + .../http-client-spans/fetch-basic/scenario.ts | 15 ++ .../http-client-spans/fetch-basic/test.ts | 48 +++++ .../fetch-strip-query/scenario.ts | 14 ++ .../fetch-strip-query/test.ts | 53 ++++++ .../http-basic}/scenario.ts | 0 .../http-basic}/test.ts | 6 +- .../http-strip-query/scenario.ts | 31 +++ .../http-strip-query/test.ts | 53 ++++++ packages/browser/src/tracing/request.ts | 23 ++- .../bun/test/integrations/bunserver.test.ts | 19 +- packages/core/src/fetch.ts | 16 +- packages/core/test/utils-hoist/url.test.ts | 123 ++++++++++++ 21 files changed, 821 insertions(+), 23 deletions(-) create mode 100644 dev-packages/browser-integration-tests/suites/tracing/request/fetch-strip-query-and-fragment/init.js create mode 100644 dev-packages/browser-integration-tests/suites/tracing/request/fetch-strip-query-and-fragment/subject.js create mode 100644 dev-packages/browser-integration-tests/suites/tracing/request/fetch-strip-query-and-fragment/template.html create mode 100644 dev-packages/browser-integration-tests/suites/tracing/request/fetch-strip-query-and-fragment/test.ts create mode 100644 dev-packages/browser-integration-tests/suites/tracing/request/xhr-strip-query-and-fragment/init.js create mode 100644 dev-packages/browser-integration-tests/suites/tracing/request/xhr-strip-query-and-fragment/subject.js create mode 100644 dev-packages/browser-integration-tests/suites/tracing/request/xhr-strip-query-and-fragment/template.html create mode 100644 dev-packages/browser-integration-tests/suites/tracing/request/xhr-strip-query-and-fragment/test.ts create mode 100644 dev-packages/node-integration-tests/suites/tracing/http-client-spans/fetch-basic/scenario.ts create mode 100644 dev-packages/node-integration-tests/suites/tracing/http-client-spans/fetch-basic/test.ts create mode 100644 dev-packages/node-integration-tests/suites/tracing/http-client-spans/fetch-strip-query/scenario.ts create mode 100644 dev-packages/node-integration-tests/suites/tracing/http-client-spans/fetch-strip-query/test.ts rename dev-packages/node-integration-tests/suites/tracing/{spans => http-client-spans/http-basic}/scenario.ts (100%) rename dev-packages/node-integration-tests/suites/tracing/{spans => http-client-spans/http-basic}/test.ts (87%) create mode 100644 dev-packages/node-integration-tests/suites/tracing/http-client-spans/http-strip-query/scenario.ts create mode 100644 dev-packages/node-integration-tests/suites/tracing/http-client-spans/http-strip-query/test.ts diff --git a/dev-packages/browser-integration-tests/suites/tracing/request/fetch-strip-query-and-fragment/init.js b/dev-packages/browser-integration-tests/suites/tracing/request/fetch-strip-query-and-fragment/init.js new file mode 100644 index 000000000000..de6b87574482 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/request/fetch-strip-query-and-fragment/init.js @@ -0,0 +1,11 @@ +import * as Sentry from '@sentry/browser'; + +window.Sentry = Sentry; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + integrations: [Sentry.browserTracingIntegration({ instrumentPageLoad: false, instrumentNavigation: false })], + tracePropagationTargets: ['http://sentry-test-site.example'], + tracesSampleRate: 1, + autoSessionTracking: false, +}); diff --git a/dev-packages/browser-integration-tests/suites/tracing/request/fetch-strip-query-and-fragment/subject.js b/dev-packages/browser-integration-tests/suites/tracing/request/fetch-strip-query-and-fragment/subject.js new file mode 100644 index 000000000000..37441bf4463a --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/request/fetch-strip-query-and-fragment/subject.js @@ -0,0 +1,19 @@ +function withRootSpan(cb) { + return Sentry.startSpan({ name: 'rootSpan' }, cb); +} + +document.getElementById('btnQuery').addEventListener('click', async () => { + await withRootSpan(() => fetch('http://sentry-test-site.example/0?id=123;page=5')); +}); + +document.getElementById('btnFragment').addEventListener('click', async () => { + await withRootSpan(() => fetch('http://sentry-test-site.example/1#fragment')); +}); + +document.getElementById('btnQueryFragment').addEventListener('click', async () => { + await withRootSpan(() => fetch('http://sentry-test-site.example/2?id=1#fragment')); +}); + +document.getElementById('btnQueryFragmentSameOrigin').addEventListener('click', async () => { + await withRootSpan(() => fetch('/api/users?id=1#fragment')); +}); diff --git a/dev-packages/browser-integration-tests/suites/tracing/request/fetch-strip-query-and-fragment/template.html b/dev-packages/browser-integration-tests/suites/tracing/request/fetch-strip-query-and-fragment/template.html new file mode 100644 index 000000000000..d02fa0868f56 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/request/fetch-strip-query-and-fragment/template.html @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/dev-packages/browser-integration-tests/suites/tracing/request/fetch-strip-query-and-fragment/test.ts b/dev-packages/browser-integration-tests/suites/tracing/request/fetch-strip-query-and-fragment/test.ts new file mode 100644 index 000000000000..a0fea6e6af29 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/request/fetch-strip-query-and-fragment/test.ts @@ -0,0 +1,176 @@ +import { expect } from '@playwright/test'; +import { sentryTest } from '../../../../utils/fixtures'; +import { envelopeRequestParser, shouldSkipTracingTest, waitForTransactionRequest } from '../../../../utils/helpers'; + +sentryTest('strips query params in fetch request spans', async ({ getLocalTestUrl, page }) => { + if (shouldSkipTracingTest()) { + sentryTest.skip(); + } + + await page.route('http://sentry-test-site.example/*', route => route.fulfill({ body: 'ok' })); + + const url = await getLocalTestUrl({ testDir: __dirname }); + + await page.goto(url); + + const txnPromise = waitForTransactionRequest(page); + await page.locator('#btnQuery').click(); + const transactionEvent = envelopeRequestParser(await txnPromise); + + expect(transactionEvent.transaction).toEqual('rootSpan'); + + const requestSpan = transactionEvent.spans?.find(({ op }) => op === 'http.client'); + + expect(requestSpan).toMatchObject({ + description: 'GET http://sentry-test-site.example/0', + parent_span_id: transactionEvent.contexts?.trace?.span_id, + span_id: expect.stringMatching(/[a-f0-9]{16}/), + start_timestamp: expect.any(Number), + timestamp: expect.any(Number), + trace_id: transactionEvent.contexts?.trace?.trace_id, + data: expect.objectContaining({ + 'http.method': 'GET', + 'http.url': 'http://sentry-test-site.example/0?id=123;page=5', + 'http.query': '?id=123;page=5', + 'http.response.status_code': 200, + 'http.response_content_length': 2, + 'sentry.op': 'http.client', + 'sentry.origin': 'auto.http.browser', + type: 'fetch', + 'server.address': 'sentry-test-site.example', + url: 'http://sentry-test-site.example/0?id=123;page=5', + }), + }); + + expect(requestSpan?.data).not.toHaveProperty('http.fragment'); +}); + +sentryTest('strips hash fragment in fetch request spans', async ({ getLocalTestUrl, page }) => { + if (shouldSkipTracingTest()) { + sentryTest.skip(); + } + + await page.route('http://sentry-test-site.example/*', route => route.fulfill({ body: 'ok' })); + + const url = await getLocalTestUrl({ testDir: __dirname }); + + await page.goto(url); + + const txnPromise = waitForTransactionRequest(page); + await page.locator('#btnFragment').click(); + const transactionEvent = envelopeRequestParser(await txnPromise); + + expect(transactionEvent.transaction).toEqual('rootSpan'); + + const requestSpan = transactionEvent.spans?.find(({ op }) => op === 'http.client'); + + expect(requestSpan).toMatchObject({ + description: 'GET http://sentry-test-site.example/1', + parent_span_id: transactionEvent.contexts?.trace?.span_id, + span_id: expect.stringMatching(/[a-f0-9]{16}/), + start_timestamp: expect.any(Number), + timestamp: expect.any(Number), + trace_id: transactionEvent.contexts?.trace?.trace_id, + data: expect.objectContaining({ + 'http.method': 'GET', + 'http.url': 'http://sentry-test-site.example/1#fragment', + 'http.fragment': '#fragment', + 'http.response.status_code': 200, + 'http.response_content_length': 2, + 'sentry.op': 'http.client', + 'sentry.origin': 'auto.http.browser', + type: 'fetch', + 'server.address': 'sentry-test-site.example', + url: 'http://sentry-test-site.example/1#fragment', + }), + }); + + expect(requestSpan?.data).not.toHaveProperty('http.query'); +}); + +sentryTest('strips hash fragment and query params in fetch request spans', async ({ getLocalTestUrl, page }) => { + if (shouldSkipTracingTest()) { + sentryTest.skip(); + } + + await page.route('http://sentry-test-site.example/*', route => route.fulfill({ body: 'ok' })); + + const url = await getLocalTestUrl({ testDir: __dirname }); + + await page.goto(url); + + const txnPromise = waitForTransactionRequest(page); + await page.locator('#btnQueryFragment').click(); + const transactionEvent = envelopeRequestParser(await txnPromise); + + expect(transactionEvent.transaction).toEqual('rootSpan'); + + const requestSpan = transactionEvent.spans?.find(({ op }) => op === 'http.client'); + + expect(requestSpan).toMatchObject({ + description: 'GET http://sentry-test-site.example/2', + parent_span_id: transactionEvent.contexts?.trace?.span_id, + span_id: expect.stringMatching(/[a-f0-9]{16}/), + start_timestamp: expect.any(Number), + timestamp: expect.any(Number), + trace_id: transactionEvent.contexts?.trace?.trace_id, + data: expect.objectContaining({ + 'http.method': 'GET', + 'http.url': 'http://sentry-test-site.example/2?id=1#fragment', + 'http.query': '?id=1', + 'http.fragment': '#fragment', + 'http.response.status_code': 200, + 'http.response_content_length': 2, + 'sentry.op': 'http.client', + 'sentry.origin': 'auto.http.browser', + type: 'fetch', + 'server.address': 'sentry-test-site.example', + url: 'http://sentry-test-site.example/2?id=1#fragment', + }), + }); +}); + +sentryTest( + 'strips hash fragment and query params in same-origin fetch request spans', + async ({ getLocalTestUrl, page }) => { + if (shouldSkipTracingTest()) { + sentryTest.skip(); + } + + await page.route('**/*', route => route.fulfill({ body: 'ok' })); + + const url = await getLocalTestUrl({ testDir: __dirname }); + + await page.goto(url); + + const txnPromise = waitForTransactionRequest(page); + await page.locator('#btnQueryFragmentSameOrigin').click(); + const transactionEvent = envelopeRequestParser(await txnPromise); + + expect(transactionEvent.transaction).toEqual('rootSpan'); + + const requestSpan = transactionEvent.spans?.find(({ op }) => op === 'http.client'); + + expect(requestSpan).toMatchObject({ + description: 'GET /api/users', + parent_span_id: transactionEvent.contexts?.trace?.span_id, + span_id: expect.stringMatching(/[a-f0-9]{16}/), + start_timestamp: expect.any(Number), + timestamp: expect.any(Number), + trace_id: transactionEvent.contexts?.trace?.trace_id, + data: expect.objectContaining({ + 'http.method': 'GET', + 'http.url': 'http://sentry-test.io/api/users?id=1#fragment', + 'http.query': '?id=1', + 'http.fragment': '#fragment', + 'http.response.status_code': 200, + 'http.response_content_length': 2, + 'sentry.op': 'http.client', + 'sentry.origin': 'auto.http.browser', + type: 'fetch', + 'server.address': 'sentry-test.io', + url: '/api/users?id=1#fragment', + }), + }); + }, +); diff --git a/dev-packages/browser-integration-tests/suites/tracing/request/xhr-strip-query-and-fragment/init.js b/dev-packages/browser-integration-tests/suites/tracing/request/xhr-strip-query-and-fragment/init.js new file mode 100644 index 000000000000..de6b87574482 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/request/xhr-strip-query-and-fragment/init.js @@ -0,0 +1,11 @@ +import * as Sentry from '@sentry/browser'; + +window.Sentry = Sentry; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + integrations: [Sentry.browserTracingIntegration({ instrumentPageLoad: false, instrumentNavigation: false })], + tracePropagationTargets: ['http://sentry-test-site.example'], + tracesSampleRate: 1, + autoSessionTracking: false, +}); diff --git a/dev-packages/browser-integration-tests/suites/tracing/request/xhr-strip-query-and-fragment/subject.js b/dev-packages/browser-integration-tests/suites/tracing/request/xhr-strip-query-and-fragment/subject.js new file mode 100644 index 000000000000..e27c6d3cf013 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/request/xhr-strip-query-and-fragment/subject.js @@ -0,0 +1,29 @@ +function withRootSpan(cb) { + return Sentry.startSpan({ name: 'rootSpan' }, cb); +} + +function makeXHRRequest(url) { + return new Promise((resolve, reject) => { + const xhr = new XMLHttpRequest(); + xhr.open('GET', url); + xhr.onload = () => resolve(xhr.responseText); + xhr.onerror = () => reject(xhr.statusText); + xhr.send(); + }); +} + +document.getElementById('btnQuery').addEventListener('click', async () => { + await withRootSpan(() => makeXHRRequest('http://sentry-test-site.example/0?id=123;page=5')); +}); + +document.getElementById('btnFragment').addEventListener('click', async () => { + await withRootSpan(() => makeXHRRequest('http://sentry-test-site.example/1#fragment')); +}); + +document.getElementById('btnQueryFragment').addEventListener('click', async () => { + await withRootSpan(() => makeXHRRequest('http://sentry-test-site.example/2?id=1#fragment')); +}); + +document.getElementById('btnQueryFragmentSameOrigin').addEventListener('click', async () => { + await withRootSpan(() => makeXHRRequest('/api/users?id=1#fragment')); +}); diff --git a/dev-packages/browser-integration-tests/suites/tracing/request/xhr-strip-query-and-fragment/template.html b/dev-packages/browser-integration-tests/suites/tracing/request/xhr-strip-query-and-fragment/template.html new file mode 100644 index 000000000000..533636f821c3 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/request/xhr-strip-query-and-fragment/template.html @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/dev-packages/browser-integration-tests/suites/tracing/request/xhr-strip-query-and-fragment/test.ts b/dev-packages/browser-integration-tests/suites/tracing/request/xhr-strip-query-and-fragment/test.ts new file mode 100644 index 000000000000..d4ed06fcdd4e --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/request/xhr-strip-query-and-fragment/test.ts @@ -0,0 +1,172 @@ +import { expect } from '@playwright/test'; +import { sentryTest } from '../../../../utils/fixtures'; +import { envelopeRequestParser, shouldSkipTracingTest, waitForTransactionRequest } from '../../../../utils/helpers'; + +sentryTest('strips query params in XHR request spans', async ({ getLocalTestUrl, page }) => { + if (shouldSkipTracingTest()) { + sentryTest.skip(); + } + + await page.route('http://sentry-test-site.example/*', route => route.fulfill({ body: 'ok' })); + + const url = await getLocalTestUrl({ testDir: __dirname }); + + await page.goto(url); + + const txnPromise = waitForTransactionRequest(page); + await page.locator('#btnQuery').click(); + const transactionEvent = envelopeRequestParser(await txnPromise); + + expect(transactionEvent.transaction).toEqual('rootSpan'); + + const requestSpan = transactionEvent.spans?.find(({ op }) => op === 'http.client'); + + expect(requestSpan).toMatchObject({ + description: 'GET http://sentry-test-site.example/0', + parent_span_id: transactionEvent.contexts?.trace?.span_id, + span_id: expect.stringMatching(/[a-f0-9]{16}/), + start_timestamp: expect.any(Number), + timestamp: expect.any(Number), + trace_id: transactionEvent.contexts?.trace?.trace_id, + data: expect.objectContaining({ + 'http.method': 'GET', + 'http.url': 'http://sentry-test-site.example/0?id=123;page=5', + 'http.query': '?id=123;page=5', + 'http.response.status_code': 200, + 'sentry.op': 'http.client', + 'sentry.origin': 'auto.http.browser', + type: 'xhr', + 'server.address': 'sentry-test-site.example', + url: 'http://sentry-test-site.example/0?id=123;page=5', + }), + }); + + expect(requestSpan?.data).not.toHaveProperty('http.fragment'); +}); + +sentryTest('strips hash fragment in XHR request spans', async ({ getLocalTestUrl, page }) => { + if (shouldSkipTracingTest()) { + sentryTest.skip(); + } + + await page.route('http://sentry-test-site.example/*', route => route.fulfill({ body: 'ok' })); + + const url = await getLocalTestUrl({ testDir: __dirname }); + + await page.goto(url); + + const txnPromise = waitForTransactionRequest(page); + await page.locator('#btnFragment').click(); + const transactionEvent = envelopeRequestParser(await txnPromise); + + expect(transactionEvent.transaction).toEqual('rootSpan'); + + const requestSpan = transactionEvent.spans?.find(({ op }) => op === 'http.client'); + + expect(requestSpan).toMatchObject({ + description: 'GET http://sentry-test-site.example/1', + parent_span_id: transactionEvent.contexts?.trace?.span_id, + span_id: expect.stringMatching(/[a-f0-9]{16}/), + start_timestamp: expect.any(Number), + timestamp: expect.any(Number), + trace_id: transactionEvent.contexts?.trace?.trace_id, + data: expect.objectContaining({ + 'http.method': 'GET', + 'http.url': 'http://sentry-test-site.example/1#fragment', + 'http.fragment': '#fragment', + 'http.response.status_code': 200, + 'sentry.op': 'http.client', + 'sentry.origin': 'auto.http.browser', + type: 'xhr', + 'server.address': 'sentry-test-site.example', + url: 'http://sentry-test-site.example/1#fragment', + }), + }); + + expect(requestSpan?.data).not.toHaveProperty('http.query'); +}); + +sentryTest('strips hash fragment and query params in XHR request spans', async ({ getLocalTestUrl, page }) => { + if (shouldSkipTracingTest()) { + sentryTest.skip(); + } + + await page.route('http://sentry-test-site.example/*', route => route.fulfill({ body: 'ok' })); + + const url = await getLocalTestUrl({ testDir: __dirname }); + + await page.goto(url); + + const txnPromise = waitForTransactionRequest(page); + await page.locator('#btnQueryFragment').click(); + const transactionEvent = envelopeRequestParser(await txnPromise); + + expect(transactionEvent.transaction).toEqual('rootSpan'); + + const requestSpan = transactionEvent.spans?.find(({ op }) => op === 'http.client'); + + expect(requestSpan).toMatchObject({ + description: 'GET http://sentry-test-site.example/2', + parent_span_id: transactionEvent.contexts?.trace?.span_id, + span_id: expect.stringMatching(/[a-f0-9]{16}/), + start_timestamp: expect.any(Number), + timestamp: expect.any(Number), + trace_id: transactionEvent.contexts?.trace?.trace_id, + data: expect.objectContaining({ + 'http.method': 'GET', + 'http.url': 'http://sentry-test-site.example/2?id=1#fragment', + 'http.query': '?id=1', + 'http.fragment': '#fragment', + 'http.response.status_code': 200, + 'sentry.op': 'http.client', + 'sentry.origin': 'auto.http.browser', + type: 'xhr', + 'server.address': 'sentry-test-site.example', + url: 'http://sentry-test-site.example/2?id=1#fragment', + }), + }); +}); + +sentryTest( + 'strips hash fragment and query params in same-origin XHR request spans', + async ({ getLocalTestUrl, page }) => { + if (shouldSkipTracingTest()) { + sentryTest.skip(); + } + + await page.route('**/*', route => route.fulfill({ body: 'ok' })); + + const url = await getLocalTestUrl({ testDir: __dirname }); + + await page.goto(url); + + const txnPromise = waitForTransactionRequest(page); + await page.locator('#btnQueryFragmentSameOrigin').click(); + const transactionEvent = envelopeRequestParser(await txnPromise); + + expect(transactionEvent.transaction).toEqual('rootSpan'); + + const requestSpan = transactionEvent.spans?.find(({ op }) => op === 'http.client'); + + expect(requestSpan).toMatchObject({ + description: 'GET /api/users', + parent_span_id: transactionEvent.contexts?.trace?.span_id, + span_id: expect.stringMatching(/[a-f0-9]{16}/), + start_timestamp: expect.any(Number), + timestamp: expect.any(Number), + trace_id: transactionEvent.contexts?.trace?.trace_id, + data: expect.objectContaining({ + 'http.method': 'GET', + 'http.url': 'http://sentry-test.io/api/users?id=1#fragment', + 'http.query': '?id=1', + 'http.fragment': '#fragment', + 'http.response.status_code': 200, + 'sentry.op': 'http.client', + 'sentry.origin': 'auto.http.browser', + type: 'xhr', + 'server.address': 'sentry-test.io', + url: '/api/users?id=1#fragment', + }), + }); + }, +); diff --git a/dev-packages/node-integration-tests/package.json b/dev-packages/node-integration-tests/package.json index e123e852f4b9..9d89dea67940 100644 --- a/dev-packages/node-integration-tests/package.json +++ b/dev-packages/node-integration-tests/package.json @@ -20,6 +20,7 @@ "fix": "eslint . --format stylish --fix", "type-check": "tsc", "test": "jest --config ./jest.config.js", + "test:no-prisma": "jest --config ./jest.config.js", "test:watch": "yarn test --watch" }, "dependencies": { diff --git a/dev-packages/node-integration-tests/suites/tracing/http-client-spans/fetch-basic/scenario.ts b/dev-packages/node-integration-tests/suites/tracing/http-client-spans/fetch-basic/scenario.ts new file mode 100644 index 000000000000..44ea548bab8f --- /dev/null +++ b/dev-packages/node-integration-tests/suites/tracing/http-client-spans/fetch-basic/scenario.ts @@ -0,0 +1,15 @@ +import { loggingTransport } from '@sentry-internal/node-integration-tests'; +import * as Sentry from '@sentry/node'; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', + tracesSampleRate: 1.0, + transport: loggingTransport, +}); + +// eslint-disable-next-line @typescript-eslint/no-floating-promises +Sentry.startSpan({ name: 'test_transaction' }, async () => { + await fetch(`${process.env.SERVER_URL}/api/v0`); + await fetch(`${process.env.SERVER_URL}/api/v1`); +}); diff --git a/dev-packages/node-integration-tests/suites/tracing/http-client-spans/fetch-basic/test.ts b/dev-packages/node-integration-tests/suites/tracing/http-client-spans/fetch-basic/test.ts new file mode 100644 index 000000000000..006190864fe6 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/tracing/http-client-spans/fetch-basic/test.ts @@ -0,0 +1,48 @@ +import { createRunner } from '../../../../utils/runner'; +import { createTestServer } from '../../../../utils/server'; + +test('captures spans for outgoing fetch requests', done => { + expect.assertions(3); + + createTestServer(done) + .get('/api/v0', () => { + // Just ensure we're called + expect(true).toBe(true); + }) + .get( + '/api/v1', + () => { + // Just ensure we're called + expect(true).toBe(true); + }, + 404, + ) + .start() + .then(([SERVER_URL, closeTestServer]) => { + createRunner(__dirname, 'scenario.ts') + .withEnv({ SERVER_URL }) + .expect({ + transaction: { + transaction: 'test_transaction', + spans: expect.arrayContaining([ + expect.objectContaining({ + description: expect.stringMatching(/GET .*\/api\/v0/), + op: 'http.client', + origin: 'auto.http.otel.node_fetch', + status: 'ok', + }), + expect.objectContaining({ + description: expect.stringMatching(/GET .*\/api\/v1/), + op: 'http.client', + origin: 'auto.http.otel.node_fetch', + status: 'not_found', + data: expect.objectContaining({ + 'http.response.status_code': 404, + }), + }), + ]), + }, + }) + .start(closeTestServer); + }); +}); diff --git a/dev-packages/node-integration-tests/suites/tracing/http-client-spans/fetch-strip-query/scenario.ts b/dev-packages/node-integration-tests/suites/tracing/http-client-spans/fetch-strip-query/scenario.ts new file mode 100644 index 000000000000..0c72d545c39b --- /dev/null +++ b/dev-packages/node-integration-tests/suites/tracing/http-client-spans/fetch-strip-query/scenario.ts @@ -0,0 +1,14 @@ +import { loggingTransport } from '@sentry-internal/node-integration-tests'; +import * as Sentry from '@sentry/node'; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', + tracesSampleRate: 1.0, + transport: loggingTransport, +}); + +// eslint-disable-next-line @typescript-eslint/no-floating-promises +Sentry.startSpan({ name: 'test_transaction' }, async () => { + await fetch(`${process.env.SERVER_URL}/api/v0/users?id=1#fragment`); +}); diff --git a/dev-packages/node-integration-tests/suites/tracing/http-client-spans/fetch-strip-query/test.ts b/dev-packages/node-integration-tests/suites/tracing/http-client-spans/fetch-strip-query/test.ts new file mode 100644 index 000000000000..12bb11727228 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/tracing/http-client-spans/fetch-strip-query/test.ts @@ -0,0 +1,53 @@ +import { createRunner } from '../../../../utils/runner'; +import { createTestServer } from '../../../../utils/server'; + +test('strips and handles query params in spans of outgoing fetch requests', done => { + expect.assertions(4); + + createTestServer(done) + .get('/api/v0/users', () => { + // Just ensure we're called + expect(true).toBe(true); + }) + .start() + .then(([SERVER_URL, closeTestServer]) => { + createRunner(__dirname, 'scenario.ts') + .withEnv({ SERVER_URL }) + .expect({ + transaction: txn => { + expect(txn.transaction).toEqual('test_transaction'); + expect(txn.spans).toHaveLength(1); + expect(txn.spans?.[0]).toMatchObject({ + data: { + url: `${SERVER_URL}/api/v0/users`, + 'url.full': `${SERVER_URL}/api/v0/users?id=1`, + 'url.path': '/api/v0/users', + 'url.query': '?id=1', + 'url.scheme': 'http', + 'http.query': 'id=1', + 'http.request.method': 'GET', + 'http.request.method_original': 'GET', + 'http.response.header.content-length': 0, + 'http.response.status_code': 200, + 'network.peer.address': '::1', + 'network.peer.port': expect.any(Number), + 'otel.kind': 'CLIENT', + 'server.port': expect.any(Number), + 'user_agent.original': 'node', + 'sentry.op': 'http.client', + 'sentry.origin': 'auto.http.otel.node_fetch', + 'server.address': 'localhost', + }, + description: `GET ${SERVER_URL}/api/v0/users`, + op: 'http.client', + origin: 'auto.http.otel.node_fetch', + status: 'ok', + parent_span_id: txn.contexts?.trace?.span_id, + span_id: expect.stringMatching(/[a-f0-9]{16}/), + trace_id: txn.contexts?.trace?.trace_id, + }); + }, + }) + .start(closeTestServer); + }); +}); diff --git a/dev-packages/node-integration-tests/suites/tracing/spans/scenario.ts b/dev-packages/node-integration-tests/suites/tracing/http-client-spans/http-basic/scenario.ts similarity index 100% rename from dev-packages/node-integration-tests/suites/tracing/spans/scenario.ts rename to dev-packages/node-integration-tests/suites/tracing/http-client-spans/http-basic/scenario.ts diff --git a/dev-packages/node-integration-tests/suites/tracing/spans/test.ts b/dev-packages/node-integration-tests/suites/tracing/http-client-spans/http-basic/test.ts similarity index 87% rename from dev-packages/node-integration-tests/suites/tracing/spans/test.ts rename to dev-packages/node-integration-tests/suites/tracing/http-client-spans/http-basic/test.ts index e349622d39f8..bb642baf0e1c 100644 --- a/dev-packages/node-integration-tests/suites/tracing/spans/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/http-client-spans/http-basic/test.ts @@ -1,7 +1,7 @@ -import { createRunner } from '../../../utils/runner'; -import { createTestServer } from '../../../utils/server'; +import { createRunner } from '../../../../utils/runner'; +import { createTestServer } from '../../../../utils/server'; -test('should capture spans for outgoing http requests', done => { +test('captures spans for outgoing http requests', done => { expect.assertions(3); createTestServer(done) diff --git a/dev-packages/node-integration-tests/suites/tracing/http-client-spans/http-strip-query/scenario.ts b/dev-packages/node-integration-tests/suites/tracing/http-client-spans/http-strip-query/scenario.ts new file mode 100644 index 000000000000..074c9778aa75 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/tracing/http-client-spans/http-strip-query/scenario.ts @@ -0,0 +1,31 @@ +import { loggingTransport } from '@sentry-internal/node-integration-tests'; +import * as Sentry from '@sentry/node'; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', + tracesSampleRate: 1.0, + transport: loggingTransport, +}); + +import * as http from 'http'; + +// eslint-disable-next-line @typescript-eslint/no-floating-promises +Sentry.startSpan({ name: 'test_transaction' }, async () => { + await makeHttpRequest(`${process.env.SERVER_URL}/api/v0/users?id=1#fragment`); +}); + +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-integration-tests/suites/tracing/http-client-spans/http-strip-query/test.ts b/dev-packages/node-integration-tests/suites/tracing/http-client-spans/http-strip-query/test.ts new file mode 100644 index 000000000000..37b638635eb9 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/tracing/http-client-spans/http-strip-query/test.ts @@ -0,0 +1,53 @@ +import { createRunner } from '../../../../utils/runner'; +import { createTestServer } from '../../../../utils/server'; + +test('strips and handles query params in spans of outgoing http requests', done => { + expect.assertions(4); + + createTestServer(done) + .get('/api/v0/users', () => { + // Just ensure we're called + expect(true).toBe(true); + }) + .start() + .then(([SERVER_URL, closeTestServer]) => { + createRunner(__dirname, 'scenario.ts') + .withEnv({ SERVER_URL }) + .expect({ + transaction: txn => { + expect(txn.transaction).toEqual('test_transaction'); + expect(txn.spans).toHaveLength(1); + expect(txn.spans?.[0]).toMatchObject({ + data: { + url: `${SERVER_URL}/api/v0/users`, + 'http.url': `${SERVER_URL}/api/v0/users?id=1`, + 'http.target': '/api/v0/users?id=1', + 'http.flavor': '1.1', + 'http.host': expect.stringMatching(/localhost:\d+$/), + 'http.method': 'GET', + 'http.query': 'id=1', + 'http.response.status_code': 200, + 'http.response_content_length_uncompressed': 0, + 'http.status_code': 200, + 'http.status_text': 'OK', + 'net.peer.ip': '::1', + 'net.peer.name': 'localhost', + 'net.peer.port': expect.any(Number), + 'net.transport': 'ip_tcp', + 'otel.kind': 'CLIENT', + 'sentry.op': 'http.client', + 'sentry.origin': 'auto.http.otel.http', + }, + description: `GET ${SERVER_URL}/api/v0/users`, + op: 'http.client', + origin: 'auto.http.otel.http', + status: 'ok', + parent_span_id: txn.contexts?.trace?.span_id, + span_id: expect.stringMatching(/[a-f0-9]{16}/), + trace_id: txn.contexts?.trace?.trace_id, + }); + }, + }) + .start(closeTestServer); + }); +}); diff --git a/packages/browser/src/tracing/request.ts b/packages/browser/src/tracing/request.ts index 144aec73c977..17dd71f0abba 100644 --- a/packages/browser/src/tracing/request.ts +++ b/packages/browser/src/tracing/request.ts @@ -24,6 +24,7 @@ import { spanToJSON, startInactiveSpan, stringMatchesSomePattern, + stripUrlQueryAndFragment, } from '@sentry/core'; import { WINDOW } from '../helpers'; @@ -324,7 +325,9 @@ export function xhrCallback( return undefined; } - const shouldCreateSpanResult = hasSpansEnabled() && shouldCreateSpan(sentryXhrData.url); + const { url, method } = sentryXhrData; + + const shouldCreateSpanResult = hasSpansEnabled() && shouldCreateSpan(url); // check first if the request has finished and is tracked by an existing span which should now end if (handlerData.endTimestamp && shouldCreateSpanResult) { @@ -342,23 +345,27 @@ export function xhrCallback( return undefined; } - const fullUrl = getFullURL(sentryXhrData.url); - const host = fullUrl ? parseUrl(fullUrl).host : undefined; + const fullUrl = getFullURL(url); + const parsedUrl = fullUrl ? parseUrl(fullUrl) : parseUrl(url); + + const urlForSpanName = stripUrlQueryAndFragment(url); const hasParent = !!getActiveSpan(); const span = shouldCreateSpanResult && hasParent ? startInactiveSpan({ - name: `${sentryXhrData.method} ${sentryXhrData.url}`, + name: `${method} ${urlForSpanName}`, attributes: { + url, type: 'xhr', - 'http.method': sentryXhrData.method, + 'http.method': method, 'http.url': fullUrl, - url: sentryXhrData.url, - 'server.address': host, + 'server.address': parsedUrl?.host, [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.browser', [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'http.client', + ...(parsedUrl?.search && { 'http.query': parsedUrl?.search }), + ...(parsedUrl?.hash && { 'http.fragment': parsedUrl?.hash }), }, }) : new SentryNonRecordingSpan(); @@ -366,7 +373,7 @@ export function xhrCallback( xhr.__sentry_xhr_span_id__ = span.spanContext().spanId; spans[xhr.__sentry_xhr_span_id__] = span; - if (shouldAttachHeaders(sentryXhrData.url)) { + if (shouldAttachHeaders(url)) { addTracingHeadersToXhrRequest( xhr, // If performance is disabled (TWP) or there's no active root span (pageload/navigation/interaction), diff --git a/packages/bun/test/integrations/bunserver.test.ts b/packages/bun/test/integrations/bunserver.test.ts index dd1f738a334b..e448402c0479 100644 --- a/packages/bun/test/integrations/bunserver.test.ts +++ b/packages/bun/test/integrations/bunserver.test.ts @@ -41,17 +41,26 @@ describe('Bun Serve Integration', () => { }, port, }); - await fetch(`http://localhost:${port}/`); + await fetch(`http://localhost:${port}/users?id=123`); server.stop(); if (!generatedSpan) { throw 'No span was generated in the test'; } - expect(spanToJSON(generatedSpan).status).toBe('ok'); - expect(spanToJSON(generatedSpan).data?.['http.response.status_code']).toEqual(200); - expect(spanToJSON(generatedSpan).op).toEqual('http.server'); - expect(spanToJSON(generatedSpan).description).toEqual('GET /'); + const spanJson = spanToJSON(generatedSpan); + expect(spanJson.status).toBe('ok'); + expect(spanJson.op).toEqual('http.server'); + expect(spanJson.description).toEqual('GET /users'); + expect(spanJson.data).toEqual({ + 'http.query': '?id=123', + 'http.request.method': 'GET', + 'http.response.status_code': 200, + 'sentry.op': 'http.server', + 'sentry.origin': 'auto.http.bun.serve', + 'sentry.sample_rate': 1, + 'sentry.source': 'url', + }); }); test('generates a post transaction', async () => { diff --git a/packages/core/src/fetch.ts b/packages/core/src/fetch.ts index 3c43584b3951..a96d421d0023 100644 --- a/packages/core/src/fetch.ts +++ b/packages/core/src/fetch.ts @@ -5,7 +5,7 @@ import { SentryNonRecordingSpan } from './tracing/sentryNonRecordingSpan'; import type { FetchBreadcrumbHint, HandlerDataFetch, Span, SpanOrigin } from './types-hoist'; import { SENTRY_BAGGAGE_KEY_PREFIX } from './utils-hoist/baggage'; import { isInstanceOf } from './utils-hoist/is'; -import { parseUrl } from './utils-hoist/url'; +import { parseUrl, stripUrlQueryAndFragment } from './utils-hoist/url'; import { hasSpansEnabled } from './utils/hasSpansEnabled'; import { getActiveSpan } from './utils/spanUtils'; import { getTraceData } from './utils/traceData'; @@ -35,7 +35,9 @@ export function instrumentFetchRequest( return undefined; } - const shouldCreateSpanResult = hasSpansEnabled() && shouldCreateSpan(handlerData.fetchData.url); + const { method, url } = handlerData.fetchData; + + const shouldCreateSpanResult = hasSpansEnabled() && shouldCreateSpan(url); if (handlerData.endTimestamp && shouldCreateSpanResult) { const spanId = handlerData.fetchData.__span; @@ -51,25 +53,25 @@ export function instrumentFetchRequest( return undefined; } - const { method, url } = handlerData.fetchData; - const fullUrl = getFullURL(url); - const host = fullUrl ? parseUrl(fullUrl).host : undefined; + const parsedUrl = fullUrl ? parseUrl(fullUrl) : parseUrl(url); const hasParent = !!getActiveSpan(); const span = shouldCreateSpanResult && hasParent ? startInactiveSpan({ - name: `${method} ${url}`, + name: `${method} ${stripUrlQueryAndFragment(url)}`, attributes: { url, type: 'fetch', 'http.method': method, 'http.url': fullUrl, - 'server.address': host, + 'server.address': parsedUrl?.host, [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: spanOrigin, [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'http.client', + ...(parsedUrl?.search && { 'http.query': parsedUrl?.search }), + ...(parsedUrl?.hash && { 'http.fragment': parsedUrl?.hash }), }, }) : new SentryNonRecordingSpan(); diff --git a/packages/core/test/utils-hoist/url.test.ts b/packages/core/test/utils-hoist/url.test.ts index cd066201945d..a16c72dc1cd2 100644 --- a/packages/core/test/utils-hoist/url.test.ts +++ b/packages/core/test/utils-hoist/url.test.ts @@ -72,3 +72,126 @@ describe('getSanitizedUrlString', () => { expect(getSanitizedUrlString(urlObject)).toEqual(sanitizedURL); }); }); + +describe('parseUrl', () => { + it.each([ + [ + 'https://somedomain.com', + { host: 'somedomain.com', path: '', search: '', hash: '', protocol: 'https', relative: '' }, + ], + [ + 'https://somedomain.com/path/to/happiness', + { + host: 'somedomain.com', + path: '/path/to/happiness', + search: '', + hash: '', + protocol: 'https', + relative: '/path/to/happiness', + }, + ], + [ + 'https://somedomain.com/path/to/happiness?auhtToken=abc123¶m2=bar', + { + host: 'somedomain.com', + path: '/path/to/happiness', + search: '?auhtToken=abc123¶m2=bar', + hash: '', + protocol: 'https', + relative: '/path/to/happiness?auhtToken=abc123¶m2=bar', + }, + ], + [ + 'https://somedomain.com/path/to/happiness?auhtToken=abc123¶m2=bar#wildfragment', + { + host: 'somedomain.com', + path: '/path/to/happiness', + search: '?auhtToken=abc123¶m2=bar', + hash: '#wildfragment', + protocol: 'https', + relative: '/path/to/happiness?auhtToken=abc123¶m2=bar#wildfragment', + }, + ], + [ + 'https://somedomain.com/path/to/happiness#somewildfragment123', + { + host: 'somedomain.com', + path: '/path/to/happiness', + search: '', + hash: '#somewildfragment123', + protocol: 'https', + relative: '/path/to/happiness#somewildfragment123', + }, + ], + [ + 'https://somedomain.com/path/to/happiness#somewildfragment123?auhtToken=abc123¶m2=bar', + { + host: 'somedomain.com', + path: '/path/to/happiness', + search: '', + hash: '#somewildfragment123?auhtToken=abc123¶m2=bar', + protocol: 'https', + relative: '/path/to/happiness#somewildfragment123?auhtToken=abc123¶m2=bar', + }, + ], + [ + // yup, this is a valid URL (protocol-agnostic URL) + '//somedomain.com/path/to/happiness?auhtToken=abc123¶m2=bar#wildfragment', + { + host: 'somedomain.com', + path: '/path/to/happiness', + search: '?auhtToken=abc123¶m2=bar', + hash: '#wildfragment', + protocol: undefined, + relative: '/path/to/happiness?auhtToken=abc123¶m2=bar#wildfragment', + }, + ], + ['', {}], + [ + '\n', + { + hash: '', + host: undefined, + path: '\n', + protocol: undefined, + relative: '\n', + search: '', + }, + ], + [ + 'somerandomString', + { + hash: '', + host: undefined, + path: 'somerandomString', + protocol: undefined, + relative: 'somerandomString', + search: '', + }, + ], + [ + 'somedomain.com', + { + host: undefined, + path: 'somedomain.com', + search: '', + hash: '', + protocol: undefined, + relative: 'somedomain.com', + }, + ], + [ + 'somedomain.com/path/?q=1#fragment', + { + host: undefined, + path: 'somedomain.com/path/', + search: '?q=1', + hash: '#fragment', + protocol: undefined, + relative: 'somedomain.com/path/?q=1#fragment', + }, + ], + ])('returns parsed partial URL object for %s', (url: string, expected: any) => { + expect(parseUrl(url)).toEqual(expected); + }); +}); From abb37a370264b1d1a119aeecb2a81cacbb7c31cc Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 17 Feb 2025 15:55:48 +0100 Subject: [PATCH 09/32] chore(deps): bump esbuild from 0.20.0 to 0.25.0 in /dev-packages/e2e-tests/test-applications/node-profiling-esm (#15366) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [esbuild](https://github.com/evanw/esbuild) from 0.20.0 to 0.25.0.
Release notes

Sourced from esbuild's releases.

v0.25.0

This release deliberately contains backwards-incompatible changes. To avoid automatically picking up releases like this, you should either be pinning the exact version of esbuild in your package.json file (recommended) or be using a version range syntax that only accepts patch upgrades such as ^0.24.0 or ~0.24.0. See npm's documentation about semver for more information.

  • Restrict access to esbuild's development server (GHSA-67mh-4wv8-2f99)

    This change addresses esbuild's first security vulnerability report. Previously esbuild set the Access-Control-Allow-Origin header to * to allow esbuild's development server to be flexible in how it's used for development. However, this allows the websites you visit to make HTTP requests to esbuild's local development server, which gives read-only access to your source code if the website were to fetch your source code's specific URL. You can read more information in the report.

    Starting with this release, CORS will now be disabled, and requests will now be denied if the host does not match the one provided to --serve=. The default host is 0.0.0.0, which refers to all of the IP addresses that represent the local machine (e.g. both 127.0.0.1 and 192.168.0.1). If you want to customize anything about esbuild's development server, you can put a proxy in front of esbuild and modify the incoming and/or outgoing requests.

    In addition, the serve() API call has been changed to return an array of hosts instead of a single host string. This makes it possible to determine all of the hosts that esbuild's development server will accept.

    Thanks to @​sapphi-red for reporting this issue.

  • Delete output files when a build fails in watch mode (#3643)

    It has been requested for esbuild to delete files when a build fails in watch mode. Previously esbuild left the old files in place, which could cause people to not immediately realize that the most recent build failed. With this release, esbuild will now delete all output files if a rebuild fails. Fixing the build error and triggering another rebuild will restore all output files again.

  • Fix correctness issues with the CSS nesting transform (#3620, #3877, #3933, #3997, #4005, #4037, #4038)

    This release fixes the following problems:

    • Naive expansion of CSS nesting can result in an exponential blow-up of generated CSS if each nesting level has multiple selectors. Previously esbuild sometimes collapsed individual nesting levels using :is() to limit expansion. However, this collapsing wasn't correct in some cases, so it has been removed to fix correctness issues.

      /* Original code */
      .parent {
        > .a,
        > .b1 > .b2 {
          color: red;
        }
      }
      

      /* Old output (with --supported:nesting=false) */
      .parent > :is(.a, .b1 > .b2) {
      color: red;
      }

      /* New output (with --supported:nesting=false) */
      .parent > .a,
      .parent > .b1 > .b2 {
      color: red;
      }

      Thanks to @​tim-we for working on a fix.

    • The & CSS nesting selector can be repeated multiple times to increase CSS specificity. Previously esbuild ignored this possibility and incorrectly considered && to have the same specificity as &. With this release, this should now work correctly:

      /* Original code (color should be red) */
      

... (truncated)

Changelog

Sourced from esbuild's changelog.

Changelog: 2024

This changelog documents all esbuild versions published in the year 2024 (versions 0.19.12 through 0.24.2).

0.24.2

  • Fix regression with --define and import.meta (#4010, #4012, #4013)

    The previous change in version 0.24.1 to use a more expression-like parser for define values to allow quoted property names introduced a regression that removed the ability to use --define:import.meta=.... Even though import is normally a keyword that can't be used as an identifier, ES modules special-case the import.meta expression to behave like an identifier anyway. This change fixes the regression.

    This fix was contributed by @​sapphi-red.

0.24.1

  • Allow es2024 as a target in tsconfig.json (#4004)

    TypeScript recently added es2024 as a compilation target, so esbuild now supports this in the target field of tsconfig.json files, such as in the following configuration file:

    {
      "compilerOptions": {
        "target": "ES2024"
      }
    }
    

    As a reminder, the only thing that esbuild uses this field for is determining whether or not to use legacy TypeScript behavior for class fields. You can read more in the documentation.

    This fix was contributed by @​billyjanitsch.

  • Allow automatic semicolon insertion after get/set

    This change fixes a grammar bug in the parser that incorrectly treated the following code as a syntax error:

    class Foo {
      get
      *x() {}
      set
      *y() {}
    }
    

    The above code will be considered valid starting with this release. This change to esbuild follows a similar change to TypeScript which will allow this syntax starting with TypeScript 5.7.

  • Allow quoted property names in --define and --pure (#4008)

    The define and pure API options now accept identifier expressions containing quoted property names. Previously all identifiers in the identifier expression had to be bare identifiers. This change now makes --define and --pure consistent with --global-name, which already supported quoted property names. For example, the following is now possible:

... (truncated)

Commits
  • e9174d6 publish 0.25.0 to npm
  • c27dbeb fix hosts in plugin-tests.js
  • 6794f60 fix hosts in node-unref-tests.js
  • de85afd Merge commit from fork
  • da1de1b fix #4065: bitwise operators can return bigints
  • f4e9d19 switch case liveness: default is always last
  • 7aa47c3 fix #4028: minify live/dead switch cases better
  • 22ecd30 minify: more constant folding for strict equality
  • 4cdf03c fix #4053: reordering of .tsx in node_modules
  • dc71977 fix #3692: 0 now picks a random ephemeral port
  • Additional commits viewable in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=esbuild&package-manager=npm_and_yarn&previous-version=0.20.0&new-version=0.25.0)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself) You can disable automated security fix PRs for this repo from the [Security Alerts page](https://github.com/getsentry/sentry-javascript/network/alerts).
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .../e2e-tests/test-applications/node-profiling-esm/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dev-packages/e2e-tests/test-applications/node-profiling-esm/package.json b/dev-packages/e2e-tests/test-applications/node-profiling-esm/package.json index b633df2df172..78536d6794ad 100644 --- a/dev-packages/e2e-tests/test-applications/node-profiling-esm/package.json +++ b/dev-packages/e2e-tests/test-applications/node-profiling-esm/package.json @@ -13,7 +13,7 @@ "@playwright/test": "~1.50.0", "@sentry/node": "latest || *", "@sentry/profiling-node": "latest || *", - "esbuild": "0.20.0", + "esbuild": "0.25.0", "typescript": "^5.7.3" }, "volta": { From 6363e75a1b1124e4c895d36613eb62a7b806945f Mon Sep 17 00:00:00 2001 From: Sigrid Huemer <32902192+s1gr1d@users.noreply.github.com> Date: Mon, 17 Feb 2025 16:51:02 +0100 Subject: [PATCH 10/32] feat(opentelemetry): Add `links` to span options (#15403) This PR adds the possibility to add a `links` array that can be passed to the span options at creation of the span. --- .../tracing/linking/scenario-span-options.ts | 27 +++++ .../suites/tracing/linking/test.ts | 30 +++++ packages/core/src/types-hoist/span.ts | 6 + .../core/src/types-hoist/startSpanOptions.ts | 7 ++ packages/opentelemetry/src/trace.ts | 3 +- .../opentelemetry/test/spanExporter.test.ts | 30 +++++ packages/opentelemetry/test/trace.test.ts | 114 ++++++++++++++++++ 7 files changed, 216 insertions(+), 1 deletion(-) create mode 100644 dev-packages/node-integration-tests/suites/tracing/linking/scenario-span-options.ts diff --git a/dev-packages/node-integration-tests/suites/tracing/linking/scenario-span-options.ts b/dev-packages/node-integration-tests/suites/tracing/linking/scenario-span-options.ts new file mode 100644 index 000000000000..5e6debe78fc4 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/tracing/linking/scenario-span-options.ts @@ -0,0 +1,27 @@ +import { loggingTransport } from '@sentry-internal/node-integration-tests'; +import * as Sentry from '@sentry/node'; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', + tracesSampleRate: 1.0, + integrations: [], + transport: loggingTransport, +}); + +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-integration-tests/suites/tracing/linking/test.ts b/dev-packages/node-integration-tests/suites/tracing/linking/test.ts index 1c4e518a4f74..57f68c1d258f 100644 --- a/dev-packages/node-integration-tests/suites/tracing/linking/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/linking/test.ts @@ -1,6 +1,36 @@ import { createRunner } from '../../../utils/runner'; describe('span links', () => { + test('should link spans by adding "links" to span options', done => { + let span1_traceId: string, span1_spanId: string; + + 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(done); + }); + test('should link spans with addLink() in trace context', done => { let span1_traceId: string, span1_spanId: string; diff --git a/packages/core/src/types-hoist/span.ts b/packages/core/src/types-hoist/span.ts index 2b82aab74934..d82463768b7f 100644 --- a/packages/core/src/types-hoist/span.ts +++ b/packages/core/src/types-hoist/span.ts @@ -182,6 +182,12 @@ export interface SentrySpanArguments { */ endTimestamp?: number | undefined; + /** + * Links to associate with the new span. Setting links here is preferred over addLink() + * as certain context information is only available during span creation. + */ + links?: SpanLink[]; + /** * Set to `true` if this span should be sent as a standalone segment span * as opposed to a transaction. diff --git a/packages/core/src/types-hoist/startSpanOptions.ts b/packages/core/src/types-hoist/startSpanOptions.ts index 6e5fa007bde8..eb3aa0b53299 100644 --- a/packages/core/src/types-hoist/startSpanOptions.ts +++ b/packages/core/src/types-hoist/startSpanOptions.ts @@ -1,4 +1,5 @@ import type { Scope } from '../scope'; +import type { SpanLink } from './link'; import type { Span, SpanAttributes, SpanTimeInput } from './span'; export interface StartSpanOptions { @@ -44,6 +45,12 @@ export interface StartSpanOptions { /** Attributes for the span. */ attributes?: SpanAttributes; + /** + * Links to associate with the new span. Setting links here is preferred over addLink() + * as it allows sampling decisions to consider the link information. + */ + links?: SpanLink[]; + /** * Experimental options without any stability guarantees. Use with caution! */ diff --git a/packages/opentelemetry/src/trace.ts b/packages/opentelemetry/src/trace.ts index 7d65a11f2295..77f3cac6ddf0 100644 --- a/packages/opentelemetry/src/trace.ts +++ b/packages/opentelemetry/src/trace.ts @@ -160,7 +160,7 @@ function getTracer(): Tracer { } function getSpanOptions(options: OpenTelemetrySpanContext): SpanOptions { - const { startTime, attributes, kind, op } = options; + const { startTime, attributes, kind, op, links } = options; // OTEL expects timestamps in ms, not seconds const fixedStartTime = typeof startTime === 'number' ? ensureTimestampInMilliseconds(startTime) : startTime; @@ -173,6 +173,7 @@ function getSpanOptions(options: OpenTelemetrySpanContext): SpanOptions { } : attributes, kind, + links, startTime: fixedStartTime, }; } diff --git a/packages/opentelemetry/test/spanExporter.test.ts b/packages/opentelemetry/test/spanExporter.test.ts index 19714c2b172f..c8052bbad2a3 100644 --- a/packages/opentelemetry/test/spanExporter.test.ts +++ b/packages/opentelemetry/test/spanExporter.test.ts @@ -135,4 +135,34 @@ describe('createTransactionForOtelSpan', () => { ); }); }); + + it('adds span link to the trace context when linked in span options', () => { + const span = startInactiveSpan({ name: 'parent1' }); + + const prevTraceId = span.spanContext().traceId; + const prevSpanId = span.spanContext().spanId; + + const linkedSpan = startInactiveSpan({ + name: 'parent2', + links: [{ context: span.spanContext(), attributes: { 'sentry.link.type': 'previous_trace' } }], + }); + + span.end(); + linkedSpan.end(); + + const event = createTransactionForOtelSpan(linkedSpan as any); + + expect(event.contexts?.trace).toEqual( + expect.objectContaining({ + links: [ + expect.objectContaining({ + attributes: { 'sentry.link.type': 'previous_trace' }, + sampled: true, + trace_id: expect.stringMatching(prevTraceId), + span_id: expect.stringMatching(prevSpanId), + }), + ], + }), + ); + }); }); diff --git a/packages/opentelemetry/test/trace.test.ts b/packages/opentelemetry/test/trace.test.ts index ba1adbb74031..0222264ad6de 100644 --- a/packages/opentelemetry/test/trace.test.ts +++ b/packages/opentelemetry/test/trace.test.ts @@ -390,6 +390,44 @@ describe('trace', () => { }); }); + it('allows to pass span links in span options', () => { + const rawSpan1 = startInactiveSpan({ name: 'pageload_span' }); + + // @ts-expect-error links exists on span + expect(rawSpan1?.links).toEqual([]); + + const span1JSON = spanToJSON(rawSpan1); + + startSpan( + { + name: '/users/:id', + links: [ + { + context: rawSpan1.spanContext(), + attributes: { 'sentry.link.type': 'previous_trace' }, + }, + ], + }, + rawSpan2 => { + const span2LinkJSON = spanToJSON(rawSpan2).links?.[0]; + + expect(span2LinkJSON?.attributes?.['sentry.link.type']).toBe('previous_trace'); + + // @ts-expect-error links and _spanContext exist on span + expect(rawSpan2?.links?.[0].context.traceId).toEqual(rawSpan1._spanContext.traceId); + // @ts-expect-error links and _spanContext exist on span + expect(rawSpan2?.links?.[0].context.traceId).toEqual(span1JSON.trace_id); + expect(span2LinkJSON?.trace_id).toBe(span1JSON.trace_id); + + // @ts-expect-error links and _spanContext exist on span + expect(rawSpan2?.links?.[0].context.spanId).toEqual(rawSpan1?._spanContext.spanId); + // @ts-expect-error links and _spanContext exist on span + expect(rawSpan2?.links?.[0].context.spanId).toEqual(span1JSON.span_id); + expect(span2LinkJSON?.span_id).toBe(span1JSON.span_id); + }, + ); + }); + it('allows to force a transaction with forceTransaction=true', async () => { const client = getClient()!; const transactionEvents: Event[] = []; @@ -651,6 +689,44 @@ describe('trace', () => { }); }); + it('allows to pass span links in span options', () => { + const rawSpan1 = startInactiveSpan({ name: 'pageload_span' }); + + // @ts-expect-error links exists on span + expect(rawSpan1?.links).toEqual([]); + + const rawSpan2 = startInactiveSpan({ + name: 'GET users/[id]', + links: [ + { + context: rawSpan1.spanContext(), + attributes: { 'sentry.link.type': 'previous_trace' }, + }, + ], + }); + + const span1JSON = spanToJSON(rawSpan1); + const span2JSON = spanToJSON(rawSpan2); + const span2LinkJSON = span2JSON.links?.[0]; + + expect(span2LinkJSON?.attributes?.['sentry.link.type']).toBe('previous_trace'); + + // @ts-expect-error links and _spanContext exist on span + expect(rawSpan2?.links?.[0].context.traceId).toEqual(rawSpan1._spanContext.traceId); + // @ts-expect-error links and _spanContext exist on span + expect(rawSpan2?.links?.[0].context.traceId).toEqual(span1JSON.trace_id); + expect(span2LinkJSON?.trace_id).toBe(span1JSON.trace_id); + + // @ts-expect-error links and _spanContext exist on span + expect(rawSpan2?.links?.[0].context.spanId).toEqual(rawSpan1?._spanContext.spanId); + // @ts-expect-error links and _spanContext exist on span + expect(rawSpan2?.links?.[0].context.spanId).toEqual(span1JSON.span_id); + expect(span2LinkJSON?.span_id).toBe(span1JSON.span_id); + + // sampling decision is inherited + expect(span2LinkJSON?.sampled).toBe(Boolean(spanToJSON(rawSpan1).data['sentry.sample_rate'])); + }); + it('allows to force a transaction with forceTransaction=true', async () => { const client = getClient()!; const transactionEvents: Event[] = []; @@ -974,6 +1050,44 @@ describe('trace', () => { }); }); + it('allows to pass span links in span options', () => { + const rawSpan1 = startInactiveSpan({ name: 'pageload_span' }); + + // @ts-expect-error links exists on span + expect(rawSpan1?.links).toEqual([]); + + const span1JSON = spanToJSON(rawSpan1); + + startSpanManual( + { + name: '/users/:id', + links: [ + { + context: rawSpan1.spanContext(), + attributes: { 'sentry.link.type': 'previous_trace' }, + }, + ], + }, + rawSpan2 => { + const span2LinkJSON = spanToJSON(rawSpan2).links?.[0]; + + expect(span2LinkJSON?.attributes?.['sentry.link.type']).toBe('previous_trace'); + + // @ts-expect-error links and _spanContext exist on span + expect(rawSpan2?.links?.[0].context.traceId).toEqual(rawSpan1._spanContext.traceId); + // @ts-expect-error links and _spanContext exist on span + expect(rawSpan2?.links?.[0].context.traceId).toEqual(span1JSON.trace_id); + expect(span2LinkJSON?.trace_id).toBe(span1JSON.trace_id); + + // @ts-expect-error links and _spanContext exist on span + expect(rawSpan2?.links?.[0].context.spanId).toEqual(rawSpan1?._spanContext.spanId); + // @ts-expect-error links and _spanContext exist on span + expect(rawSpan2?.links?.[0].context.spanId).toEqual(span1JSON.span_id); + expect(span2LinkJSON?.span_id).toBe(span1JSON.span_id); + }, + ); + }); + it('allows to force a transaction with forceTransaction=true', async () => { const client = getClient()!; const transactionEvents: Event[] = []; From 5aca7cab50963cfb6e1025fadae6ca42be1687fb Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Mon, 17 Feb 2025 17:30:44 +0100 Subject: [PATCH 11/32] fix(core): Filter out unactionable Facebook Mobile browser error (#15430) Filters out an unactionable error thrown by the Facebook Mobile browser web view. Closes https://github.com/getsentry/sentry-javascript/issues/15065 --- packages/core/src/integrations/inboundfilters.ts | 1 + .../test/lib/integrations/inboundfilters.test.ts | 16 ++++++++++++++++ 2 files changed, 17 insertions(+) diff --git a/packages/core/src/integrations/inboundfilters.ts b/packages/core/src/integrations/inboundfilters.ts index 17b4442cee57..c006359d9361 100644 --- a/packages/core/src/integrations/inboundfilters.ts +++ b/packages/core/src/integrations/inboundfilters.ts @@ -19,6 +19,7 @@ const DEFAULT_IGNORE_ERRORS = [ "vv().getRestrictions is not a function. (In 'vv().getRestrictions(1,a)', 'vv().getRestrictions' is undefined)", // Error thrown by GTM, seemingly not affecting end-users "Can't find variable: _AutofillCallbackHandler", // Unactionable error in instagram webview https://developers.facebook.com/community/threads/320013549791141/ /^Non-Error promise rejection captured with value: Object Not Found Matching Id:\d+, MethodName:simulateEvent, ParamCount:\d+$/, // unactionable error from CEFSharp, a .NET library that embeds chromium in .NET apps + /^Java exception was raised during method invocation$/, // error from Facebook Mobile browser (https://github.com/getsentry/sentry-javascript/issues/15065) ]; /** Options for the InboundFilters integration */ diff --git a/packages/core/test/lib/integrations/inboundfilters.test.ts b/packages/core/test/lib/integrations/inboundfilters.test.ts index 046ee5a168d7..9f3e212e2c76 100644 --- a/packages/core/test/lib/integrations/inboundfilters.test.ts +++ b/packages/core/test/lib/integrations/inboundfilters.test.ts @@ -281,6 +281,17 @@ const CEFSHARP_EVENT: Event = { }, }; +const FB_MOBILE_BROWSER_EVENT: Event = { + exception: { + values: [ + { + type: 'Error', + value: 'Java exception was raised during method invocation', + }, + ], + }, +}; + const MALFORMED_EVENT: Event = { exception: { values: [ @@ -402,6 +413,11 @@ describe('InboundFilters', () => { expect(eventProcessor(CEFSHARP_EVENT, {})).toBe(null); }); + it('uses default filters (FB Mobile Browser)', () => { + const eventProcessor = createInboundFiltersEventProcessor(); + expect(eventProcessor(FB_MOBILE_BROWSER_EVENT, {})).toBe(null); + }); + it('filters on last exception when multiple present', () => { const eventProcessor = createInboundFiltersEventProcessor({ ignoreErrors: ['incorrect type given for parameter `chewToy`'], From 136370c8d4f0c0cd9ec80b47ba2292406d0b4ba7 Mon Sep 17 00:00:00 2001 From: Sigrid Huemer <32902192+s1gr1d@users.noreply.github.com> Date: Mon, 17 Feb 2025 17:35:12 +0100 Subject: [PATCH 12/32] fix(core): Add Google `gmo` error to Inbound Filters (#15432) closes https://github.com/getsentry/sentry-javascript/issues/15389 --- packages/core/src/integrations/inboundfilters.ts | 1 + .../core/test/lib/integrations/inboundfilters.test.ts | 11 +++++++++++ 2 files changed, 12 insertions(+) diff --git a/packages/core/src/integrations/inboundfilters.ts b/packages/core/src/integrations/inboundfilters.ts index c006359d9361..7840ab5f6920 100644 --- a/packages/core/src/integrations/inboundfilters.ts +++ b/packages/core/src/integrations/inboundfilters.ts @@ -14,6 +14,7 @@ const DEFAULT_IGNORE_ERRORS = [ /^Javascript error: Script error\.? on line 0$/, /^ResizeObserver loop completed with undelivered notifications.$/, // The browser logs this when a ResizeObserver handler takes a bit longer. Usually this is not an actual issue though. It indicates slowness. /^Cannot redefine property: googletag$/, // This is thrown when google tag manager is used in combination with an ad blocker + /^Can't find variable: gmo$/, // Error from Google Search App https://issuetracker.google.com/issues/396043331 "undefined is not an object (evaluating 'a.L')", // Random error that happens but not actionable or noticeable to end-users. 'can\'t redefine non-configurable property "solana"', // Probably a browser extension or custom browser (Brave) throwing this error "vv().getRestrictions is not a function. (In 'vv().getRestrictions(1,a)', 'vv().getRestrictions' is undefined)", // Error thrown by GTM, seemingly not affecting end-users diff --git a/packages/core/test/lib/integrations/inboundfilters.test.ts b/packages/core/test/lib/integrations/inboundfilters.test.ts index 9f3e212e2c76..e06c6bda0da2 100644 --- a/packages/core/test/lib/integrations/inboundfilters.test.ts +++ b/packages/core/test/lib/integrations/inboundfilters.test.ts @@ -269,6 +269,12 @@ const GOOGLETAG_EVENT: Event = { }, }; +const GOOGLE_APP_GMO: Event = { + exception: { + values: [{ type: 'ReferenceError', value: "Can't find variable: gmo" }], + }, +}; + const CEFSHARP_EVENT: Event = { exception: { values: [ @@ -408,6 +414,11 @@ describe('InboundFilters', () => { expect(eventProcessor(GOOGLETAG_EVENT, {})).toBe(null); }); + it('uses default filters (Google App "gmo")', () => { + const eventProcessor = createInboundFiltersEventProcessor(); + expect(eventProcessor(GOOGLE_APP_GMO, {})).toBe(null); + }); + it('uses default filters (CEFSharp)', () => { const eventProcessor = createInboundFiltersEventProcessor(); expect(eventProcessor(CEFSHARP_EVENT, {})).toBe(null); From da8ba8d77a28b43da5014acc8dd98906d2180cc1 Mon Sep 17 00:00:00 2001 From: Jonas Date: Tue, 18 Feb 2025 09:15:47 -0500 Subject: [PATCH 13/32] profiling: bump chunk interval to 60s (#15361) Bump profiling chunk interval to 60s as per [spec](https://www.notion.so/sentry/Continuous-UI-Profiling-SDK-API-Spec-17e8b10e4b5d80c59a40c6e114470934) --- packages/profiling-node/src/integration.ts | 2 +- packages/profiling-node/test/spanProfileUtils.test.ts | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/profiling-node/src/integration.ts b/packages/profiling-node/src/integration.ts index 5b455f72974d..4fffdf490901 100644 --- a/packages/profiling-node/src/integration.ts +++ b/packages/profiling-node/src/integration.ts @@ -30,7 +30,7 @@ import { makeProfileChunkEnvelope, } from './utils'; -const CHUNK_INTERVAL_MS = 5000; +const CHUNK_INTERVAL_MS = 1000 * 60; const PROFILE_MAP = new LRUMap(50); const PROFILE_TIMEOUTS: Record = {}; diff --git a/packages/profiling-node/test/spanProfileUtils.test.ts b/packages/profiling-node/test/spanProfileUtils.test.ts index 758307d3fa34..c0640064c537 100644 --- a/packages/profiling-node/test/spanProfileUtils.test.ts +++ b/packages/profiling-node/test/spanProfileUtils.test.ts @@ -501,7 +501,7 @@ describe('continuous profiling', () => { expect(startProfilingSpy).not.toHaveBeenCalledTimes(1); Sentry.profiler.startProfiler(); - jest.advanceTimersByTime(5001); + jest.advanceTimersByTime(60_001); expect(stopProfilingSpy).toHaveBeenCalledTimes(1); expect(startProfilingSpy).toHaveBeenCalledTimes(2); }); @@ -518,7 +518,7 @@ describe('continuous profiling', () => { Sentry.profiler.startProfiler(); const profilerId = getProfilerId(); - jest.advanceTimersByTime(5001); + jest.advanceTimersByTime(60_001); expect(stopProfilingSpy).toHaveBeenCalledTimes(1); expect(startProfilingSpy).toHaveBeenCalledTimes(2); expect(getProfilerId()).toBe(profilerId); @@ -552,7 +552,7 @@ describe('continuous profiling', () => { expect(startProfilingSpy).not.toHaveBeenCalledTimes(1); Sentry.profiler.startProfiler(); - jest.advanceTimersByTime(5001); + jest.advanceTimersByTime(60_001); expect(stopProfilingSpy).toHaveBeenCalledTimes(1); }); From 8fc56d85d90b66c14d76e5a6e424db911b221be6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 18 Feb 2025 17:33:09 +0100 Subject: [PATCH 14/32] feat(deps): Bump @sentry/webpack-plugin from 2.22.7 to 3.1.2 (#15328) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [@sentry/webpack-plugin](https://github.com/getsentry/sentry-javascript-bundler-plugins) from 2.22.7 to 3.1.2.
Release notes

Sourced from @​sentry/webpack-plugin's releases.

3.1.2

  • deps: Bump @sentry/cli to 2.41.1 (#671)

3.1.1

  • fix(core): Disable release creation and source maps upload in dev mode (#666)

    This fix disables any external calls to the Sentry API for managing releases or uploading source maps, when detecting that the plugin is running in dev-mode. While this rarely actually happened, it also polluted the dev server output with unnecessary logs about missing auth tokens, which shouldn't be required in dev mode.

3.1.0

  • feat(webpack): Gate forced process exit behind experimental flag (#663)

3.0.0

Breaking Changes

  • Injected code will now use let, which was added in ES6 (ES2015). This means that ES6 is the minimum JavaScript version that the Sentry bundler plugins support.

  • Deprecated options have been removed:

    • deleteFilesAfterUpload - Use filesToDeleteAfterUpload instead
    • bundleSizeOptimizations.excludePerformanceMonitoring - Use bundleSizeOptimizations.excludeTracing instead
    • _experiments.moduleMetadata - Use moduleMetadata instead
    • cleanArtifacts - Did not do anything

List of Changes

  • fix!: Wrap injected code in block-statement to contain scope (#646)
  • chore!: Remove deprecated options (#654)
  • feat(logger): Use console methods respective to log level (#652)
  • fix(webpack): Ensure process exits when done (#653)
  • fix: Use correct replacement matcher for bundleSizeOptimizations.excludeTracing (#644)

Work in this release contributed by @​jdelStrother. Thank you for your contribution!

2.23.0

  • chore(deps): bump nanoid from 3.3.6 to 3.3.8 (#641)
  • feat(core): Detect Railway release name (#639)
  • feat(core): Write module injections to globalThis (#636)
  • feat(react-component-annotate): Allow skipping annotations on specified components (#617)
  • ref(core): Rename release management plugin name (#647)

Work in this release contributed by @​conor-ob. Thank you for your contribution!

Changelog

Sourced from @​sentry/webpack-plugin's changelog.

3.1.2

  • deps: Bump @sentry/cli to 2.41.1 (#671)

3.1.1

  • fix(core): Disable release creation and source maps upload in dev mode (#666)

    This fix disables any external calls to the Sentry API for managing releases or uploading source maps, when detecting that the plugin is running in dev-mode. While this rarely actually happened, it also polluted the dev server output with unnecessary logs about missing auth tokens, which shouldn't be required in dev mode.

3.1.0

  • feat(webpack): Gate forced process exit behind experimental flag (#663)

3.0.0

Breaking Changes

  • Injected code will now use let, which was added in ES6 (ES2015). This means that ES6 is the minimum JavaScript version that the Sentry bundler plugins support.

  • Deprecated options have been removed:

    • deleteFilesAfterUpload - Use filesToDeleteAfterUpload instead
    • bundleSizeOptimizations.excludePerformanceMonitoring - Use bundleSizeOptimizations.excludeTracing instead
    • _experiments.moduleMetadata - Use moduleMetadata instead
    • cleanArtifacts - Did not do anything

List of Changes

  • fix!: Wrap injected code in block-statement to contain scope (#646)
  • chore!: Remove deprecated options (#654)
  • feat(logger): Use console methods respective to log level (#652)
  • fix(webpack): Ensure process exits when done (#653)
  • fix: Use correct replacement matcher for bundleSizeOptimizations.excludeTracing (#644)

Work in this release contributed by @​jdelStrother. Thank you for your contribution!

2.23.0

  • chore(deps): bump nanoid from 3.3.6 to 3.3.8 (#641)
  • feat(core): Detect Railway release name (#639)
  • feat(core): Write module injections to globalThis (#636)
  • feat(react-component-annotate): Allow skipping annotations on specified components (#617)
  • ref(core): Rename release management plugin name (#647)

Work in this release contributed by @​conor-ob. Thank you for your contribution!

Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=@sentry/webpack-plugin&package-manager=npm_and_yarn&previous-version=2.22.7&new-version=3.1.2)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- packages/gatsby/package.json | 2 +- yarn.lock | 82 ------------------------------------ 2 files changed, 1 insertion(+), 83 deletions(-) diff --git a/packages/gatsby/package.json b/packages/gatsby/package.json index 883fd37f1034..d0bf2943065f 100644 --- a/packages/gatsby/package.json +++ b/packages/gatsby/package.json @@ -47,7 +47,7 @@ "dependencies": { "@sentry/core": "9.1.0", "@sentry/react": "9.1.0", - "@sentry/webpack-plugin": "2.22.7" + "@sentry/webpack-plugin": "3.1.2" }, "peerDependencies": { "gatsby": "^2.0.0 || ^3.0.0 || ^4.0.0 || ^5.0.0", diff --git a/yarn.lock b/yarn.lock index ab8921cdf802..8b41988eb8ec 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6587,11 +6587,6 @@ resolved "https://registry.yarnpkg.com/@sentry/babel-plugin-component-annotate/-/babel-plugin-component-annotate-2.22.6.tgz#829d6caf2c95c1c46108336de4e1049e6521435e" integrity sha512-V2g1Y1I5eSe7dtUVMBvAJr8BaLRr4CLrgNgtPaZyMT4Rnps82SrZ5zqmEkLXPumlXhLUWR6qzoMNN2u+RXVXfQ== -"@sentry/babel-plugin-component-annotate@2.22.7": - version "2.22.7" - resolved "https://registry.yarnpkg.com/@sentry/babel-plugin-component-annotate/-/babel-plugin-component-annotate-2.22.7.tgz#604c7e33d48528a13477e7af597c4d5fca51b8bd" - integrity sha512-aa7XKgZMVl6l04NY+3X7BP7yvQ/s8scn8KzQfTLrGRarziTlMGrsCOBQtCNWXOPEbtxAIHpZ9dsrAn5EJSivOQ== - "@sentry/babel-plugin-component-annotate@3.1.2": version "3.1.2" resolved "https://registry.yarnpkg.com/@sentry/babel-plugin-component-annotate/-/babel-plugin-component-annotate-3.1.2.tgz#5497ca5adbe775955e96c566511a0bed3ab0a3ce" @@ -6611,20 +6606,6 @@ magic-string "0.30.8" unplugin "1.0.1" -"@sentry/bundler-plugin-core@2.22.7": - version "2.22.7" - resolved "https://registry.yarnpkg.com/@sentry/bundler-plugin-core/-/bundler-plugin-core-2.22.7.tgz#28204a224cd1fef58d157e5beeb2493947a9bc35" - integrity sha512-ouQh5sqcB8vsJ8yTTe0rf+iaUkwmeUlGNFi35IkCFUQlWJ22qS6OfvNjOqFI19e6eGUXks0c/2ieFC4+9wJ+1g== - dependencies: - "@babel/core" "^7.18.5" - "@sentry/babel-plugin-component-annotate" "2.22.7" - "@sentry/cli" "2.39.1" - dotenv "^16.3.1" - find-up "^5.0.0" - glob "^9.3.2" - magic-string "0.30.8" - unplugin "1.0.1" - "@sentry/bundler-plugin-core@3.1.2": version "3.1.2" resolved "https://registry.yarnpkg.com/@sentry/bundler-plugin-core/-/bundler-plugin-core-3.1.2.tgz#29e4e686c5893b41a0d98a1bef6f0315a610bd59" @@ -6639,95 +6620,41 @@ magic-string "0.30.8" unplugin "1.0.1" -"@sentry/cli-darwin@2.39.1": - version "2.39.1" - resolved "https://registry.yarnpkg.com/@sentry/cli-darwin/-/cli-darwin-2.39.1.tgz#75c338a53834b4cf72f57599f4c72ffb36cf0781" - integrity sha512-kiNGNSAkg46LNGatfNH5tfsmI/kCAaPA62KQuFZloZiemTNzhy9/6NJP8HZ/GxGs8GDMxic6wNrV9CkVEgFLJQ== - "@sentry/cli-darwin@2.41.1": version "2.41.1" resolved "https://registry.yarnpkg.com/@sentry/cli-darwin/-/cli-darwin-2.41.1.tgz#ca7e12bf1ad59bc2df35868ae98abc8869108efa" integrity sha512-7pS3pu/SuhE6jOn3wptstAg6B5nUP878O6s+2svT7b5fKNfYUi/6NPK6dAveh2Ca0rwVq40TO4YFJabWMgTpdQ== -"@sentry/cli-linux-arm64@2.39.1": - version "2.39.1" - resolved "https://registry.yarnpkg.com/@sentry/cli-linux-arm64/-/cli-linux-arm64-2.39.1.tgz#27db44700c33fcb1e8966257020b43f8494373e6" - integrity sha512-5VbVJDatolDrWOgaffsEM7znjs0cR8bHt9Bq0mStM3tBolgAeSDHE89NgHggfZR+DJ2VWOy4vgCwkObrUD6NQw== - "@sentry/cli-linux-arm64@2.41.1": version "2.41.1" resolved "https://registry.yarnpkg.com/@sentry/cli-linux-arm64/-/cli-linux-arm64-2.41.1.tgz#948e8af8290418b1562db3531db08e69e39d74bb" integrity sha512-EzYCEnnENBnS5kpNW+2dBcrPZn1MVfywh2joGVQZTpmgDL5YFJ59VOd+K0XuEwqgFI8BSNI14KXZ75s4DD1/Vw== -"@sentry/cli-linux-arm@2.39.1": - version "2.39.1" - resolved "https://registry.yarnpkg.com/@sentry/cli-linux-arm/-/cli-linux-arm-2.39.1.tgz#451683fa9a5a60b1359d104ec71334ed16f4b63c" - integrity sha512-DkENbxyRxUrfLnJLXTA4s5UL/GoctU5Cm4ER1eB7XN7p9WsamFJd/yf2KpltkjEyiTuplv0yAbdjl1KX3vKmEQ== - "@sentry/cli-linux-arm@2.41.1": version "2.41.1" resolved "https://registry.yarnpkg.com/@sentry/cli-linux-arm/-/cli-linux-arm-2.41.1.tgz#1e5fa971ae8dfb3ea5564c8503b4e635ae6aed8a" integrity sha512-wNUvquD6qjOCczvuBGf9OiD29nuQ6yf8zzfyPJa5Bdx1QXuteKsKb6HBrMwuIR3liyuu0duzHd+H/+p1n541Hg== -"@sentry/cli-linux-i686@2.39.1": - version "2.39.1" - resolved "https://registry.yarnpkg.com/@sentry/cli-linux-i686/-/cli-linux-i686-2.39.1.tgz#9965a81f97a94e8b6d1d15589e43fee158e35201" - integrity sha512-pXWVoKXCRrY7N8vc9H7mETiV9ZCz+zSnX65JQCzZxgYrayQPJTc+NPRnZTdYdk5RlAupXaFicBI2GwOCRqVRkg== - "@sentry/cli-linux-i686@2.41.1": version "2.41.1" resolved "https://registry.yarnpkg.com/@sentry/cli-linux-i686/-/cli-linux-i686-2.41.1.tgz#3f01aff314f2ad8fd761f3e6e807a5ec09ae4eb4" integrity sha512-urpQCWrdYnSAsZY3udttuMV88wTJzKZL10xsrp7sjD/Hd+O6qSLVLkxebIlxts70jMLLFHYrQ2bkRg5kKuX6Fg== -"@sentry/cli-linux-x64@2.39.1": - version "2.39.1" - resolved "https://registry.yarnpkg.com/@sentry/cli-linux-x64/-/cli-linux-x64-2.39.1.tgz#31fe008b02f92769543dc9919e2a5cbc4cda7889" - integrity sha512-IwayNZy+it7FWG4M9LayyUmG1a/8kT9+/IEm67sT5+7dkMIMcpmHDqL8rWcPojOXuTKaOBBjkVdNMBTXy0mXlA== - "@sentry/cli-linux-x64@2.41.1": version "2.41.1" resolved "https://registry.yarnpkg.com/@sentry/cli-linux-x64/-/cli-linux-x64-2.41.1.tgz#30dbf966a4b4c1721ffccd901dfcb6f967db073d" integrity sha512-ZqpYwHXAaK4MMEFlyaLYr6mJTmpy9qP6n30jGhLTW7kHKS3s6GPLCSlNmIfeClrInEt0963fM633ZRnXa04VPw== -"@sentry/cli-win32-i686@2.39.1": - version "2.39.1" - resolved "https://registry.yarnpkg.com/@sentry/cli-win32-i686/-/cli-win32-i686-2.39.1.tgz#609e8790c49414011445e397130560c777850b35" - integrity sha512-NglnNoqHSmE+Dz/wHeIVRnV2bLMx7tIn3IQ8vXGO5HWA2f8zYJGktbkLq1Lg23PaQmeZLPGlja3gBQfZYSG10Q== - "@sentry/cli-win32-i686@2.41.1": version "2.41.1" resolved "https://registry.yarnpkg.com/@sentry/cli-win32-i686/-/cli-win32-i686-2.41.1.tgz#f88eeb5d2d4ee46c38d8616ae1eb484108ea71c2" integrity sha512-AuRimCeVsx99DIOr9cwdYBHk39tlmAuPDdy2r16iNzY0InXs4xOys4gGzM7N4vlFQvFkzuc778Su0HkfasgprA== -"@sentry/cli-win32-x64@2.39.1": - version "2.39.1" - resolved "https://registry.yarnpkg.com/@sentry/cli-win32-x64/-/cli-win32-x64-2.39.1.tgz#1a874a5570c6d162b35d9d001c96e5389d07d2cb" - integrity sha512-xv0R2CMf/X1Fte3cMWie1NXuHmUyQPDBfCyIt6k6RPFPxAYUgcqgMPznYwVMwWEA1W43PaOkSn3d8ZylsDaETw== - "@sentry/cli-win32-x64@2.41.1": version "2.41.1" resolved "https://registry.yarnpkg.com/@sentry/cli-win32-x64/-/cli-win32-x64-2.41.1.tgz#eefd95a2aa184adb464334e265b55a9142070f6f" integrity sha512-6JcPvXGye61+wPp0xdzfc2YLE/Dcud8JdaK8VxLM3b/8+Em7E+UyliDu3uF8+YGUqizY5JYTd3fs17DC8DZhLw== -"@sentry/cli@2.39.1": - version "2.39.1" - resolved "https://registry.yarnpkg.com/@sentry/cli/-/cli-2.39.1.tgz#916bb5b7567ccf7fdf94ef6cf8a2b9ab78370d29" - integrity sha512-JIb3e9vh0+OmQ0KxmexMXg9oZsR/G7HMwxt5BUIKAXZ9m17Xll4ETXTRnRUBT3sf7EpNGAmlQk1xEmVN9pYZYQ== - dependencies: - https-proxy-agent "^5.0.0" - node-fetch "^2.6.7" - progress "^2.0.3" - proxy-from-env "^1.1.0" - which "^2.0.2" - optionalDependencies: - "@sentry/cli-darwin" "2.39.1" - "@sentry/cli-linux-arm" "2.39.1" - "@sentry/cli-linux-arm64" "2.39.1" - "@sentry/cli-linux-i686" "2.39.1" - "@sentry/cli-linux-x64" "2.39.1" - "@sentry/cli-win32-i686" "2.39.1" - "@sentry/cli-win32-x64" "2.39.1" - "@sentry/cli@2.41.1", "@sentry/cli@^2.36.1", "@sentry/cli@^2.41.1": version "2.41.1" resolved "https://registry.yarnpkg.com/@sentry/cli/-/cli-2.41.1.tgz#a9467ca3ff4acfcdedec1565c9ff726b93758d29" @@ -6763,15 +6690,6 @@ "@sentry/bundler-plugin-core" "2.22.6" unplugin "1.0.1" -"@sentry/webpack-plugin@2.22.7": - version "2.22.7" - resolved "https://registry.yarnpkg.com/@sentry/webpack-plugin/-/webpack-plugin-2.22.7.tgz#992c6c782c736f22e72eb318745e28cc24aabad7" - integrity sha512-j5h5LZHWDlm/FQCCmEghQ9FzYXwfZdlOf3FE/X6rK6lrtx0JCAkq+uhMSasoyP4XYKL4P4vRS6WFSos4jxf/UA== - dependencies: - "@sentry/bundler-plugin-core" "2.22.7" - unplugin "1.0.1" - uuid "^9.0.0" - "@sentry/webpack-plugin@3.1.2": version "3.1.2" resolved "https://registry.yarnpkg.com/@sentry/webpack-plugin/-/webpack-plugin-3.1.2.tgz#e7cf2b10b6d2fb2d6106e692469d02b6ab684bba" From eaffd72fe9db40ad993e29d795d7ce7cb14a40d1 Mon Sep 17 00:00:00 2001 From: Sigrid Huemer <32902192+s1gr1d@users.noreply.github.com> Date: Wed, 19 Feb 2025 11:02:00 +0100 Subject: [PATCH 15/32] fix(nuxt): Use `SentryNuxtServerOptions` type for server init (#15441) We actually have a concrete type for the `init` on the server-side. However, this type was not used so far. Additionally, I removed the `Omit` of `'app'` as there is no `'app'` on the Node types anyway. --- packages/nuxt/src/common/types.ts | 5 +++-- packages/nuxt/src/index.types.ts | 5 ++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/nuxt/src/common/types.ts b/packages/nuxt/src/common/types.ts index 62496001273b..02e0b53bac7c 100644 --- a/packages/nuxt/src/common/types.ts +++ b/packages/nuxt/src/common/types.ts @@ -3,9 +3,10 @@ import type { SentryRollupPluginOptions } from '@sentry/rollup-plugin'; import type { SentryVitePluginOptions } from '@sentry/vite-plugin'; import type { init as initVue } from '@sentry/vue'; -// Omitting 'app' as the Nuxt SDK will add the app instance in the client plugin (users do not have to provide this) +// Omitting Vue 'app' as the Nuxt SDK will add the app instance in the client plugin (users do not have to provide this) +// Adding `& object` helps TS with inferring that this is not `undefined` but an object type export type SentryNuxtClientOptions = Omit[0] & object, 'app'>; -export type SentryNuxtServerOptions = Omit[0] & object, 'app'>; +export type SentryNuxtServerOptions = Parameters[0] & object; type SourceMapsOptions = { /** diff --git a/packages/nuxt/src/index.types.ts b/packages/nuxt/src/index.types.ts index dc9bf360af9e..b1393a076029 100644 --- a/packages/nuxt/src/index.types.ts +++ b/packages/nuxt/src/index.types.ts @@ -1,7 +1,6 @@ import type { Client, Integration, Options, StackParser } from '@sentry/core'; -import type { SentryNuxtClientOptions } from './common/types'; +import type { SentryNuxtClientOptions, SentryNuxtServerOptions } from './common/types'; import type * as clientSdk from './index.client'; -import type * as serverSdk from './index.server'; // We export everything from both the client part of the SDK and from the server part. Some of the exports collide, // which is not allowed, unless we re-export the colliding exports in this file - which we do below. @@ -9,7 +8,7 @@ export * from './index.client'; export * from './index.server'; // re-export colliding types -export declare function init(options: Options | SentryNuxtClientOptions | serverSdk.NodeOptions): Client | undefined; +export declare function init(options: Options | SentryNuxtClientOptions | SentryNuxtServerOptions): Client | undefined; export declare const linkedErrorsIntegration: typeof clientSdk.linkedErrorsIntegration; export declare const contextLinesIntegration: typeof clientSdk.contextLinesIntegration; export declare const getDefaultIntegrations: (options: Options) => Integration[]; From d42d04f6054fbf32d6b51ceff2cbb78b52f2ed3a Mon Sep 17 00:00:00 2001 From: Sigrid Huemer <32902192+s1gr1d@users.noreply.github.com> Date: Wed, 19 Feb 2025 13:28:34 +0100 Subject: [PATCH 16/32] feat(nuxt): Add `enableNitroErrorHandler` to server options (#15444) closes https://github.com/getsentry/sentry-javascript/issues/15409 --- .../nuxt-3/sentry.server.config.ts | 1 + .../server/plugins/customNitroErrorHandler.ts | 85 +++++++++++++++++++ packages/nuxt/src/common/types.ts | 15 +++- .../nuxt/src/runtime/plugins/sentry.server.ts | 20 +++-- 4 files changed, 115 insertions(+), 6 deletions(-) create mode 100644 dev-packages/e2e-tests/test-applications/nuxt-3/server/plugins/customNitroErrorHandler.ts diff --git a/dev-packages/e2e-tests/test-applications/nuxt-3/sentry.server.config.ts b/dev-packages/e2e-tests/test-applications/nuxt-3/sentry.server.config.ts index 729b2296c683..e04331934f99 100644 --- a/dev-packages/e2e-tests/test-applications/nuxt-3/sentry.server.config.ts +++ b/dev-packages/e2e-tests/test-applications/nuxt-3/sentry.server.config.ts @@ -5,4 +5,5 @@ Sentry.init({ environment: 'qa', // dynamic sampling bias to keep transactions tracesSampleRate: 1.0, // Capture 100% of the transactions tunnel: 'http://localhost:3031/', // proxy server + enableNitroErrorHandler: false, // Error handler is defined in server/plugins/customNitroErrorHandler.ts }); diff --git a/dev-packages/e2e-tests/test-applications/nuxt-3/server/plugins/customNitroErrorHandler.ts b/dev-packages/e2e-tests/test-applications/nuxt-3/server/plugins/customNitroErrorHandler.ts new file mode 100644 index 000000000000..2d9258936169 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nuxt-3/server/plugins/customNitroErrorHandler.ts @@ -0,0 +1,85 @@ +import { Context, GLOBAL_OBJ, dropUndefinedKeys, flush, logger, vercelWaitUntil } from '@sentry/core'; +import * as SentryNode from '@sentry/node'; +import { H3Error } from 'h3'; +import type { CapturedErrorContext } from 'nitropack'; +import { defineNitroPlugin } from '#imports'; + +// Copy from SDK-internal error handler (nuxt/src/runtime/plugins/sentry.server.ts) +export default defineNitroPlugin(nitroApp => { + nitroApp.hooks.hook('error', async (error, errorContext) => { + // Do not handle 404 and 422 + if (error instanceof H3Error) { + // Do not report if status code is 3xx or 4xx + if (error.statusCode >= 300 && error.statusCode < 500) { + return; + } + } + + const { method, path } = { + method: errorContext.event?._method ? errorContext.event._method : '', + path: errorContext.event?._path ? errorContext.event._path : null, + }; + + if (path) { + SentryNode.getCurrentScope().setTransactionName(`${method} ${path}`); + } + + const structuredContext = extractErrorContext(errorContext); + + SentryNode.captureException(error, { + captureContext: { contexts: { nuxt: structuredContext } }, + mechanism: { handled: false }, + }); + + await flushIfServerless(); + }); +}); + +function extractErrorContext(errorContext: CapturedErrorContext): Context { + const structuredContext: Context = { + method: undefined, + path: undefined, + tags: undefined, + }; + + if (errorContext) { + if (errorContext.event) { + structuredContext.method = errorContext.event._method || undefined; + structuredContext.path = errorContext.event._path || undefined; + } + + if (Array.isArray(errorContext.tags)) { + structuredContext.tags = errorContext.tags || undefined; + } + } + + return dropUndefinedKeys(structuredContext); +} + +async function flushIfServerless(): Promise { + const isServerless = + !!process.env.FUNCTIONS_WORKER_RUNTIME || // Azure Functions + !!process.env.LAMBDA_TASK_ROOT || // AWS Lambda + !!process.env.VERCEL || + !!process.env.NETLIFY; + + // @ts-expect-error This is not typed + if (GLOBAL_OBJ[Symbol.for('@vercel/request-context')]) { + vercelWaitUntil(flushWithTimeout()); + } else if (isServerless) { + await flushWithTimeout(); + } +} + +async function flushWithTimeout(): Promise { + const sentryClient = SentryNode.getClient(); + const isDebug = sentryClient ? sentryClient.getOptions().debug : false; + + try { + isDebug && logger.log('Flushing events...'); + await flush(2000); + isDebug && logger.log('Done flushing events'); + } catch (e) { + isDebug && logger.log('Error while flushing events:\n', e); + } +} diff --git a/packages/nuxt/src/common/types.ts b/packages/nuxt/src/common/types.ts index 02e0b53bac7c..599b564f62a2 100644 --- a/packages/nuxt/src/common/types.ts +++ b/packages/nuxt/src/common/types.ts @@ -6,7 +6,20 @@ import type { init as initVue } from '@sentry/vue'; // Omitting Vue 'app' as the Nuxt SDK will add the app instance in the client plugin (users do not have to provide this) // Adding `& object` helps TS with inferring that this is not `undefined` but an object type export type SentryNuxtClientOptions = Omit[0] & object, 'app'>; -export type SentryNuxtServerOptions = Parameters[0] & object; +export type SentryNuxtServerOptions = Parameters[0] & { + /** + * Enables the Sentry error handler for the Nitro error hook. + * + * When enabled, exceptions are automatically sent to Sentry with additional data such as the transaction name and Nitro error context. + * It's recommended to keep this enabled unless you need to implement a custom error handler. + * + * If you need a custom implementation, disable this option and refer to the default handler as a reference: + * https://github.com/getsentry/sentry-javascript/blob/da8ba8d77a28b43da5014acc8dd98906d2180cc1/packages/nuxt/src/runtime/plugins/sentry.server.ts#L20-L46 + * + * @default true + */ + enableNitroErrorHandler?: boolean; +}; type SourceMapsOptions = { /** diff --git a/packages/nuxt/src/runtime/plugins/sentry.server.ts b/packages/nuxt/src/runtime/plugins/sentry.server.ts index 5d828775b62f..f65ac64b9982 100644 --- a/packages/nuxt/src/runtime/plugins/sentry.server.ts +++ b/packages/nuxt/src/runtime/plugins/sentry.server.ts @@ -1,14 +1,13 @@ import { GLOBAL_OBJ, flush, - getClient, getDefaultIsolationScope, getIsolationScope, logger, vercelWaitUntil, withIsolationScope, } from '@sentry/core'; -import * as Sentry from '@sentry/node'; +import * as SentryNode from '@sentry/node'; import { type EventHandler, H3Error } from 'h3'; import { defineNitroPlugin } from 'nitropack/runtime'; import type { NuxtRenderHTMLContext } from 'nuxt/app'; @@ -18,6 +17,17 @@ export default defineNitroPlugin(nitroApp => { nitroApp.h3App.handler = patchEventHandler(nitroApp.h3App.handler); nitroApp.hooks.hook('error', async (error, errorContext) => { + const sentryClient = SentryNode.getClient(); + const sentryClientOptions = sentryClient?.getOptions(); + + if ( + sentryClientOptions && + 'enableNitroErrorHandler' in sentryClientOptions && + sentryClientOptions.enableNitroErrorHandler === false + ) { + return; + } + // Do not handle 404 and 422 if (error instanceof H3Error) { // Do not report if status code is 3xx or 4xx @@ -32,12 +42,12 @@ export default defineNitroPlugin(nitroApp => { }; if (path) { - Sentry.getCurrentScope().setTransactionName(`${method} ${path}`); + SentryNode.getCurrentScope().setTransactionName(`${method} ${path}`); } const structuredContext = extractErrorContext(errorContext); - Sentry.captureException(error, { + SentryNode.captureException(error, { captureContext: { contexts: { nuxt: structuredContext } }, mechanism: { handled: false }, }); @@ -67,7 +77,7 @@ async function flushIfServerless(): Promise { } async function flushWithTimeout(): Promise { - const sentryClient = getClient(); + const sentryClient = SentryNode.getClient(); const isDebug = sentryClient ? sentryClient.getOptions().debug : false; try { From adca0f52c8592f4c7dd19ca3aba5026f1e1d926a Mon Sep 17 00:00:00 2001 From: Sigrid Huemer <32902192+s1gr1d@users.noreply.github.com> Date: Wed, 19 Feb 2025 16:32:20 +0100 Subject: [PATCH 17/32] fix(nuxt): Only use filename with file extension from command (#15445) The Nuxt SDK also uses the preview command to determine where to put the Sentry top-level import. Like this: `node .output/server/index.mjs` (would add Sentry to `index.mjs`) However, the function to get the file name also used folder names without file extensions which led to Sentry not being included at the top of the index file. This was a problem in e.g. Azure as the command is the following: `npx @azure/static-web-apps-cli start ./public --api-location ./server` --- packages/nuxt/src/vite/utils.ts | 2 +- packages/nuxt/test/vite/utils.test.ts | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/nuxt/src/vite/utils.ts b/packages/nuxt/src/vite/utils.ts index 59da97499550..c0df11485d5f 100644 --- a/packages/nuxt/src/vite/utils.ts +++ b/packages/nuxt/src/vite/utils.ts @@ -30,7 +30,7 @@ export function findDefaultSdkInitFile(type: 'server' | 'client'): string | unde * Extracts the filename from a node command with a path. */ export function getFilenameFromNodeStartCommand(nodeCommand: string): string | null { - const regex = /[^/\\]+$/; + const regex = /[^/\\]+\.[^/\\]+$/; const match = nodeCommand.match(regex); return match ? match[0] : null; } diff --git a/packages/nuxt/test/vite/utils.test.ts b/packages/nuxt/test/vite/utils.test.ts index f2f6b2b23c8d..24e5a601535e 100644 --- a/packages/nuxt/test/vite/utils.test.ts +++ b/packages/nuxt/test/vite/utils.test.ts @@ -107,6 +107,12 @@ describe('getFilenameFromPath', () => { const filename = getFilenameFromNodeStartCommand(path); expect(filename).toBeNull(); }); + + it('should return null for commands without file extensions', () => { + const path = 'npx @azure/static-web-apps-cli start .output/public --api-location .output/server'; + const filename = getFilenameFromNodeStartCommand(path); + expect(filename).toBeNull(); + }); }); describe('removeSentryQueryFromPath', () => { From a0be1a52508c2f4f7c3f34b53322ff02a38b3df8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Domagoj=20Gavrani=C4=87?= Date: Wed, 19 Feb 2025 16:58:03 +0100 Subject: [PATCH 18/32] feat(replay): Expose rrweb recordCrossOriginIframes under _experiments (#14916) Closes #14809. I have tested this with my project and it sort of works. While it's not perfect (sometimes produces an unplayable replay), it might unblock other users looking to experiment with this feature. Verify: - [ ] If you've added code that should be tested, please add tests. - [x] Ensure your code lints and the test suite passes (`yarn lint`) & (`yarn test`). This just exposes the API under experiments, as such I don't think it requires tests. --------- Co-authored-by: Billy Vong --- packages/replay-internal/src/integration.ts | 2 ++ packages/replay-internal/src/types/replay.ts | 5 +++++ packages/replay-internal/src/types/rrweb.ts | 1 + packages/replay-internal/test/integration/rrweb.test.ts | 3 +++ 4 files changed, 11 insertions(+) diff --git a/packages/replay-internal/src/integration.ts b/packages/replay-internal/src/integration.ts index c2a9206feb0d..4ec1a357eac4 100644 --- a/packages/replay-internal/src/integration.ts +++ b/packages/replay-internal/src/integration.ts @@ -150,6 +150,8 @@ export class Replay implements Integration { // this can happen if the error is frozen or does not allow mutation for other reasons } }, + // experimental support for recording iframes from different origins + recordCrossOriginIframes: Boolean(_experiments.recordCrossOriginIframes), }; this._initialOptions = { diff --git a/packages/replay-internal/src/types/replay.ts b/packages/replay-internal/src/types/replay.ts index 280db17db57a..cc8e6f1827ac 100644 --- a/packages/replay-internal/src/types/replay.ts +++ b/packages/replay-internal/src/types/replay.ts @@ -229,6 +229,11 @@ export interface ReplayPluginOptions extends ReplayNetworkOptions { captureExceptions: boolean; traceInternals: boolean; continuousCheckout: number; + /** + * Before enabling, please read the security considerations: + * https://github.com/rrweb-io/rrweb/blob/master/docs/recipes/cross-origin-iframes.md#considerations + */ + recordCrossOriginIframes: boolean; autoFlushOnFeedback: boolean; }>; } diff --git a/packages/replay-internal/src/types/rrweb.ts b/packages/replay-internal/src/types/rrweb.ts index 60e562cadf55..33f5e1b3bf7f 100644 --- a/packages/replay-internal/src/types/rrweb.ts +++ b/packages/replay-internal/src/types/rrweb.ts @@ -43,6 +43,7 @@ export type RrwebRecordOptions = { maskTextSelector?: string; blockSelector?: string; maskInputOptions?: Record; + recordCrossOriginIframes?: boolean; } & Record; export interface CanvasManagerInterface { diff --git a/packages/replay-internal/test/integration/rrweb.test.ts b/packages/replay-internal/test/integration/rrweb.test.ts index 7f156c542f08..0e6ada8b0d2a 100644 --- a/packages/replay-internal/test/integration/rrweb.test.ts +++ b/packages/replay-internal/test/integration/rrweb.test.ts @@ -40,6 +40,7 @@ describe('Integration | rrweb', () => { "maskTextFn": undefined, "maskTextSelector": ".sentry-mask,[data-sentry-mask]", "onMutation": [Function], + "recordCrossOriginIframes": false, "slimDOMOptions": "all", "unblockSelector": "", "unmaskTextSelector": "", @@ -80,6 +81,7 @@ describe('Integration | rrweb', () => { "maskTextFn": undefined, "maskTextSelector": ".sentry-mask,[data-sentry-mask]", "onMutation": [Function], + "recordCrossOriginIframes": false, "slimDOMOptions": "all", "unblockSelector": "", "unmaskTextSelector": "", @@ -131,6 +133,7 @@ describe('Integration | rrweb', () => { "maskTextFn": undefined, "maskTextSelector": ".sentry-mask,[data-sentry-mask]", "onMutation": [Function], + "recordCrossOriginIframes": false, "sampling": { "mousemove": false, }, From 6c69710a5e393d41fd519ddb7688004d24b4b8cc Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Thu, 20 Feb 2025 15:37:44 +0100 Subject: [PATCH 19/32] fix(sveltekit): Avoid loading vite config to determine source maps setting (#15440) - read sourcemap generation config in `config` hook of our source maps settings sub plugin - resolve the `filesToDeleteAfterUpload` promise whenever we know what to set it to (using a promise allows us to defer this decision to plugin hook runtime rather than plugin creation time) - adusted tests --- packages/sveltekit/package.json | 6 +- packages/sveltekit/src/vite/sourceMaps.ts | 174 +++++++++--------- .../sveltekit/test/vite/sourceMaps.test.ts | 33 ++-- yarn.lock | 66 ++++--- 4 files changed, 135 insertions(+), 144 deletions(-) diff --git a/packages/sveltekit/package.json b/packages/sveltekit/package.json index ba1535d799de..664f057ee808 100644 --- a/packages/sveltekit/package.json +++ b/packages/sveltekit/package.json @@ -9,9 +9,7 @@ "engines": { "node": ">=18" }, - "files": [ - "/build" - ], + "files": ["/build"], "main": "build/cjs/index.server.js", "module": "build/esm/index.server.js", "browser": "build/esm/index.client.js", @@ -44,7 +42,7 @@ "@sentry/node": "9.1.0", "@sentry/opentelemetry": "9.1.0", "@sentry/svelte": "9.1.0", - "@sentry/vite-plugin": "2.22.6", + "@sentry/vite-plugin": "3.2.0", "magic-string": "0.30.7", "magicast": "0.2.8", "sorcery": "1.0.0" diff --git a/packages/sveltekit/src/vite/sourceMaps.ts b/packages/sveltekit/src/vite/sourceMaps.ts index 799688b33845..78ee6389c5da 100644 --- a/packages/sveltekit/src/vite/sourceMaps.ts +++ b/packages/sveltekit/src/vite/sourceMaps.ts @@ -24,13 +24,6 @@ type Sorcery = { load(filepath: string): Promise; }; -type GlobalWithSourceMapSetting = typeof globalThis & { - _sentry_sourceMapSetting?: { - updatedSourceMapSetting?: boolean | 'inline' | 'hidden'; - previousSourceMapSetting?: UserSourceMapSetting; - }; -}; - // storing this in the module scope because `makeCustomSentryVitePlugin` is called multiple times // and we only want to generate a uuid once in case we have to fall back to it. const releaseName = detectSentryRelease(); @@ -57,8 +50,6 @@ export async function makeCustomSentryVitePlugins(options?: CustomSentryVitePlug const usedAdapter = options?.adapter || 'other'; const adapterOutputDir = await getAdapterOutputDir(svelteConfig, usedAdapter); - const globalWithSourceMapSetting = globalThis as GlobalWithSourceMapSetting; - const defaultPluginOptions: SentryVitePluginOptions = { release: { name: releaseName, @@ -70,61 +61,8 @@ export async function makeCustomSentryVitePlugins(options?: CustomSentryVitePlug }, }; - // Including all hidden (`.*`) directories by default so that folders like .vercel, - // .netlify, etc are also cleaned up. Additionally, we include the adapter output - // dir which could be a non-hidden directory, like `build` for the Node adapter. - const defaultFileDeletionGlob = ['./.*/**/*.map', `./${adapterOutputDir}/**/*.map`]; - - if (!globalWithSourceMapSetting._sentry_sourceMapSetting) { - let configFile: { - path: string; - config: UserConfig; - dependencies: string[]; - } | null = null; - - try { - // @ts-expect-error - the dynamic import here works fine - const Vite = await import('vite'); - configFile = await Vite.loadConfigFromFile({ command: 'build', mode: 'production' }); - } catch { - if (options?.debug) { - consoleSandbox(() => { - // eslint-disable-next-line no-console - console.warn( - '[Sentry] Could not import Vite to load your vite config. Please set `build.sourcemap` to `true` or `hidden` to enable source map generation.', - ); - }); - } - } - - if (configFile) { - globalWithSourceMapSetting._sentry_sourceMapSetting = getUpdatedSourceMapSetting(configFile.config); - } else { - if (options?.debug) { - consoleSandbox(() => { - // eslint-disable-next-line no-console - console.warn( - '[Sentry] Could not load Vite config with Vite "production" mode. This is needed for Sentry to automatically update source map settings.', - ); - }); - } - } - - if (options?.debug && globalWithSourceMapSetting._sentry_sourceMapSetting?.previousSourceMapSetting === 'unset') { - consoleSandbox(() => { - // eslint-disable-next-line no-console - console.warn( - `[Sentry] Automatically setting \`sourceMapsUploadOptions.sourcemaps.filesToDeleteAfterUpload: [${defaultFileDeletionGlob - .map(file => `"${file}"`) - .join(', ')}]\` to delete generated source maps after they were uploaded to Sentry.`, - ); - }); - } - } - - const shouldDeleteDefaultSourceMaps = - globalWithSourceMapSetting._sentry_sourceMapSetting?.previousSourceMapSetting === 'unset' && - !options?.sourcemaps?.filesToDeleteAfterUpload; + const { promise: filesToDeleteAfterUpload, resolve: resolveFilesToDeleteAfterUpload } = + createFilesToDeleteAfterUploadPromise(); const mergedOptions = { ...defaultPluginOptions, @@ -135,9 +73,7 @@ export async function makeCustomSentryVitePlugins(options?: CustomSentryVitePlug }, sourcemaps: { ...options?.sourcemaps, - filesToDeleteAfterUpload: shouldDeleteDefaultSourceMaps - ? defaultFileDeletionGlob - : options?.sourcemaps?.filesToDeleteAfterUpload, + filesToDeleteAfterUpload, }, }; @@ -163,6 +99,10 @@ export async function makeCustomSentryVitePlugins(options?: CustomSentryVitePlug console.warn( 'sentry-vite-debug-id-upload-plugin not found in sentryPlugins! Cannot modify plugin - returning default Sentry Vite plugins', ); + + // resolving filesToDeleteAfterUpload here, because we return the original deletion plugin which awaits the promise + resolveFilesToDeleteAfterUpload(undefined); + return sentryPlugins; } @@ -172,6 +112,10 @@ export async function makeCustomSentryVitePlugins(options?: CustomSentryVitePlug console.warn( 'sentry-file-deletion-plugin not found in sentryPlugins! Cannot modify plugin - returning default Sentry Vite plugins', ); + + // resolving filesToDeleteAfterUpload here, because we return the original deletion plugin which awaits the promise + resolveFilesToDeleteAfterUpload(undefined); + return sentryPlugins; } @@ -181,6 +125,10 @@ export async function makeCustomSentryVitePlugins(options?: CustomSentryVitePlug console.warn( 'sentry-release-management-plugin not found in sentryPlugins! Cannot modify plugin - returning default Sentry Vite plugins', ); + + // resolving filesToDeleteAfterUpload here, because we return the original deletion plugin which awaits the promise + resolveFilesToDeleteAfterUpload(undefined); + return sentryPlugins; } @@ -205,37 +153,66 @@ export async function makeCustomSentryVitePlugins(options?: CustomSentryVitePlug const sourceMapSettingsPlugin: Plugin = { name: 'sentry-sveltekit-update-source-map-setting-plugin', apply: 'build', // only apply this plugin at build time - config: (config: UserConfig) => { + config: async (config: UserConfig) => { const settingKey = 'build.sourcemap'; - if (globalWithSourceMapSetting._sentry_sourceMapSetting?.previousSourceMapSetting === 'unset') { + const { updatedSourceMapSetting, previousSourceMapSetting } = getUpdatedSourceMapSetting(config); + + const userProvidedFilesToDeleteAfterUpload = await options?.sourcemaps?.filesToDeleteAfterUpload; + + if (previousSourceMapSetting === 'unset') { consoleSandbox(() => { // eslint-disable-next-line no-console console.log(`[Sentry] Enabled source map generation in the build options with \`${settingKey}: "hidden"\`.`); }); + if (userProvidedFilesToDeleteAfterUpload) { + resolveFilesToDeleteAfterUpload(userProvidedFilesToDeleteAfterUpload); + } else { + // Including all hidden (`.*`) directories by default so that folders like .vercel, + // .netlify, etc are also cleaned up. Additionally, we include the adapter output + // dir which could be a non-hidden directory, like `build` for the Node adapter. + const defaultFileDeletionGlob = ['./.*/**/*.map', `./${adapterOutputDir}/**/*.map`]; + + consoleSandbox(() => { + // eslint-disable-next-line no-console + console.warn( + `[Sentry] Automatically setting \`sourceMapsUploadOptions.sourcemaps.filesToDeleteAfterUpload: [${defaultFileDeletionGlob + .map(file => `"${file}"`) + .join(', ')}]\` to delete generated source maps after they were uploaded to Sentry.`, + ); + }); + + // In case we enabled source maps and users didn't specify a glob patter to delete, we set a default pattern: + resolveFilesToDeleteAfterUpload(defaultFileDeletionGlob); + } + return { ...config, - build: { ...config.build, sourcemap: 'hidden' }, + build: { ...config.build, sourcemap: updatedSourceMapSetting }, }; - } else if (globalWithSourceMapSetting._sentry_sourceMapSetting?.previousSourceMapSetting === 'disabled') { + } + + if (previousSourceMapSetting === 'disabled') { consoleSandbox(() => { // eslint-disable-next-line no-console console.warn( `[Sentry] Parts of source map generation are currently disabled in your Vite configuration (\`${settingKey}: false\`). This setting is either a default setting or was explicitly set in your configuration. Sentry won't override this setting. Without source maps, code snippets on the Sentry Issues page will remain minified. To show unminified code, enable source maps in \`${settingKey}\` (e.g. by setting them to \`hidden\`).`, ); }); - } else if (globalWithSourceMapSetting._sentry_sourceMapSetting?.previousSourceMapSetting === 'enabled') { + } else if (previousSourceMapSetting === 'enabled') { if (mergedOptions?.debug) { consoleSandbox(() => { // eslint-disable-next-line no-console console.log( - `[Sentry] We discovered you enabled source map generation in your Vite configuration (\`${settingKey}\`). Sentry will keep this source map setting. This will un-minify the code snippet on the Sentry Issue page.`, + `[Sentry] We discovered you enabled source map generation in your Vite configuration (\`${settingKey}\`). Sentry will keep this source map setting. This will un-minify the code snippet on the Sentry Issue page.`, ); }); } } + resolveFilesToDeleteAfterUpload(userProvidedFilesToDeleteAfterUpload); + return config; }, }; @@ -423,7 +400,7 @@ export async function makeCustomSentryVitePlugins(options?: CustomSentryVitePlug /** * Whether the user enabled (true, 'hidden', 'inline') or disabled (false) source maps */ -export type UserSourceMapSetting = 'enabled' | 'disabled' | 'unset' | undefined; +type UserSourceMapSetting = 'enabled' | 'disabled' | 'unset' | undefined; /** There are 3 ways to set up source map generation (https://github.com/getsentry/sentry-javascript/issues/13993) * @@ -445,25 +422,25 @@ export function getUpdatedSourceMapSetting(viteConfig: { sourcemap?: boolean | 'inline' | 'hidden'; }; }): { updatedSourceMapSetting: boolean | 'inline' | 'hidden'; previousSourceMapSetting: UserSourceMapSetting } { - let previousSourceMapSetting: UserSourceMapSetting; - let updatedSourceMapSetting: boolean | 'inline' | 'hidden' | undefined; - viteConfig.build = viteConfig.build || {}; - const viteSourceMap = viteConfig.build.sourcemap; - - if (viteSourceMap === false) { - previousSourceMapSetting = 'disabled'; - updatedSourceMapSetting = viteSourceMap; - } else if (viteSourceMap && ['hidden', 'inline', true].includes(viteSourceMap)) { - previousSourceMapSetting = 'enabled'; - updatedSourceMapSetting = viteSourceMap; - } else { - previousSourceMapSetting = 'unset'; - updatedSourceMapSetting = 'hidden'; + const originalSourcemapSetting = viteConfig.build.sourcemap; + + if (originalSourcemapSetting === false) { + return { + previousSourceMapSetting: 'disabled', + updatedSourceMapSetting: originalSourcemapSetting, + }; + } + + if (originalSourcemapSetting && ['hidden', 'inline', true].includes(originalSourcemapSetting)) { + return { previousSourceMapSetting: 'enabled', updatedSourceMapSetting: originalSourcemapSetting }; } - return { previousSourceMapSetting, updatedSourceMapSetting }; + return { + previousSourceMapSetting: 'unset', + updatedSourceMapSetting: 'hidden', + }; } function getFiles(dir: string): string[] { @@ -499,3 +476,22 @@ function detectSentryRelease(): string { return release; } + +/** + * Creates a deferred promise that can be resolved/rejected by calling the + * `resolve` or `reject` function. + * Inspired by: https://stackoverflow.com/a/69027809 + */ +function createFilesToDeleteAfterUploadPromise(): { + promise: Promise; + resolve: (value: string | string[] | undefined) => void; + reject: (reason?: unknown) => void; +} { + let resolve!: (value: string | string[] | undefined) => void; + let reject!: (reason?: unknown) => void; + const promise = new Promise((res, rej) => { + resolve = res; + reject = rej; + }); + return { resolve, reject, promise }; +} diff --git a/packages/sveltekit/test/vite/sourceMaps.test.ts b/packages/sveltekit/test/vite/sourceMaps.test.ts index 378cbd2099e1..1410e9b992f0 100644 --- a/packages/sveltekit/test/vite/sourceMaps.test.ts +++ b/packages/sveltekit/test/vite/sourceMaps.test.ts @@ -1,5 +1,5 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'; -import { makeCustomSentryVitePlugins } from '../../src/vite/sourceMaps'; +import { getUpdatedSourceMapSetting, makeCustomSentryVitePlugins } from '../../src/vite/sourceMaps'; import type { Plugin } from 'vite'; @@ -113,7 +113,7 @@ describe('makeCustomSentryVitePlugins()', () => { const plugin = await getSentryViteSubPlugin('sentry-sveltekit-update-source-map-setting-plugin'); // @ts-expect-error this function exists! - const sentryConfig = plugin.config(originalConfig); + const sentryConfig = await plugin.config(originalConfig); expect(sentryConfig).toEqual(originalConfig); }); @@ -132,7 +132,7 @@ describe('makeCustomSentryVitePlugins()', () => { const plugin = await getSentryViteSubPlugin('sentry-sveltekit-update-source-map-setting-plugin'); // @ts-expect-error this function exists! - const sentryConfig = plugin.config(originalConfig); + const sentryConfig = await plugin.config(originalConfig); expect(sentryConfig).toEqual({ build: { @@ -155,7 +155,7 @@ describe('makeCustomSentryVitePlugins()', () => { const plugin = await getSentryViteSubPlugin('sentry-sveltekit-update-source-map-setting-plugin'); // @ts-expect-error this function exists! - const sentryConfig = plugin.config(originalConfig); + const sentryConfig = await plugin.config(originalConfig); expect(sentryConfig).toEqual({ ...originalConfig, build: { @@ -320,22 +320,23 @@ describe('makeCustomSentryVitePlugins()', () => { describe('changeViteSourceMapSettings()', () => { const cases = [ { sourcemap: false, expectedSourcemap: false, expectedPrevious: 'disabled' }, - { sourcemap: 'hidden', expectedSourcemap: 'hidden', expectedPrevious: 'enabled' }, - { sourcemap: 'inline', expectedSourcemap: 'inline', expectedPrevious: 'enabled' }, + { sourcemap: 'hidden' as const, expectedSourcemap: 'hidden', expectedPrevious: 'enabled' }, + { sourcemap: 'inline' as const, expectedSourcemap: 'inline', expectedPrevious: 'enabled' }, { sourcemap: true, expectedSourcemap: true, expectedPrevious: 'enabled' }, { sourcemap: undefined, expectedSourcemap: 'hidden', expectedPrevious: 'unset' }, ]; - it.each(cases)('handles vite source map settings $1', async ({ sourcemap, expectedSourcemap, expectedPrevious }) => { - const viteConfig = { build: { sourcemap } }; + it.each(cases)( + 'handles vite source map setting `build.sourcemap: $sourcemap`', + async ({ sourcemap, expectedSourcemap, expectedPrevious }) => { + const viteConfig = { build: { sourcemap } }; - const { getUpdatedSourceMapSetting } = await import('../../src/vite/sourceMaps'); + const result = getUpdatedSourceMapSetting(viteConfig); - const result = getUpdatedSourceMapSetting(viteConfig); - - expect(result).toEqual({ - updatedSourceMapSetting: expectedSourcemap, - previousSourceMapSetting: expectedPrevious, - }); - }); + expect(result).toEqual({ + updatedSourceMapSetting: expectedSourcemap, + previousSourceMapSetting: expectedPrevious, + }); + }, + ); }); diff --git a/yarn.lock b/yarn.lock index 8b41988eb8ec..0c7b60c816e1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6592,6 +6592,11 @@ resolved "https://registry.yarnpkg.com/@sentry/babel-plugin-component-annotate/-/babel-plugin-component-annotate-3.1.2.tgz#5497ca5adbe775955e96c566511a0bed3ab0a3ce" integrity sha512-5h2WXRJ6swKA0TwxHHryC8M2QyOfS9QhTAL6ElPfkEYe9HhJieXmxsDpyspbqAa26ccnCUcmwE5vL34jAjt4sQ== +"@sentry/babel-plugin-component-annotate@3.2.0": + version "3.2.0" + resolved "https://registry.yarnpkg.com/@sentry/babel-plugin-component-annotate/-/babel-plugin-component-annotate-3.2.0.tgz#17c000cf6cc315bb620eddbd95c88dfb2471cfb9" + integrity sha512-Sg7nLRP1yiJYl/KdGGxYGbjvLq5rswyeB5yESgfWX34XUNZaFgmNvw4pU/QEKVeYgcPyOulgJ+y80ewujyffTA== + "@sentry/bundler-plugin-core@2.22.6": version "2.22.6" resolved "https://registry.yarnpkg.com/@sentry/bundler-plugin-core/-/bundler-plugin-core-2.22.6.tgz#a1ea1fd43700a3ece9e7db016997e79a2782b87d" @@ -6620,6 +6625,20 @@ magic-string "0.30.8" unplugin "1.0.1" +"@sentry/bundler-plugin-core@3.2.0": + version "3.2.0" + resolved "https://registry.yarnpkg.com/@sentry/bundler-plugin-core/-/bundler-plugin-core-3.2.0.tgz#023ec92530a35fbec7c7077b7a8be2e79f0f9dd5" + integrity sha512-Q/ogVylue3XaFawyIxzuiic+7Dp4w63eJtRtVH8VBebNURyJ/re4GVoP1QNGccE1R243tXY1y2GiwqiJkAONOg== + dependencies: + "@babel/core" "^7.18.5" + "@sentry/babel-plugin-component-annotate" "3.2.0" + "@sentry/cli" "2.41.1" + dotenv "^16.3.1" + find-up "^5.0.0" + glob "^9.3.2" + magic-string "0.30.8" + unplugin "1.0.1" + "@sentry/cli-darwin@2.41.1": version "2.41.1" resolved "https://registry.yarnpkg.com/@sentry/cli-darwin/-/cli-darwin-2.41.1.tgz#ca7e12bf1ad59bc2df35868ae98abc8869108efa" @@ -6690,6 +6709,14 @@ "@sentry/bundler-plugin-core" "2.22.6" unplugin "1.0.1" +"@sentry/vite-plugin@3.2.0": + version "3.2.0" + resolved "https://registry.yarnpkg.com/@sentry/vite-plugin/-/vite-plugin-3.2.0.tgz#0785b6e04e0aed8a4d6b57a433a2da11c14e6cd0" + integrity sha512-IVBoAzZmpoX9+mnmIMq2ndxlFPoWMuYSE5Mek5zOWpYh+GbPxvkrxvM+vg0HeLH4r5v9Tm0FWcEZDgDIZqtoSg== + dependencies: + "@sentry/bundler-plugin-core" "3.2.0" + unplugin "1.0.1" + "@sentry/webpack-plugin@3.1.2": version "3.1.2" resolved "https://registry.yarnpkg.com/@sentry/webpack-plugin/-/webpack-plugin-3.1.2.tgz#e7cf2b10b6d2fb2d6106e692469d02b6ab684bba" @@ -7903,12 +7930,7 @@ dependencies: "@types/unist" "*" -"@types/history-4@npm:@types/history@4.7.8": - version "4.7.8" - resolved "https://registry.yarnpkg.com/@types/history/-/history-4.7.8.tgz#49348387983075705fe8f4e02fb67f7daaec4934" - integrity sha512-S78QIYirQcUoo6UJZx9CSP0O2ix9IaeAXwQi26Rhr/+mg7qqPy8TzaxHSUut7eGjL8WmLccT7/MXf304WjqHcA== - -"@types/history-5@npm:@types/history@4.7.8": +"@types/history-4@npm:@types/history@4.7.8", "@types/history-5@npm:@types/history@4.7.8": version "4.7.8" resolved "https://registry.yarnpkg.com/@types/history/-/history-4.7.8.tgz#49348387983075705fe8f4e02fb67f7daaec4934" integrity sha512-S78QIYirQcUoo6UJZx9CSP0O2ix9IaeAXwQi26Rhr/+mg7qqPy8TzaxHSUut7eGjL8WmLccT7/MXf304WjqHcA== @@ -27443,16 +27465,7 @@ string-template@~0.2.1: resolved "https://registry.yarnpkg.com/string-template/-/string-template-0.2.1.tgz#42932e598a352d01fc22ec3367d9d84eec6c9add" integrity sha1-QpMuWYo1LQH8IuwzZ9nYTuxsmt0= -"string-width-cjs@npm:string-width@^4.2.0": - version "4.2.3" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" - integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== - dependencies: - emoji-regex "^8.0.0" - is-fullwidth-code-point "^3.0.0" - strip-ansi "^6.0.1" - -string-width@4.2.3, "string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.0.0, string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.2, string-width@^4.2.3: +"string-width-cjs@npm:string-width@^4.2.0", string-width@4.2.3, "string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.0.0, string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.2, string-width@^4.2.3: version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -27555,14 +27568,7 @@ stringify-object@^3.2.1: is-obj "^1.0.1" is-regexp "^1.0.0" -"strip-ansi-cjs@npm:strip-ansi@^6.0.1": - version "6.0.1" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" - integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== - dependencies: - ansi-regex "^5.0.1" - -strip-ansi@6.0.1, strip-ansi@^6.0.0, strip-ansi@^6.0.1: +"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@6.0.1, strip-ansi@^6.0.0, strip-ansi@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== @@ -27727,7 +27733,6 @@ stylus@0.59.0, stylus@^0.59.0: sucrase@^3.27.0, sucrase@^3.35.0, sucrase@getsentry/sucrase#es2020-polyfills: version "3.36.0" - uid fd682f6129e507c00bb4e6319cc5d6b767e36061 resolved "https://codeload.github.com/getsentry/sucrase/tar.gz/fd682f6129e507c00bb4e6319cc5d6b767e36061" dependencies: "@jridgewell/gen-mapping" "^0.3.2" @@ -30363,16 +30368,7 @@ wrangler@^3.67.1: optionalDependencies: fsevents "~2.3.2" -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": - version "7.0.0" - resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" - integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== - dependencies: - ansi-styles "^4.0.0" - string-width "^4.1.0" - strip-ansi "^6.0.0" - -wrap-ansi@7.0.0, wrap-ansi@^7.0.0: +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@7.0.0, wrap-ansi@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== From d804dd41f4c5819ec51fcaad4e00c11373d84d77 Mon Sep 17 00:00:00 2001 From: Tim Fish Date: Thu, 20 Feb 2025 17:49:04 +0100 Subject: [PATCH 20/32] chore(profiling-node): Remove duplicate types (#15427) These types were moved to `@sentry-internal/node-cpu-profiler` but I didn't remove them here during the migration. --- packages/profiling-node/src/integration.ts | 5 +- .../profiling-node/src/spanProfileUtils.ts | 3 +- packages/profiling-node/src/types.ts | 84 ------------------- packages/profiling-node/src/utils.ts | 2 +- packages/profiling-node/test/utils.test.ts | 7 +- 5 files changed, 9 insertions(+), 92 deletions(-) delete mode 100644 packages/profiling-node/src/types.ts diff --git a/packages/profiling-node/src/integration.ts b/packages/profiling-node/src/integration.ts index 4fffdf490901..e134a272b929 100644 --- a/packages/profiling-node/src/integration.ts +++ b/packages/profiling-node/src/integration.ts @@ -1,6 +1,5 @@ /* eslint-disable max-lines */ - -import { CpuProfilerBindings } from '@sentry-internal/node-cpu-profiler'; +import { CpuProfilerBindings, ProfileFormat, type RawThreadCpuProfile } from '@sentry-internal/node-cpu-profiler'; import type { Event, IntegrationFn, Profile, ProfileChunk, ProfilingIntegration, Span } from '@sentry/core'; import { LRUMap, @@ -18,8 +17,6 @@ import type { NodeClient } from '@sentry/node'; import { DEBUG_BUILD } from './debug-build'; import { NODE_MAJOR, NODE_VERSION } from './nodeVersion'; import { MAX_PROFILE_DURATION_MS, maybeProfileSpan, stopSpanProfile } from './spanProfileUtils'; -import type { RawThreadCpuProfile } from './types'; -import { ProfileFormat } from './types'; import { PROFILER_THREAD_ID_STRING, PROFILER_THREAD_NAME, diff --git a/packages/profiling-node/src/spanProfileUtils.ts b/packages/profiling-node/src/spanProfileUtils.ts index 342075bde890..9ff20816895c 100644 --- a/packages/profiling-node/src/spanProfileUtils.ts +++ b/packages/profiling-node/src/spanProfileUtils.ts @@ -1,9 +1,8 @@ -import { CpuProfilerBindings } from '@sentry-internal/node-cpu-profiler'; +import { CpuProfilerBindings, type RawThreadCpuProfile } from '@sentry-internal/node-cpu-profiler'; import type { CustomSamplingContext, Span } from '@sentry/core'; import { logger, spanIsSampled, spanToJSON, uuid4 } from '@sentry/core'; import type { NodeClient } from '@sentry/node'; import { DEBUG_BUILD } from './debug-build'; -import type { RawThreadCpuProfile } from './types'; import { isValidSampleRate } from './utils'; export const MAX_PROFILE_DURATION_MS = 30 * 1000; diff --git a/packages/profiling-node/src/types.ts b/packages/profiling-node/src/types.ts deleted file mode 100644 index 9b8f039b3c95..000000000000 --- a/packages/profiling-node/src/types.ts +++ /dev/null @@ -1,84 +0,0 @@ -import type { Event } from '@sentry/core'; - -interface Sample { - stack_id: number; - thread_id: string; - elapsed_since_start_ns: string; -} - -interface ChunkSample { - stack_id: number; - thread_id: string; - timestamp: number; -} - -type Frame = { - function: string; - file: string; - lineno: number; - colno: number; -}; - -interface Measurement { - unit: string; - values: { - elapsed_since_start_ns: number; - value: number; - }[]; -} - -// Profile is marked as optional because it is deleted from the metadata -// by the integration before the event is processed by other integrations. -export interface ProfiledEvent extends Event { - sdkProcessingMetadata: { - profile?: RawThreadCpuProfile; - }; -} - -interface BaseProfile { - profile_id?: string; - stacks: number[][]; - frames: Frame[]; - resources: string[]; - profiler_logging_mode: 'eager' | 'lazy'; - measurements: Record; -} -export interface RawThreadCpuProfile extends BaseProfile { - samples: Sample[]; -} - -export interface RawChunkCpuProfile extends BaseProfile { - samples: ChunkSample[]; -} - -export interface PrivateV8CpuProfilerBindings { - startProfiling?: (name: string) => void; - - stopProfiling?( - name: string, - format: ProfileFormat.THREAD, - threadId: number, - collectResources: boolean, - ): RawThreadCpuProfile | null; - stopProfiling?( - name: string, - format: ProfileFormat.CHUNK, - threadId: number, - collectResources: boolean, - ): RawChunkCpuProfile | null; - - // Helper methods exposed for testing - getFrameModule(abs_path: string): string; -} - -export enum ProfileFormat { - THREAD = 0, - CHUNK = 1, -} - -export interface V8CpuProfilerBindings { - startProfiling(name: string): void; - - stopProfiling(name: string, format: ProfileFormat.THREAD): RawThreadCpuProfile | null; - stopProfiling(name: string, format: ProfileFormat.CHUNK): RawChunkCpuProfile | null; -} diff --git a/packages/profiling-node/src/utils.ts b/packages/profiling-node/src/utils.ts index e6ab3803ebdd..23b05f14f67b 100644 --- a/packages/profiling-node/src/utils.ts +++ b/packages/profiling-node/src/utils.ts @@ -27,8 +27,8 @@ import { import { env, versions } from 'process'; import { isMainThread, threadId } from 'worker_threads'; +import type { RawChunkCpuProfile, RawThreadCpuProfile } from '@sentry-internal/node-cpu-profiler'; import { DEBUG_BUILD } from './debug-build'; -import type { RawChunkCpuProfile, RawThreadCpuProfile } from './types'; // We require the file because if we import it, it will be included in the bundle. // I guess tsc does not check file contents when it's imported. diff --git a/packages/profiling-node/test/utils.test.ts b/packages/profiling-node/test/utils.test.ts index dac0bf16be79..2e5b79e9baee 100644 --- a/packages/profiling-node/test/utils.test.ts +++ b/packages/profiling-node/test/utils.test.ts @@ -1,6 +1,7 @@ import { addItemToEnvelope, createEnvelope, uuid4 } from '@sentry/core'; import type { Event } from '@sentry/core'; +import type { RawThreadCpuProfile } from '@sentry-internal/node-cpu-profiler'; import { addProfilesToEnvelope, findProfiledTransactionsFromEnvelope, @@ -8,7 +9,11 @@ import { isValidSampleRate, } from '../src/utils'; -import type { ProfiledEvent } from '../src/types'; +interface ProfiledEvent extends Event { + sdkProcessingMetadata: { + profile?: RawThreadCpuProfile; + }; +} function makeProfile( props: Partial, From 1aa5bbe78254a653bb69b34b2232d92edbc4e9eb Mon Sep 17 00:00:00 2001 From: Tim Fish Date: Thu, 20 Feb 2025 17:50:45 +0100 Subject: [PATCH 21/32] feat(node): Support Express v5 (#15380) The goal is for Express to eventually support publishing events to `diagnostics_channel` (#15107) so that Open Telemetry can instrument it without monkey-patching internal code. However, this might take a while and it would be great to support Express v5 now. This PR is a stop-gap solution until that work is complete and published. This PR vendors the code added in my otel PR: - https://github.com/open-telemetry/opentelemetry-js-contrib/pull/2437 - Adds a new instrumentation specifically for hooking express v5 - Copies the Express v4 integration tests to test v5 - The only changes in the tests is the removal of a couple of complex regex tests where the regexes are no longer supported by Express. - Modifies the NestJs v11 tests which now support full Express spans --- .../nestjs-11/tests/errors.test.ts | 12 +- .../nestjs-11/tests/transactions.test.ts | 15 +- .../node-integration-tests/package.json | 2 + .../handle-error-scope-data-loss/server.ts | 31 ++ .../handle-error-scope-data-loss/test.ts | 85 +++++ .../handle-error-tracesSampleRate-0/server.ts | 22 ++ .../handle-error-tracesSampleRate-0/test.ts | 38 ++ .../server.ts | 21 ++ .../test.ts | 37 ++ .../suites/express-v5/multiple-init/server.ts | 74 ++++ .../suites/express-v5/multiple-init/test.ts | 70 ++++ .../common-infix-parameterized/server.ts | 33 ++ .../common-infix-parameterized/test.ts | 13 + .../multiple-routers/common-infix/server.ts | 34 ++ .../multiple-routers/common-infix/test.ts | 13 + .../server.ts | 33 ++ .../test.ts | 13 + .../common-prefix-parameterized/server.ts | 33 ++ .../common-prefix-parameterized/test.ts | 13 + .../server.ts | 33 ++ .../test.ts | 13 + .../server.ts | 33 ++ .../test.ts | 13 + .../multiple-routers/common-prefix/server.ts | 33 ++ .../multiple-routers/common-prefix/test.ts | 13 + .../multiple-routers/complex-router/server.ts | 33 ++ .../multiple-routers/complex-router/test.ts | 52 +++ .../middle-layer-parameterized/server.ts | 33 ++ .../middle-layer-parameterized/test.ts | 23 ++ .../suites/express-v5/package.json | 6 + .../suites/express-v5/requestUser/server.js | 49 +++ .../suites/express-v5/requestUser/test.ts | 42 +++ .../baggage-header-assign/test.ts | 147 ++++++++ .../sentry-trace/baggage-header-out/server.ts | 38 ++ .../sentry-trace/baggage-header-out/test.ts | 35 ++ .../server.ts | 43 +++ .../test.ts | 69 ++++ .../baggage-other-vendors/server.ts | 37 ++ .../baggage-other-vendors/test.ts | 25 ++ .../baggage-transaction-name/server.ts | 37 ++ .../baggage-transaction-name/test.ts | 20 ++ .../suites/express-v5/sentry-trace/server.ts | 33 ++ .../trace-header-assign/server.ts | 32 ++ .../sentry-trace/trace-header-assign/test.ts | 27 ++ .../sentry-trace/trace-header-out/test.ts | 23 ++ .../setupExpressErrorHandler/server.js | 33 ++ .../setupExpressErrorHandler/test.ts | 30 ++ .../express-v5/span-isolationScope/server.ts | 29 ++ .../express-v5/span-isolationScope/test.ts | 38 ++ .../suites/express-v5/tracing/server.js | 48 +++ .../suites/express-v5/tracing/test.ts | 240 +++++++++++++ .../scenario-normalizedRequest.js | 34 ++ .../tracing/tracesSampler/server.js | 39 +++ .../express-v5/tracing/tracesSampler/test.ts | 44 +++ .../express-v5/tracing/updateName/server.js | 58 ++++ .../express-v5/tracing/updateName/test.ts | 94 +++++ .../express-v5/tracing/withError/server.js | 30 ++ .../express-v5/tracing/withError/test.ts | 28 ++ .../express-v5/without-tracing/server.ts | 40 +++ .../suites/express-v5/without-tracing/test.ts | 132 +++++++ .../express-v5/enums/AttributeNames.ts | 19 + .../express-v5/enums/ExpressLayerType.ts | 20 ++ .../tracing/express-v5/instrumentation.ts | 324 ++++++++++++++++++ .../tracing/express-v5/internal-types.ts | 63 ++++ .../integrations/tracing/express-v5/types.ts | 65 ++++ .../integrations/tracing/express-v5/utils.ts | 191 +++++++++++ .../node/src/integrations/tracing/express.ts | 79 +++-- .../node/src/integrations/tracing/index.ts | 3 +- 68 files changed, 3166 insertions(+), 49 deletions(-) create mode 100644 dev-packages/node-integration-tests/suites/express-v5/handle-error-scope-data-loss/server.ts create mode 100644 dev-packages/node-integration-tests/suites/express-v5/handle-error-scope-data-loss/test.ts create mode 100644 dev-packages/node-integration-tests/suites/express-v5/handle-error-tracesSampleRate-0/server.ts create mode 100644 dev-packages/node-integration-tests/suites/express-v5/handle-error-tracesSampleRate-0/test.ts create mode 100644 dev-packages/node-integration-tests/suites/express-v5/handle-error-tracesSampleRate-unset/server.ts create mode 100644 dev-packages/node-integration-tests/suites/express-v5/handle-error-tracesSampleRate-unset/test.ts create mode 100644 dev-packages/node-integration-tests/suites/express-v5/multiple-init/server.ts create mode 100644 dev-packages/node-integration-tests/suites/express-v5/multiple-init/test.ts create mode 100644 dev-packages/node-integration-tests/suites/express-v5/multiple-routers/common-infix-parameterized/server.ts create mode 100644 dev-packages/node-integration-tests/suites/express-v5/multiple-routers/common-infix-parameterized/test.ts create mode 100644 dev-packages/node-integration-tests/suites/express-v5/multiple-routers/common-infix/server.ts create mode 100644 dev-packages/node-integration-tests/suites/express-v5/multiple-routers/common-infix/test.ts create mode 100644 dev-packages/node-integration-tests/suites/express-v5/multiple-routers/common-prefix-parameterized-reverse/server.ts create mode 100644 dev-packages/node-integration-tests/suites/express-v5/multiple-routers/common-prefix-parameterized-reverse/test.ts create mode 100644 dev-packages/node-integration-tests/suites/express-v5/multiple-routers/common-prefix-parameterized/server.ts create mode 100644 dev-packages/node-integration-tests/suites/express-v5/multiple-routers/common-prefix-parameterized/test.ts create mode 100644 dev-packages/node-integration-tests/suites/express-v5/multiple-routers/common-prefix-same-length-parameterized copy/server.ts create mode 100644 dev-packages/node-integration-tests/suites/express-v5/multiple-routers/common-prefix-same-length-parameterized copy/test.ts create mode 100644 dev-packages/node-integration-tests/suites/express-v5/multiple-routers/common-prefix-same-length-parameterized/server.ts create mode 100644 dev-packages/node-integration-tests/suites/express-v5/multiple-routers/common-prefix-same-length-parameterized/test.ts create mode 100644 dev-packages/node-integration-tests/suites/express-v5/multiple-routers/common-prefix/server.ts create mode 100644 dev-packages/node-integration-tests/suites/express-v5/multiple-routers/common-prefix/test.ts create mode 100644 dev-packages/node-integration-tests/suites/express-v5/multiple-routers/complex-router/server.ts create mode 100644 dev-packages/node-integration-tests/suites/express-v5/multiple-routers/complex-router/test.ts create mode 100644 dev-packages/node-integration-tests/suites/express-v5/multiple-routers/middle-layer-parameterized/server.ts create mode 100644 dev-packages/node-integration-tests/suites/express-v5/multiple-routers/middle-layer-parameterized/test.ts create mode 100644 dev-packages/node-integration-tests/suites/express-v5/package.json create mode 100644 dev-packages/node-integration-tests/suites/express-v5/requestUser/server.js create mode 100644 dev-packages/node-integration-tests/suites/express-v5/requestUser/test.ts create mode 100644 dev-packages/node-integration-tests/suites/express-v5/sentry-trace/baggage-header-assign/test.ts create mode 100644 dev-packages/node-integration-tests/suites/express-v5/sentry-trace/baggage-header-out/server.ts create mode 100644 dev-packages/node-integration-tests/suites/express-v5/sentry-trace/baggage-header-out/test.ts create mode 100644 dev-packages/node-integration-tests/suites/express-v5/sentry-trace/baggage-other-vendors-with-sentry-entries/server.ts create mode 100644 dev-packages/node-integration-tests/suites/express-v5/sentry-trace/baggage-other-vendors-with-sentry-entries/test.ts create mode 100644 dev-packages/node-integration-tests/suites/express-v5/sentry-trace/baggage-other-vendors/server.ts create mode 100644 dev-packages/node-integration-tests/suites/express-v5/sentry-trace/baggage-other-vendors/test.ts create mode 100644 dev-packages/node-integration-tests/suites/express-v5/sentry-trace/baggage-transaction-name/server.ts create mode 100644 dev-packages/node-integration-tests/suites/express-v5/sentry-trace/baggage-transaction-name/test.ts create mode 100644 dev-packages/node-integration-tests/suites/express-v5/sentry-trace/server.ts create mode 100644 dev-packages/node-integration-tests/suites/express-v5/sentry-trace/trace-header-assign/server.ts create mode 100644 dev-packages/node-integration-tests/suites/express-v5/sentry-trace/trace-header-assign/test.ts create mode 100644 dev-packages/node-integration-tests/suites/express-v5/sentry-trace/trace-header-out/test.ts create mode 100644 dev-packages/node-integration-tests/suites/express-v5/setupExpressErrorHandler/server.js create mode 100644 dev-packages/node-integration-tests/suites/express-v5/setupExpressErrorHandler/test.ts create mode 100644 dev-packages/node-integration-tests/suites/express-v5/span-isolationScope/server.ts create mode 100644 dev-packages/node-integration-tests/suites/express-v5/span-isolationScope/test.ts create mode 100644 dev-packages/node-integration-tests/suites/express-v5/tracing/server.js create mode 100644 dev-packages/node-integration-tests/suites/express-v5/tracing/test.ts create mode 100644 dev-packages/node-integration-tests/suites/express-v5/tracing/tracesSampler/scenario-normalizedRequest.js create mode 100644 dev-packages/node-integration-tests/suites/express-v5/tracing/tracesSampler/server.js create mode 100644 dev-packages/node-integration-tests/suites/express-v5/tracing/tracesSampler/test.ts create mode 100644 dev-packages/node-integration-tests/suites/express-v5/tracing/updateName/server.js create mode 100644 dev-packages/node-integration-tests/suites/express-v5/tracing/updateName/test.ts create mode 100644 dev-packages/node-integration-tests/suites/express-v5/tracing/withError/server.js create mode 100644 dev-packages/node-integration-tests/suites/express-v5/tracing/withError/test.ts create mode 100644 dev-packages/node-integration-tests/suites/express-v5/without-tracing/server.ts create mode 100644 dev-packages/node-integration-tests/suites/express-v5/without-tracing/test.ts create mode 100644 packages/node/src/integrations/tracing/express-v5/enums/AttributeNames.ts create mode 100644 packages/node/src/integrations/tracing/express-v5/enums/ExpressLayerType.ts create mode 100644 packages/node/src/integrations/tracing/express-v5/instrumentation.ts create mode 100644 packages/node/src/integrations/tracing/express-v5/internal-types.ts create mode 100644 packages/node/src/integrations/tracing/express-v5/types.ts create mode 100644 packages/node/src/integrations/tracing/express-v5/utils.ts diff --git a/dev-packages/e2e-tests/test-applications/nestjs-11/tests/errors.test.ts b/dev-packages/e2e-tests/test-applications/nestjs-11/tests/errors.test.ts index 0fa13fea32aa..a24d1010eca4 100644 --- a/dev-packages/e2e-tests/test-applications/nestjs-11/tests/errors.test.ts +++ b/dev-packages/e2e-tests/test-applications/nestjs-11/tests/errors.test.ts @@ -50,13 +50,11 @@ test('Does not send HttpExceptions to Sentry', async ({ baseURL }) => { }); const transactionEventPromise400 = waitForTransaction('nestjs-11', transactionEvent => { - // todo(express-5): parametrize /test-expected-400-exception/:id - return transactionEvent?.transaction === 'GET /test-expected-400-exception/123'; + return transactionEvent?.transaction === 'GET /test-expected-400-exception/:id'; }); const transactionEventPromise500 = waitForTransaction('nestjs-11', transactionEvent => { - // todo(express-5): parametrize /test-expected-500-exception/:id - return transactionEvent?.transaction === 'GET /test-expected-500-exception/123'; + return transactionEvent?.transaction === 'GET /test-expected-500-exception/:id'; }); const response400 = await fetch(`${baseURL}/test-expected-400-exception/123`); @@ -81,13 +79,11 @@ test('Does not send RpcExceptions to Sentry', async ({ baseURL }) => { errorEventOccurred = true; } - // todo(express-5): parametrize /test-expected-rpc-exception/:id - return event?.transaction === 'GET /test-expected-rpc-exception/123'; + return event?.transaction === 'GET /test-expected-rpc-exception/:id'; }); const transactionEventPromise = waitForTransaction('nestjs-11', transactionEvent => { - // todo(express-5): parametrize /test-expected-rpc-exception/:id - return transactionEvent?.transaction === 'GET /test-expected-rpc-exception/123'; + return transactionEvent?.transaction === 'GET /test-expected-rpc-exception/:id'; }); const response = await fetch(`${baseURL}/test-expected-rpc-exception/123`); diff --git a/dev-packages/e2e-tests/test-applications/nestjs-11/tests/transactions.test.ts b/dev-packages/e2e-tests/test-applications/nestjs-11/tests/transactions.test.ts index 7e0947d53ec1..1209eae1ada9 100644 --- a/dev-packages/e2e-tests/test-applications/nestjs-11/tests/transactions.test.ts +++ b/dev-packages/e2e-tests/test-applications/nestjs-11/tests/transactions.test.ts @@ -15,7 +15,7 @@ test('Sends an API route transaction', async ({ baseURL }) => { expect(transactionEvent.contexts?.trace).toEqual({ data: { - 'sentry.source': 'url', // todo(express-5): 'route' + 'sentry.source': 'route', 'sentry.origin': 'auto.http.otel.http', 'sentry.op': 'http.server', 'sentry.sample_rate': 1, @@ -37,7 +37,7 @@ test('Sends an API route transaction', async ({ baseURL }) => { 'net.peer.port': expect.any(Number), 'http.status_code': 200, 'http.status_text': 'OK', - // 'http.route': '/test-transaction', // todo(express-5): add this line again + 'http.route': '/test-transaction', }, op: 'http.server', span_id: expect.stringMatching(/[a-f0-9]{16}/), @@ -49,7 +49,6 @@ test('Sends an API route transaction', async ({ baseURL }) => { expect(transactionEvent).toEqual( expect.objectContaining({ spans: expect.arrayContaining([ - /* todo(express-5): add this part again { data: { 'express.name': '/test-transaction', @@ -67,7 +66,7 @@ test('Sends an API route transaction', async ({ baseURL }) => { timestamp: expect.any(Number), trace_id: expect.stringMatching(/[a-f0-9]{32}/), origin: 'auto.http.otel.express', - }, */ + }, { data: { 'sentry.origin': 'manual', @@ -117,7 +116,7 @@ test('Sends an API route transaction', async ({ baseURL }) => { transaction: 'GET /test-transaction', type: 'transaction', transaction_info: { - source: 'url', // todo(express-5): 'route' + source: 'route', }, }), ); @@ -272,8 +271,7 @@ test('API route transaction includes nest pipe span for valid request', async ({ const transactionEventPromise = waitForTransaction('nestjs-11', transactionEvent => { return ( transactionEvent?.contexts?.trace?.op === 'http.server' && - // todo(express-5): parametrize test-pipe-instrumentation/:id - transactionEvent?.transaction === 'GET /test-pipe-instrumentation/123' && + transactionEvent?.transaction === 'GET /test-pipe-instrumentation/:id' && transactionEvent?.request?.url?.includes('/test-pipe-instrumentation/123') ); }); @@ -310,8 +308,7 @@ test('API route transaction includes nest pipe span for invalid request', async const transactionEventPromise = waitForTransaction('nestjs-11', transactionEvent => { return ( transactionEvent?.contexts?.trace?.op === 'http.server' && - // todo(express-5): parametrize test-pipe-instrumentation/:id - transactionEvent?.transaction === 'GET /test-pipe-instrumentation/abc' && + transactionEvent?.transaction === 'GET /test-pipe-instrumentation/:id' && transactionEvent?.request?.url?.includes('/test-pipe-instrumentation/abc') ); }); diff --git a/dev-packages/node-integration-tests/package.json b/dev-packages/node-integration-tests/package.json index 9d89dea67940..bbb7e300ecee 100644 --- a/dev-packages/node-integration-tests/package.json +++ b/dev-packages/node-integration-tests/package.json @@ -16,9 +16,11 @@ "build:types": "tsc -p tsconfig.types.json", "clean": "rimraf -g **/node_modules && run-p clean:script", "clean:script": "node scripts/clean.js", + "express-v5-install": "cd suites/express-v5 && yarn --no-lockfile", "lint": "eslint . --format stylish", "fix": "eslint . --format stylish --fix", "type-check": "tsc", + "pretest": "yarn express-v5-install", "test": "jest --config ./jest.config.js", "test:no-prisma": "jest --config ./jest.config.js", "test:watch": "yarn test --watch" diff --git a/dev-packages/node-integration-tests/suites/express-v5/handle-error-scope-data-loss/server.ts b/dev-packages/node-integration-tests/suites/express-v5/handle-error-scope-data-loss/server.ts new file mode 100644 index 000000000000..079d9834b01c --- /dev/null +++ b/dev-packages/node-integration-tests/suites/express-v5/handle-error-scope-data-loss/server.ts @@ -0,0 +1,31 @@ +import { loggingTransport } from '@sentry-internal/node-integration-tests'; +import * as Sentry from '@sentry/node'; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', + transport: loggingTransport, +}); + +import { startExpressServerAndSendPortToRunner } from '@sentry-internal/node-integration-tests'; +import express from 'express'; + +const app = express(); + +Sentry.setTag('global', 'tag'); + +app.get('/test/withScope', () => { + Sentry.withScope(scope => { + scope.setTag('local', 'tag'); + throw new Error('test_error'); + }); +}); + +app.get('/test/isolationScope', () => { + Sentry.getIsolationScope().setTag('isolation-scope', 'tag'); + throw new Error('isolation_test_error'); +}); + +Sentry.setupExpressErrorHandler(app); + +startExpressServerAndSendPortToRunner(app); diff --git a/dev-packages/node-integration-tests/suites/express-v5/handle-error-scope-data-loss/test.ts b/dev-packages/node-integration-tests/suites/express-v5/handle-error-scope-data-loss/test.ts new file mode 100644 index 000000000000..58d4a299174c --- /dev/null +++ b/dev-packages/node-integration-tests/suites/express-v5/handle-error-scope-data-loss/test.ts @@ -0,0 +1,85 @@ +import { cleanupChildProcesses, createRunner } from '../../../utils/runner'; + +afterAll(() => { + cleanupChildProcesses(); +}); + +/** + * Why does this test exist? + * + * We recently discovered that errors caught by global handlers will potentially loose scope data from the active scope + * where the error was originally thrown in. The simple example in this test (see subject.ts) demonstrates this behavior + * (in a Node environment but the same behavior applies to the browser; see the test there). + * + * This test nevertheless covers the behavior so that we're aware. + */ +test('withScope scope is NOT applied to thrown error caught by global handler', done => { + createRunner(__dirname, 'server.ts') + .expect({ + event: { + exception: { + values: [ + { + mechanism: { + type: 'middleware', + handled: false, + }, + type: 'Error', + value: 'test_error', + stacktrace: { + frames: expect.arrayContaining([ + expect.objectContaining({ + function: expect.any(String), + lineno: expect.any(Number), + colno: expect.any(Number), + }), + ]), + }, + }, + ], + }, + // 'local' tag is not applied to the event + tags: expect.not.objectContaining({ local: expect.anything() }), + }, + }) + .start(done) + .makeRequest('get', '/test/withScope', { expectError: true }); +}); + +/** + * This test shows that the isolation scope set tags are applied correctly to the error. + */ +test('isolation scope is applied to thrown error caught by global handler', done => { + createRunner(__dirname, 'server.ts') + .expect({ + event: { + exception: { + values: [ + { + mechanism: { + type: 'middleware', + handled: false, + }, + type: 'Error', + value: 'isolation_test_error', + stacktrace: { + frames: expect.arrayContaining([ + expect.objectContaining({ + function: expect.any(String), + lineno: expect.any(Number), + colno: expect.any(Number), + }), + ]), + }, + }, + ], + }, + tags: { + global: 'tag', + 'isolation-scope': 'tag', + }, + }, + }) + .start(done) + .makeRequest('get', '/test/isolationScope', { expectError: true }); +}); diff --git a/dev-packages/node-integration-tests/suites/express-v5/handle-error-tracesSampleRate-0/server.ts b/dev-packages/node-integration-tests/suites/express-v5/handle-error-tracesSampleRate-0/server.ts new file mode 100644 index 000000000000..3f52580dda1d --- /dev/null +++ b/dev-packages/node-integration-tests/suites/express-v5/handle-error-tracesSampleRate-0/server.ts @@ -0,0 +1,22 @@ +import { loggingTransport } from '@sentry-internal/node-integration-tests'; +import * as Sentry from '@sentry/node'; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', + transport: loggingTransport, + tracesSampleRate: 1, +}); + +import { startExpressServerAndSendPortToRunner } from '@sentry-internal/node-integration-tests'; +import express from 'express'; + +const app = express(); + +app.get('/test/express/:id', req => { + throw new Error(`test_error with id ${req.params.id}`); +}); + +Sentry.setupExpressErrorHandler(app); + +startExpressServerAndSendPortToRunner(app); diff --git a/dev-packages/node-integration-tests/suites/express-v5/handle-error-tracesSampleRate-0/test.ts b/dev-packages/node-integration-tests/suites/express-v5/handle-error-tracesSampleRate-0/test.ts new file mode 100644 index 000000000000..3ad6a3d2068f --- /dev/null +++ b/dev-packages/node-integration-tests/suites/express-v5/handle-error-tracesSampleRate-0/test.ts @@ -0,0 +1,38 @@ +import { cleanupChildProcesses, createRunner } from '../../../utils/runner'; + +afterAll(() => { + cleanupChildProcesses(); +}); + +test('should capture and send Express controller error with txn name if tracesSampleRate is 0', done => { + createRunner(__dirname, 'server.ts') + .ignore('transaction') + .expect({ + event: { + exception: { + values: [ + { + mechanism: { + type: 'middleware', + handled: false, + }, + type: 'Error', + value: 'test_error with id 123', + stacktrace: { + frames: expect.arrayContaining([ + expect.objectContaining({ + function: expect.any(String), + lineno: expect.any(Number), + colno: expect.any(Number), + }), + ]), + }, + }, + ], + }, + transaction: 'GET /test/express/:id', + }, + }) + .start(done) + .makeRequest('get', '/test/express/123', { expectError: true }); +}); diff --git a/dev-packages/node-integration-tests/suites/express-v5/handle-error-tracesSampleRate-unset/server.ts b/dev-packages/node-integration-tests/suites/express-v5/handle-error-tracesSampleRate-unset/server.ts new file mode 100644 index 000000000000..38833d7a9bc7 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/express-v5/handle-error-tracesSampleRate-unset/server.ts @@ -0,0 +1,21 @@ +import { loggingTransport } from '@sentry-internal/node-integration-tests'; +import * as Sentry from '@sentry/node'; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', + transport: loggingTransport, +}); + +import { startExpressServerAndSendPortToRunner } from '@sentry-internal/node-integration-tests'; +import express from 'express'; + +const app = express(); + +app.get('/test/express/:id', req => { + throw new Error(`test_error with id ${req.params.id}`); +}); + +Sentry.setupExpressErrorHandler(app); + +startExpressServerAndSendPortToRunner(app); diff --git a/dev-packages/node-integration-tests/suites/express-v5/handle-error-tracesSampleRate-unset/test.ts b/dev-packages/node-integration-tests/suites/express-v5/handle-error-tracesSampleRate-unset/test.ts new file mode 100644 index 000000000000..b02d74016ad4 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/express-v5/handle-error-tracesSampleRate-unset/test.ts @@ -0,0 +1,37 @@ +import { cleanupChildProcesses, createRunner } from '../../../utils/runner'; + +afterAll(() => { + cleanupChildProcesses(); +}); + +test('should capture and send Express controller error if tracesSampleRate is not set.', done => { + createRunner(__dirname, 'server.ts') + .ignore('transaction') + .expect({ + event: { + exception: { + values: [ + { + mechanism: { + type: 'middleware', + handled: false, + }, + type: 'Error', + value: 'test_error with id 123', + stacktrace: { + frames: expect.arrayContaining([ + expect.objectContaining({ + function: expect.any(String), + lineno: expect.any(Number), + colno: expect.any(Number), + }), + ]), + }, + }, + ], + }, + }, + }) + .start(done) + .makeRequest('get', '/test/express/123', { expectError: true }); +}); diff --git a/dev-packages/node-integration-tests/suites/express-v5/multiple-init/server.ts b/dev-packages/node-integration-tests/suites/express-v5/multiple-init/server.ts new file mode 100644 index 000000000000..39d56710f043 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/express-v5/multiple-init/server.ts @@ -0,0 +1,74 @@ +import { loggingTransport } from '@sentry-internal/node-integration-tests'; +import * as Sentry from '@sentry/node'; + +Sentry.init({ + // No dsn, means client is disabled + // dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', + transport: loggingTransport, +}); + +// We add http integration to ensure request isolation etc. works +const initialClient = Sentry.getClient(); +initialClient?.addIntegration(Sentry.httpIntegration()); + +// Store this so we can update the client later +const initialCurrentScope = Sentry.getCurrentScope(); + +import { startExpressServerAndSendPortToRunner } from '@sentry-internal/node-integration-tests'; +import express from 'express'; + +const app = express(); + +Sentry.setTag('global', 'tag'); + +app.get('/test/no-init', (_req, res) => { + Sentry.addBreadcrumb({ message: 'no init breadcrumb' }); + Sentry.setTag('no-init', 'tag'); + + res.send({}); +}); + +app.get('/test/init', (_req, res) => { + // Call init again, but with DSN + Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', + transport: loggingTransport, + }); + // Set this on initial scope, to ensure it can be inherited + initialCurrentScope.setClient(Sentry.getClient()!); + + Sentry.addBreadcrumb({ message: 'init breadcrumb' }); + Sentry.setTag('init', 'tag'); + + res.send({}); +}); + +app.get('/test/error/:id', (req, res) => { + const id = req.params.id; + Sentry.addBreadcrumb({ message: `error breadcrumb ${id}` }); + Sentry.setTag('error', id); + + Sentry.captureException(new Error(`This is an exception ${id}`)); + + setTimeout(() => { + // We flush to ensure we are sending exceptions in a certain order + Sentry.flush(1000).then( + () => { + // We send this so we can wait for this, to know the test is ended & server can be closed + if (id === '3') { + Sentry.captureException(new Error('Final exception was captured')); + } + res.send({}); + }, + () => { + res.send({}); + }, + ); + }, 1); +}); + +Sentry.setupExpressErrorHandler(app); + +startExpressServerAndSendPortToRunner(app); diff --git a/dev-packages/node-integration-tests/suites/express-v5/multiple-init/test.ts b/dev-packages/node-integration-tests/suites/express-v5/multiple-init/test.ts new file mode 100644 index 000000000000..b80669a7c432 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/express-v5/multiple-init/test.ts @@ -0,0 +1,70 @@ +import { cleanupChildProcesses, createRunner } from '../../../utils/runner'; + +afterAll(() => { + cleanupChildProcesses(); +}); + +test('allows to call init multiple times', done => { + const runner = createRunner(__dirname, 'server.ts') + .expect({ + event: { + exception: { + values: [ + { + value: 'This is an exception 2', + }, + ], + }, + breadcrumbs: [ + { + message: 'error breadcrumb 2', + timestamp: expect.any(Number), + }, + ], + tags: { + global: 'tag', + error: '2', + }, + }, + }) + .expect({ + event: { + exception: { + values: [ + { + value: 'This is an exception 3', + }, + ], + }, + breadcrumbs: [ + { + message: 'error breadcrumb 3', + timestamp: expect.any(Number), + }, + ], + tags: { + global: 'tag', + error: '3', + }, + }, + }) + .expect({ + event: { + exception: { + values: [ + { + value: 'Final exception was captured', + }, + ], + }, + }, + }) + .start(done); + + runner + .makeRequest('get', '/test/no-init') + .then(() => runner.makeRequest('get', '/test/error/1')) + .then(() => runner.makeRequest('get', '/test/init')) + .then(() => runner.makeRequest('get', '/test/error/2')) + .then(() => runner.makeRequest('get', '/test/error/3')); +}); diff --git a/dev-packages/node-integration-tests/suites/express-v5/multiple-routers/common-infix-parameterized/server.ts b/dev-packages/node-integration-tests/suites/express-v5/multiple-routers/common-infix-parameterized/server.ts new file mode 100644 index 000000000000..673c146e9d8c --- /dev/null +++ b/dev-packages/node-integration-tests/suites/express-v5/multiple-routers/common-infix-parameterized/server.ts @@ -0,0 +1,33 @@ +import { loggingTransport } from '@sentry-internal/node-integration-tests'; +import * as Sentry from '@sentry/node'; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', + tracesSampleRate: 1.0, + transport: loggingTransport, +}); + +import { startExpressServerAndSendPortToRunner } from '@sentry-internal/node-integration-tests'; +import cors from 'cors'; +import express from 'express'; + +const app = express(); + +app.use(cors()); + +const APIv1 = express.Router(); + +APIv1.get('/user/:userId', function (_req, res) { + Sentry.captureMessage('Custom Message'); + res.send('Success'); +}); + +const root = express.Router(); + +app.use('/api2/v1', root); +app.use('/api/v1', APIv1); + +Sentry.setupExpressErrorHandler(app); + +startExpressServerAndSendPortToRunner(app); diff --git a/dev-packages/node-integration-tests/suites/express-v5/multiple-routers/common-infix-parameterized/test.ts b/dev-packages/node-integration-tests/suites/express-v5/multiple-routers/common-infix-parameterized/test.ts new file mode 100644 index 000000000000..e7b9edabbfc8 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/express-v5/multiple-routers/common-infix-parameterized/test.ts @@ -0,0 +1,13 @@ +import { cleanupChildProcesses, createRunner } from '../../../../utils/runner'; + +afterAll(() => { + cleanupChildProcesses(); +}); + +test('should construct correct url with common infixes with multiple parameterized routers.', done => { + createRunner(__dirname, 'server.ts') + .ignore('transaction') + .expect({ event: { message: 'Custom Message', transaction: 'GET /api/v1/user/:userId' } }) + .start(done) + .makeRequest('get', '/api/v1/user/3212'); +}); diff --git a/dev-packages/node-integration-tests/suites/express-v5/multiple-routers/common-infix/server.ts b/dev-packages/node-integration-tests/suites/express-v5/multiple-routers/common-infix/server.ts new file mode 100644 index 000000000000..24073af67fa4 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/express-v5/multiple-routers/common-infix/server.ts @@ -0,0 +1,34 @@ +import { loggingTransport } from '@sentry-internal/node-integration-tests'; +import * as Sentry from '@sentry/node'; + +Sentry.init({ + debug: true, + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', + tracesSampleRate: 1.0, + transport: loggingTransport, +}); + +import { startExpressServerAndSendPortToRunner } from '@sentry-internal/node-integration-tests'; +import cors from 'cors'; +import express from 'express'; + +const app = express(); + +app.use(cors()); + +const APIv1 = express.Router(); + +APIv1.get('/test', function (_req, res) { + Sentry.captureMessage('Custom Message'); + res.send('Success'); +}); + +const root = express.Router(); + +app.use('/api/v1', root); +app.use('/api2/v1', APIv1); + +Sentry.setupExpressErrorHandler(app); + +startExpressServerAndSendPortToRunner(app); diff --git a/dev-packages/node-integration-tests/suites/express-v5/multiple-routers/common-infix/test.ts b/dev-packages/node-integration-tests/suites/express-v5/multiple-routers/common-infix/test.ts new file mode 100644 index 000000000000..52d6b631bea2 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/express-v5/multiple-routers/common-infix/test.ts @@ -0,0 +1,13 @@ +import { cleanupChildProcesses, createRunner } from '../../../../utils/runner'; + +afterAll(() => { + cleanupChildProcesses(); +}); + +test('should construct correct url with common infixes with multiple routers.', done => { + createRunner(__dirname, 'server.ts') + .ignore('transaction') + .expect({ event: { message: 'Custom Message', transaction: 'GET /api2/v1/test' } }) + .start(done) + .makeRequest('get', '/api2/v1/test'); +}); diff --git a/dev-packages/node-integration-tests/suites/express-v5/multiple-routers/common-prefix-parameterized-reverse/server.ts b/dev-packages/node-integration-tests/suites/express-v5/multiple-routers/common-prefix-parameterized-reverse/server.ts new file mode 100644 index 000000000000..755a32bf4389 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/express-v5/multiple-routers/common-prefix-parameterized-reverse/server.ts @@ -0,0 +1,33 @@ +import { loggingTransport } from '@sentry-internal/node-integration-tests'; +import * as Sentry from '@sentry/node'; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', + tracesSampleRate: 1.0, + transport: loggingTransport, +}); + +import { startExpressServerAndSendPortToRunner } from '@sentry-internal/node-integration-tests'; +import cors from 'cors'; +import express from 'express'; + +const app = express(); + +app.use(cors()); + +const APIv1 = express.Router(); + +APIv1.get('/user/:userId', function (_req, res) { + Sentry.captureMessage('Custom Message'); + res.send('Success'); +}); + +const root = express.Router(); + +app.use('/api/v1', APIv1); +app.use('/api', root); + +Sentry.setupExpressErrorHandler(app); + +startExpressServerAndSendPortToRunner(app); diff --git a/dev-packages/node-integration-tests/suites/express-v5/multiple-routers/common-prefix-parameterized-reverse/test.ts b/dev-packages/node-integration-tests/suites/express-v5/multiple-routers/common-prefix-parameterized-reverse/test.ts new file mode 100644 index 000000000000..5fabe5b92df6 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/express-v5/multiple-routers/common-prefix-parameterized-reverse/test.ts @@ -0,0 +1,13 @@ +import { cleanupChildProcesses, createRunner } from '../../../../utils/runner'; + +afterAll(() => { + cleanupChildProcesses(); +}); + +test('should construct correct urls with multiple parameterized routers (use order reversed).', done => { + createRunner(__dirname, 'server.ts') + .ignore('transaction') + .expect({ event: { message: 'Custom Message', transaction: 'GET /api/v1/user/:userId' } }) + .start(done) + .makeRequest('get', '/api/v1/user/1234/'); +}); diff --git a/dev-packages/node-integration-tests/suites/express-v5/multiple-routers/common-prefix-parameterized/server.ts b/dev-packages/node-integration-tests/suites/express-v5/multiple-routers/common-prefix-parameterized/server.ts new file mode 100644 index 000000000000..7db74e8e3dea --- /dev/null +++ b/dev-packages/node-integration-tests/suites/express-v5/multiple-routers/common-prefix-parameterized/server.ts @@ -0,0 +1,33 @@ +import { loggingTransport } from '@sentry-internal/node-integration-tests'; +import * as Sentry from '@sentry/node'; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', + tracesSampleRate: 1.0, + transport: loggingTransport, +}); + +import { startExpressServerAndSendPortToRunner } from '@sentry-internal/node-integration-tests'; +import cors from 'cors'; +import express from 'express'; + +const app = express(); + +app.use(cors()); + +const APIv1 = express.Router(); + +APIv1.get('/user/:userId', function (_req, res) { + Sentry.captureMessage('Custom Message'); + res.send('Success'); +}); + +const root = express.Router(); + +app.use('/api', root); +app.use('/api/v1', APIv1); + +Sentry.setupExpressErrorHandler(app); + +startExpressServerAndSendPortToRunner(app); diff --git a/dev-packages/node-integration-tests/suites/express-v5/multiple-routers/common-prefix-parameterized/test.ts b/dev-packages/node-integration-tests/suites/express-v5/multiple-routers/common-prefix-parameterized/test.ts new file mode 100644 index 000000000000..bab934f54522 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/express-v5/multiple-routers/common-prefix-parameterized/test.ts @@ -0,0 +1,13 @@ +import { cleanupChildProcesses, createRunner } from '../../../../utils/runner'; + +afterAll(() => { + cleanupChildProcesses(); +}); + +test('should construct correct urls with multiple parameterized routers.', done => { + createRunner(__dirname, 'server.ts') + .ignore('transaction') + .expect({ event: { message: 'Custom Message', transaction: 'GET /api/v1/user/:userId' } }) + .start(done) + .makeRequest('get', '/api/v1/user/1234/'); +}); diff --git a/dev-packages/node-integration-tests/suites/express-v5/multiple-routers/common-prefix-same-length-parameterized copy/server.ts b/dev-packages/node-integration-tests/suites/express-v5/multiple-routers/common-prefix-same-length-parameterized copy/server.ts new file mode 100644 index 000000000000..654afa3b8c8d --- /dev/null +++ b/dev-packages/node-integration-tests/suites/express-v5/multiple-routers/common-prefix-same-length-parameterized copy/server.ts @@ -0,0 +1,33 @@ +import { loggingTransport } from '@sentry-internal/node-integration-tests'; +import * as Sentry from '@sentry/node'; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', + tracesSampleRate: 1.0, + transport: loggingTransport, +}); + +import { startExpressServerAndSendPortToRunner } from '@sentry-internal/node-integration-tests'; +import cors from 'cors'; +import express from 'express'; + +const app = express(); + +app.use(cors()); + +const APIv1 = express.Router(); + +APIv1.get('/:userId', function (_req, res) { + Sentry.captureMessage('Custom Message'); + res.send('Success'); +}); + +const root = express.Router(); + +app.use('/api/v1', APIv1); +app.use('/api', root); + +Sentry.setupExpressErrorHandler(app); + +startExpressServerAndSendPortToRunner(app); diff --git a/dev-packages/node-integration-tests/suites/express-v5/multiple-routers/common-prefix-same-length-parameterized copy/test.ts b/dev-packages/node-integration-tests/suites/express-v5/multiple-routers/common-prefix-same-length-parameterized copy/test.ts new file mode 100644 index 000000000000..94d363f4faa4 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/express-v5/multiple-routers/common-prefix-same-length-parameterized copy/test.ts @@ -0,0 +1,13 @@ +import { cleanupChildProcesses, createRunner } from '../../../../utils/runner'; + +afterAll(() => { + cleanupChildProcesses(); +}); + +test('should construct correct url with multiple parameterized routers of the same length (use order reversed).', done => { + createRunner(__dirname, 'server.ts') + .ignore('transaction') + .expect({ event: { message: 'Custom Message', transaction: 'GET /api/v1/:userId' } }) + .start(done) + .makeRequest('get', '/api/v1/1234/'); +}); diff --git a/dev-packages/node-integration-tests/suites/express-v5/multiple-routers/common-prefix-same-length-parameterized/server.ts b/dev-packages/node-integration-tests/suites/express-v5/multiple-routers/common-prefix-same-length-parameterized/server.ts new file mode 100644 index 000000000000..017c810ed842 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/express-v5/multiple-routers/common-prefix-same-length-parameterized/server.ts @@ -0,0 +1,33 @@ +import { loggingTransport } from '@sentry-internal/node-integration-tests'; +import * as Sentry from '@sentry/node'; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', + tracesSampleRate: 1.0, + transport: loggingTransport, +}); + +import { startExpressServerAndSendPortToRunner } from '@sentry-internal/node-integration-tests'; +import cors from 'cors'; +import express from 'express'; + +const app = express(); + +app.use(cors()); + +const APIv1 = express.Router(); + +APIv1.get('/:userId', function (_req, res) { + Sentry.captureMessage('Custom Message'); + res.send('Success'); +}); + +const root = express.Router(); + +app.use('/api', root); +app.use('/api/v1', APIv1); + +Sentry.setupExpressErrorHandler(app); + +startExpressServerAndSendPortToRunner(app); diff --git a/dev-packages/node-integration-tests/suites/express-v5/multiple-routers/common-prefix-same-length-parameterized/test.ts b/dev-packages/node-integration-tests/suites/express-v5/multiple-routers/common-prefix-same-length-parameterized/test.ts new file mode 100644 index 000000000000..373b2c102c4c --- /dev/null +++ b/dev-packages/node-integration-tests/suites/express-v5/multiple-routers/common-prefix-same-length-parameterized/test.ts @@ -0,0 +1,13 @@ +import { cleanupChildProcesses, createRunner } from '../../../../utils/runner'; + +afterAll(() => { + cleanupChildProcesses(); +}); + +test('should construct correct url with multiple parameterized routers of the same length.', done => { + createRunner(__dirname, 'server.ts') + .ignore('transaction') + .expect({ event: { message: 'Custom Message', transaction: 'GET /api/v1/:userId' } }) + .start(done) + .makeRequest('get', '/api/v1/1234/'); +}); diff --git a/dev-packages/node-integration-tests/suites/express-v5/multiple-routers/common-prefix/server.ts b/dev-packages/node-integration-tests/suites/express-v5/multiple-routers/common-prefix/server.ts new file mode 100644 index 000000000000..497cbf2efffb --- /dev/null +++ b/dev-packages/node-integration-tests/suites/express-v5/multiple-routers/common-prefix/server.ts @@ -0,0 +1,33 @@ +import { loggingTransport } from '@sentry-internal/node-integration-tests'; +import * as Sentry from '@sentry/node'; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', + tracesSampleRate: 1.0, + transport: loggingTransport, +}); + +import { startExpressServerAndSendPortToRunner } from '@sentry-internal/node-integration-tests'; +import cors from 'cors'; +import express from 'express'; + +const app = express(); + +app.use(cors()); + +const APIv1 = express.Router(); + +APIv1.get('/test', function (_req, res) { + Sentry.captureMessage('Custom Message'); + res.send('Success'); +}); + +const root = express.Router(); + +app.use('/api', root); +app.use('/api/v1', APIv1); + +Sentry.setupExpressErrorHandler(app); + +startExpressServerAndSendPortToRunner(app); diff --git a/dev-packages/node-integration-tests/suites/express-v5/multiple-routers/common-prefix/test.ts b/dev-packages/node-integration-tests/suites/express-v5/multiple-routers/common-prefix/test.ts new file mode 100644 index 000000000000..ea217bf6bc05 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/express-v5/multiple-routers/common-prefix/test.ts @@ -0,0 +1,13 @@ +import { cleanupChildProcesses, createRunner } from '../../../../utils/runner'; + +afterAll(() => { + cleanupChildProcesses(); +}); + +test('should construct correct urls with multiple routers.', done => { + createRunner(__dirname, 'server.ts') + .ignore('transaction') + .expect({ event: { message: 'Custom Message', transaction: 'GET /api/v1/test' } }) + .start(done) + .makeRequest('get', '/api/v1/test'); +}); diff --git a/dev-packages/node-integration-tests/suites/express-v5/multiple-routers/complex-router/server.ts b/dev-packages/node-integration-tests/suites/express-v5/multiple-routers/complex-router/server.ts new file mode 100644 index 000000000000..b7ffeeba937a --- /dev/null +++ b/dev-packages/node-integration-tests/suites/express-v5/multiple-routers/complex-router/server.ts @@ -0,0 +1,33 @@ +import { loggingTransport } from '@sentry-internal/node-integration-tests'; +import * as Sentry from '@sentry/node'; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', + tracesSampleRate: 1.0, + transport: loggingTransport, +}); + +import { startExpressServerAndSendPortToRunner } from '@sentry-internal/node-integration-tests'; +import express from 'express'; + +const app = express(); + +const APIv1 = express.Router(); + +APIv1.use( + '/users/:userId', + APIv1.get('/posts/:postId', (_req, res) => { + Sentry.captureMessage('Custom Message'); + return res.send('Success'); + }), +); + +const router = express.Router(); + +app.use('/api', router); +app.use('/api/api/v1', APIv1.use('/sub-router', APIv1)); + +Sentry.setupExpressErrorHandler(app); + +startExpressServerAndSendPortToRunner(app); diff --git a/dev-packages/node-integration-tests/suites/express-v5/multiple-routers/complex-router/test.ts b/dev-packages/node-integration-tests/suites/express-v5/multiple-routers/complex-router/test.ts new file mode 100644 index 000000000000..fe065d0dc550 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/express-v5/multiple-routers/complex-router/test.ts @@ -0,0 +1,52 @@ +import { cleanupChildProcesses, createRunner } from '../../../../utils/runner'; + +afterAll(() => { + cleanupChildProcesses(); +}); + +describe('complex-router', () => { + test('should construct correct url with multiple parameterized routers, when param is also contain in middle layer route and express used multiple middlewares with route', done => { + const EXPECTED_TRANSACTION = { + transaction: 'GET /api/api/v1/sub-router/users/:userId/posts/:postId', + transaction_info: { + source: 'route', + }, + }; + + createRunner(__dirname, 'server.ts') + .ignore('event') + .expect({ transaction: EXPECTED_TRANSACTION as any }) + .start(done) + .makeRequest('get', '/api/api/v1/sub-router/users/123/posts/456'); + }); + + test('should construct correct url with multiple parameterized routers, when param is also contain in middle layer route and express used multiple middlewares with route and original url has query params', done => { + const EXPECTED_TRANSACTION = { + transaction: 'GET /api/api/v1/sub-router/users/:userId/posts/:postId', + transaction_info: { + source: 'route', + }, + }; + + createRunner(__dirname, 'server.ts') + .ignore('event') + .expect({ transaction: EXPECTED_TRANSACTION as any }) + .start(done) + .makeRequest('get', '/api/api/v1/sub-router/users/123/posts/456?param=1'); + }); + + test('should construct correct url with multiple parameterized routers, when param is also contain in middle layer route and express used multiple middlewares with route and original url ends with trailing slash and has query params', done => { + const EXPECTED_TRANSACTION = { + transaction: 'GET /api/api/v1/sub-router/users/:userId/posts/:postId', + transaction_info: { + source: 'route', + }, + }; + + createRunner(__dirname, 'server.ts') + .ignore('event') + .expect({ transaction: EXPECTED_TRANSACTION as any }) + .start(done) + .makeRequest('get', '/api/api/v1/sub-router/users/123/posts/456/?param=1'); + }); +}); diff --git a/dev-packages/node-integration-tests/suites/express-v5/multiple-routers/middle-layer-parameterized/server.ts b/dev-packages/node-integration-tests/suites/express-v5/multiple-routers/middle-layer-parameterized/server.ts new file mode 100644 index 000000000000..12a00ce4e1db --- /dev/null +++ b/dev-packages/node-integration-tests/suites/express-v5/multiple-routers/middle-layer-parameterized/server.ts @@ -0,0 +1,33 @@ +import { loggingTransport } from '@sentry-internal/node-integration-tests'; +import * as Sentry from '@sentry/node'; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', + tracesSampleRate: 1.0, + transport: loggingTransport, +}); + +import { startExpressServerAndSendPortToRunner } from '@sentry-internal/node-integration-tests'; +import express from 'express'; + +const app = express(); + +const APIv1 = express.Router(); + +APIv1.use( + '/users/:userId', + APIv1.get('/posts/:postId', (_req, res) => { + Sentry.captureMessage('Custom Message'); + return res.send('Success'); + }), +); + +const root = express.Router(); + +app.use('/api/v1', APIv1); +app.use('/api', root); + +Sentry.setupExpressErrorHandler(app); + +startExpressServerAndSendPortToRunner(app); diff --git a/dev-packages/node-integration-tests/suites/express-v5/multiple-routers/middle-layer-parameterized/test.ts b/dev-packages/node-integration-tests/suites/express-v5/multiple-routers/middle-layer-parameterized/test.ts new file mode 100644 index 000000000000..52a6ce154684 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/express-v5/multiple-routers/middle-layer-parameterized/test.ts @@ -0,0 +1,23 @@ +import { cleanupChildProcesses, createRunner } from '../../../../utils/runner'; + +afterAll(() => { + cleanupChildProcesses(); +}); + +// Before Node 16, parametrization is not working properly here +describe('middle-layer-parameterized', () => { + test('should construct correct url with multiple parameterized routers, when param is also contain in middle layer route', done => { + const EXPECTED_TRANSACTION = { + transaction: 'GET /api/v1/users/:userId/posts/:postId', + transaction_info: { + source: 'route', + }, + }; + + createRunner(__dirname, 'server.ts') + .ignore('event') + .expect({ transaction: EXPECTED_TRANSACTION as any }) + .start(done) + .makeRequest('get', '/api/v1/users/123/posts/456'); + }); +}); diff --git a/dev-packages/node-integration-tests/suites/express-v5/package.json b/dev-packages/node-integration-tests/suites/express-v5/package.json new file mode 100644 index 000000000000..b3855635c556 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/express-v5/package.json @@ -0,0 +1,6 @@ +{ + "name": "express-v5", + "dependencies": { + "express": "^5.0.0" + } +} diff --git a/dev-packages/node-integration-tests/suites/express-v5/requestUser/server.js b/dev-packages/node-integration-tests/suites/express-v5/requestUser/server.js new file mode 100644 index 000000000000..d93d22905506 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/express-v5/requestUser/server.js @@ -0,0 +1,49 @@ +const { loggingTransport } = require('@sentry-internal/node-integration-tests'); +const Sentry = require('@sentry/node'); + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', + transport: loggingTransport, + debug: true, +}); + +// express must be required after Sentry is initialized +const express = require('express'); +const cors = require('cors'); +const { startExpressServerAndSendPortToRunner } = require('@sentry-internal/node-integration-tests'); + +const app = express(); + +app.use(cors()); + +app.use((req, _res, next) => { + // We simulate this, which would in other cases be done by some middleware + req.user = { + id: '1', + email: 'test@sentry.io', + }; + + next(); +}); + +app.get('/test1', (_req, _res) => { + throw new Error('error_1'); +}); + +app.use((_req, _res, next) => { + Sentry.setUser({ + id: '2', + email: 'test2@sentry.io', + }); + + next(); +}); + +app.get('/test2', (_req, _res) => { + throw new Error('error_2'); +}); + +Sentry.setupExpressErrorHandler(app); + +startExpressServerAndSendPortToRunner(app); diff --git a/dev-packages/node-integration-tests/suites/express-v5/requestUser/test.ts b/dev-packages/node-integration-tests/suites/express-v5/requestUser/test.ts new file mode 100644 index 000000000000..2a9fc58a7c18 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/express-v5/requestUser/test.ts @@ -0,0 +1,42 @@ +import { cleanupChildProcesses, createRunner } from '../../../utils/runner'; + +describe('express user handling', () => { + afterAll(() => { + cleanupChildProcesses(); + }); + + test('ignores user from request', done => { + expect.assertions(2); + + createRunner(__dirname, 'server.js') + .expect({ + event: event => { + expect(event.user).toBeUndefined(); + expect(event.exception?.values?.[0]?.value).toBe('error_1'); + }, + }) + .start(done) + .makeRequest('get', '/test1', { expectError: true }); + }); + + test('using setUser in middleware works', done => { + createRunner(__dirname, 'server.js') + .expect({ + event: { + user: { + id: '2', + email: 'test2@sentry.io', + }, + exception: { + values: [ + { + value: 'error_2', + }, + ], + }, + }, + }) + .start(done) + .makeRequest('get', '/test2', { expectError: true }); + }); +}); diff --git a/dev-packages/node-integration-tests/suites/express-v5/sentry-trace/baggage-header-assign/test.ts b/dev-packages/node-integration-tests/suites/express-v5/sentry-trace/baggage-header-assign/test.ts new file mode 100644 index 000000000000..513cf6146d0f --- /dev/null +++ b/dev-packages/node-integration-tests/suites/express-v5/sentry-trace/baggage-header-assign/test.ts @@ -0,0 +1,147 @@ +import { parseBaggageHeader } from '@sentry/core'; +import { cleanupChildProcesses, createRunner } from '../../../../utils/runner'; +import type { TestAPIResponse } from '../server'; + +afterAll(() => { + cleanupChildProcesses(); +}); + +test('Should overwrite baggage if the incoming request already has Sentry baggage data but no sentry-trace', async () => { + const runner = createRunner(__dirname, '..', 'server.ts').start(); + + const response = await runner.makeRequest('get', '/test/express', { + headers: { + baggage: 'sentry-release=2.0.0,sentry-environment=myEnv', + }, + }); + + expect(response).toBeDefined(); + expect(response).not.toMatchObject({ + test_data: { + host: 'somewhere.not.sentry', + baggage: 'sentry-release=2.0.0,sentry-environment=myEnv', + }, + }); +}); + +test('Should propagate sentry trace baggage data from an incoming to an outgoing request.', async () => { + const runner = createRunner(__dirname, '..', 'server.ts').start(); + + const response = await runner.makeRequest('get', '/test/express', { + headers: { + 'sentry-trace': '12312012123120121231201212312012-1121201211212012-1', + baggage: 'sentry-release=2.0.0,sentry-environment=myEnv,dogs=great,sentry-sample_rand=0.42', + }, + }); + + expect(response).toBeDefined(); + expect(response).toMatchObject({ + test_data: { + host: 'somewhere.not.sentry', + baggage: 'sentry-release=2.0.0,sentry-environment=myEnv,sentry-sample_rand=0.42', + }, + }); +}); + +test('Should not propagate baggage data from an incoming to an outgoing request if sentry-trace is faulty.', async () => { + const runner = createRunner(__dirname, '..', 'server.ts').start(); + + const response = await runner.makeRequest('get', '/test/express', { + headers: { + 'sentry-trace': '', + baggage: 'sentry-release=2.0.0,sentry-environment=myEnv,dogs=great', + }, + }); + + expect(response).toBeDefined(); + expect(response).not.toMatchObject({ + test_data: { + host: 'somewhere.not.sentry', + baggage: 'sentry-release=2.0.0,sentry-environment=myEnv', + }, + }); +}); + +test('Should not propagate baggage if sentry-trace header is present in incoming request but no baggage header', async () => { + const runner = createRunner(__dirname, '..', 'server.ts').start(); + + const response = await runner.makeRequest('get', '/test/express', { + headers: { + 'sentry-trace': '12312012123120121231201212312012-1121201211212012-1', + }, + }); + + expect(response).toBeDefined(); + expect(response).toMatchObject({ + test_data: { + host: 'somewhere.not.sentry', + }, + }); +}); + +test('Should not propagate baggage and ignore original 3rd party baggage entries if sentry-trace header is present', async () => { + const runner = createRunner(__dirname, '..', 'server.ts').start(); + + const response = await runner.makeRequest('get', '/test/express', { + headers: { + 'sentry-trace': '12312012123120121231201212312012-1121201211212012-1', + baggage: 'foo=bar', + }, + }); + + expect(response).toBeDefined(); + expect(response).toMatchObject({ + test_data: { + host: 'somewhere.not.sentry', + }, + }); +}); + +test('Should populate and propagate sentry baggage if sentry-trace header does not exist', async () => { + const runner = createRunner(__dirname, '..', 'server.ts').start(); + + const response = await runner.makeRequest('get', '/test/express'); + + expect(response).toBeDefined(); + + const parsedBaggage = parseBaggageHeader(response?.test_data.baggage); + + expect(response?.test_data.host).toBe('somewhere.not.sentry'); + expect(parsedBaggage).toStrictEqual({ + 'sentry-environment': 'prod', + 'sentry-release': '1.0', + 'sentry-public_key': 'public', + // TraceId changes, hence we only expect that the string contains the traceid key + 'sentry-trace_id': expect.stringMatching(/[\S]*/), + 'sentry-sample_rand': expect.stringMatching(/[\S]*/), + 'sentry-sample_rate': '1', + 'sentry-sampled': 'true', + 'sentry-transaction': 'GET /test/express', + }); +}); + +test('Should populate Sentry and ignore 3rd party content if sentry-trace header does not exist', async () => { + const runner = createRunner(__dirname, '..', 'server.ts').start(); + + const response = await runner.makeRequest('get', '/test/express', { + headers: { + baggage: 'foo=bar,bar=baz', + }, + }); + + expect(response).toBeDefined(); + expect(response?.test_data.host).toBe('somewhere.not.sentry'); + + const parsedBaggage = parseBaggageHeader(response?.test_data.baggage); + expect(parsedBaggage).toStrictEqual({ + 'sentry-environment': 'prod', + 'sentry-release': '1.0', + 'sentry-public_key': 'public', + // TraceId changes, hence we only expect that the string contains the traceid key + 'sentry-trace_id': expect.stringMatching(/[\S]*/), + 'sentry-sample_rand': expect.stringMatching(/[\S]*/), + 'sentry-sample_rate': '1', + 'sentry-sampled': 'true', + 'sentry-transaction': 'GET /test/express', + }); +}); diff --git a/dev-packages/node-integration-tests/suites/express-v5/sentry-trace/baggage-header-out/server.ts b/dev-packages/node-integration-tests/suites/express-v5/sentry-trace/baggage-header-out/server.ts new file mode 100644 index 000000000000..07c21c8d21ea --- /dev/null +++ b/dev-packages/node-integration-tests/suites/express-v5/sentry-trace/baggage-header-out/server.ts @@ -0,0 +1,38 @@ +import { loggingTransport, startExpressServerAndSendPortToRunner } from '@sentry-internal/node-integration-tests'; +import * as Sentry from '@sentry/node'; + +export type TestAPIResponse = { test_data: { host: string; 'sentry-trace': string; baggage: string } }; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', + environment: 'prod', + tracePropagationTargets: [/^(?!.*express).*$/], + tracesSampleRate: 1.0, + transport: loggingTransport, +}); + +import http from 'http'; +import cors from 'cors'; +import express from 'express'; + +const app = express(); + +Sentry.setUser({ id: 'user123' }); + +app.use(cors()); + +app.get('/test/express', (_req, res) => { + const span = Sentry.getActiveSpan(); + const traceId = span?.spanContext().traceId; + const headers = http.get('http://somewhere.not.sentry/').getHeaders(); + if (traceId) { + headers['baggage'] = (headers['baggage'] as string).replace(traceId, '__SENTRY_TRACE_ID__'); + } + // Responding with the headers outgoing request headers back to the assertions. + res.send({ test_data: headers }); +}); + +Sentry.setupExpressErrorHandler(app); + +startExpressServerAndSendPortToRunner(app); diff --git a/dev-packages/node-integration-tests/suites/express-v5/sentry-trace/baggage-header-out/test.ts b/dev-packages/node-integration-tests/suites/express-v5/sentry-trace/baggage-header-out/test.ts new file mode 100644 index 000000000000..72b6a7139f35 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/express-v5/sentry-trace/baggage-header-out/test.ts @@ -0,0 +1,35 @@ +import { cleanupChildProcesses, createRunner } from '../../../../utils/runner'; +import type { TestAPIResponse } from './server'; + +afterAll(() => { + cleanupChildProcesses(); +}); + +test('should attach a baggage header to an outgoing request.', 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?.split(','); + + [ + 'sentry-environment=prod', + 'sentry-public_key=public', + 'sentry-release=1.0', + 'sentry-sample_rate=1', + 'sentry-sampled=true', + 'sentry-trace_id=__SENTRY_TRACE_ID__', + 'sentry-transaction=GET%20%2Ftest%2Fexpress', + expect.stringMatching(/sentry-sample_rand=0\.[0-9]+/), + ].forEach(item => { + expect(baggage).toContainEqual(item); + }); + + expect(response).toMatchObject({ + test_data: { + host: 'somewhere.not.sentry', + }, + }); +}); diff --git a/dev-packages/node-integration-tests/suites/express-v5/sentry-trace/baggage-other-vendors-with-sentry-entries/server.ts b/dev-packages/node-integration-tests/suites/express-v5/sentry-trace/baggage-other-vendors-with-sentry-entries/server.ts new file mode 100644 index 000000000000..260fb34af5c2 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/express-v5/sentry-trace/baggage-other-vendors-with-sentry-entries/server.ts @@ -0,0 +1,43 @@ +import { loggingTransport } from '@sentry-internal/node-integration-tests'; +import * as Sentry from '@sentry/node'; + +export type TestAPIResponse = { test_data: { host: string; 'sentry-trace': string; baggage: string } }; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', + environment: 'prod', + // disable requests to /express + tracePropagationTargets: [/^(?!.*express).*$/], + tracesSampleRate: 1.0, + transport: loggingTransport, +}); + +import * as http from 'http'; +import { startExpressServerAndSendPortToRunner } from '@sentry-internal/node-integration-tests'; +import cors from 'cors'; +import express from 'express'; + +const app = express(); + +app.use(cors()); + +app.get('/test/express', (_req, res) => { + // simulate setting a "third party" baggage header which the Sentry SDK should merge with Sentry DSC entries + const headers = http + .get({ + hostname: 'somewhere.not.sentry', + headers: { + baggage: + 'other=vendor,foo=bar,third=party,sentry-release=9.9.9,sentry-environment=staging,sentry-sample_rate=0.54,last=item', + }, + }) + .getHeaders(); + + // Responding with the headers outgoing request headers back to the assertions. + res.send({ test_data: headers }); +}); + +Sentry.setupExpressErrorHandler(app); + +startExpressServerAndSendPortToRunner(app); diff --git a/dev-packages/node-integration-tests/suites/express-v5/sentry-trace/baggage-other-vendors-with-sentry-entries/test.ts b/dev-packages/node-integration-tests/suites/express-v5/sentry-trace/baggage-other-vendors-with-sentry-entries/test.ts new file mode 100644 index 000000000000..ebf2a15bedf4 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/express-v5/sentry-trace/baggage-other-vendors-with-sentry-entries/test.ts @@ -0,0 +1,69 @@ +import { cleanupChildProcesses, createRunner } from '../../../../utils/runner'; +import type { TestAPIResponse } from '../server'; + +afterAll(() => { + cleanupChildProcesses(); +}); + +test('should ignore sentry-values in `baggage` header of a third party vendor and overwrite them with incoming DSC', async () => { + const runner = createRunner(__dirname, 'server.ts').start(); + + const response = await runner.makeRequest('get', '/test/express', { + headers: { + 'sentry-trace': '12312012123120121231201212312012-1121201211212012-1', + baggage: 'sentry-release=2.1.0,sentry-environment=myEnv', + }, + }); + + expect(response).toBeDefined(); + + const baggage = response?.test_data.baggage?.split(',').sort(); + + expect(response).toMatchObject({ + test_data: { + host: 'somewhere.not.sentry', + }, + }); + + expect(baggage).toEqual([ + 'foo=bar', + 'last=item', + 'other=vendor', + 'sentry-environment=myEnv', + 'sentry-release=2.1.0', + expect.stringMatching(/sentry-sample_rand=[0-9]+/), + 'sentry-sample_rate=0.54', + 'third=party', + ]); +}); + +test('should ignore sentry-values in `baggage` header of a third party vendor and overwrite them with new DSC', 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?.split(',').sort(); + + expect(response).toMatchObject({ + test_data: { + host: 'somewhere.not.sentry', + }, + }); + + expect(baggage).toEqual([ + 'foo=bar', + 'last=item', + 'other=vendor', + 'sentry-environment=prod', + 'sentry-public_key=public', + 'sentry-release=1.0', + expect.stringMatching(/sentry-sample_rand=[0-9]+/), + 'sentry-sample_rate=1', + 'sentry-sampled=true', + expect.stringMatching(/sentry-trace_id=[0-9a-f]{32}/), + 'sentry-transaction=GET%20%2Ftest%2Fexpress', + 'third=party', + ]); +}); diff --git a/dev-packages/node-integration-tests/suites/express-v5/sentry-trace/baggage-other-vendors/server.ts b/dev-packages/node-integration-tests/suites/express-v5/sentry-trace/baggage-other-vendors/server.ts new file mode 100644 index 000000000000..1c00fbd72bde --- /dev/null +++ b/dev-packages/node-integration-tests/suites/express-v5/sentry-trace/baggage-other-vendors/server.ts @@ -0,0 +1,37 @@ +import { loggingTransport } from '@sentry-internal/node-integration-tests'; +import * as Sentry from '@sentry/node'; + +export type TestAPIResponse = { test_data: { host: string; 'sentry-trace': string; baggage: string } }; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', + environment: 'prod', + // disable requests to /express + tracePropagationTargets: [/^(?!.*express).*$/], + tracesSampleRate: 1.0, + transport: loggingTransport, +}); + +import http from 'http'; +import { startExpressServerAndSendPortToRunner } from '@sentry-internal/node-integration-tests'; +import cors from 'cors'; +import express from 'express'; + +const app = express(); + +app.use(cors()); + +app.get('/test/express', (_req, res) => { + // simulate setting a "third party" baggage header which the Sentry SDK should merge with Sentry DSC entries + const headers = http + .get({ hostname: 'somewhere.not.sentry', headers: { baggage: 'other=vendor,foo=bar,third=party' } }) + .getHeaders(); + + // Responding with the headers outgoing request headers back to the assertions. + res.send({ test_data: headers }); +}); + +Sentry.setupExpressErrorHandler(app); + +startExpressServerAndSendPortToRunner(app); diff --git a/dev-packages/node-integration-tests/suites/express-v5/sentry-trace/baggage-other-vendors/test.ts b/dev-packages/node-integration-tests/suites/express-v5/sentry-trace/baggage-other-vendors/test.ts new file mode 100644 index 000000000000..0beecb54a905 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/express-v5/sentry-trace/baggage-other-vendors/test.ts @@ -0,0 +1,25 @@ +import { cleanupChildProcesses, createRunner } from '../../../../utils/runner'; +import type { TestAPIResponse } from './server'; + +afterAll(() => { + cleanupChildProcesses(); +}); + +test('should merge `baggage` header of a third party vendor with the Sentry DSC baggage items', async () => { + const runner = createRunner(__dirname, 'server.ts').start(); + + const response = await runner.makeRequest('get', '/test/express', { + headers: { + 'sentry-trace': '12312012123120121231201212312012-1121201211212012-1', + baggage: 'sentry-release=2.0.0,sentry-environment=myEnv,sentry-sample_rand=0.42', + }, + }); + + expect(response).toBeDefined(); + expect(response).toMatchObject({ + test_data: { + host: 'somewhere.not.sentry', + baggage: 'other=vendor,foo=bar,third=party,sentry-release=2.0.0,sentry-environment=myEnv,sentry-sample_rand=0.42', + }, + }); +}); diff --git a/dev-packages/node-integration-tests/suites/express-v5/sentry-trace/baggage-transaction-name/server.ts b/dev-packages/node-integration-tests/suites/express-v5/sentry-trace/baggage-transaction-name/server.ts new file mode 100644 index 000000000000..80bb7b38a39a --- /dev/null +++ b/dev-packages/node-integration-tests/suites/express-v5/sentry-trace/baggage-transaction-name/server.ts @@ -0,0 +1,37 @@ +import { loggingTransport } from '@sentry-internal/node-integration-tests'; +import * as Sentry from '@sentry/node'; + +export type TestAPIResponse = { test_data: { host: string; 'sentry-trace': string; baggage: string } }; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', + environment: 'prod', + // disable requests to /express + tracePropagationTargets: [/^(?!.*express).*$/], + tracesSampleRate: 1.0, + // TODO: We're rethinking the mechanism for including Pii data in DSC, hence commenting out sendDefaultPii for now + // sendDefaultPii: true, + transport: loggingTransport, +}); + +import http from 'http'; +import { startExpressServerAndSendPortToRunner } from '@sentry-internal/node-integration-tests'; +import cors from 'cors'; +import express from 'express'; + +const app = express(); + +Sentry.setUser({ id: 'user123' }); + +app.use(cors()); + +app.get('/test/express', (_req, res) => { + const headers = http.get('http://somewhere.not.sentry/').getHeaders(); + // Responding with the headers outgoing request headers back to the assertions. + res.send({ test_data: headers }); +}); + +Sentry.setupExpressErrorHandler(app); + +startExpressServerAndSendPortToRunner(app); diff --git a/dev-packages/node-integration-tests/suites/express-v5/sentry-trace/baggage-transaction-name/test.ts b/dev-packages/node-integration-tests/suites/express-v5/sentry-trace/baggage-transaction-name/test.ts new file mode 100644 index 000000000000..1001d0839aea --- /dev/null +++ b/dev-packages/node-integration-tests/suites/express-v5/sentry-trace/baggage-transaction-name/test.ts @@ -0,0 +1,20 @@ +import { cleanupChildProcesses, createRunner } from '../../../../utils/runner'; +import type { TestAPIResponse } from '../server'; + +afterAll(() => { + cleanupChildProcesses(); +}); + +test('Includes transaction in baggage if the transaction name is parameterized', async () => { + const runner = createRunner(__dirname, 'server.ts').start(); + + const response = await runner.makeRequest('get', '/test/express'); + + expect(response).toBeDefined(); + expect(response).toMatchObject({ + test_data: { + host: 'somewhere.not.sentry', + baggage: expect.stringContaining('sentry-transaction=GET%20%2Ftest%2Fexpress'), + }, + }); +}); diff --git a/dev-packages/node-integration-tests/suites/express-v5/sentry-trace/server.ts b/dev-packages/node-integration-tests/suites/express-v5/sentry-trace/server.ts new file mode 100644 index 000000000000..6ebc2d4cac95 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/express-v5/sentry-trace/server.ts @@ -0,0 +1,33 @@ +import { loggingTransport } from '@sentry-internal/node-integration-tests'; +import * as Sentry from '@sentry/node'; + +export type TestAPIResponse = { test_data: { host: string; 'sentry-trace': string; baggage: string } }; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', + environment: 'prod', + tracePropagationTargets: [/^(?!.*express).*$/], + tracesSampleRate: 1.0, + transport: loggingTransport, +}); + +import http from 'http'; +import { startExpressServerAndSendPortToRunner } from '@sentry-internal/node-integration-tests'; +import cors from 'cors'; +import express from 'express'; + +const app = express(); + +app.use(cors()); + +app.get('/test/express', (_req, res) => { + const headers = http.get('http://somewhere.not.sentry/').getHeaders(); + + // Responding with the headers outgoing request headers back to the assertions. + res.send({ test_data: headers }); +}); + +Sentry.setupExpressErrorHandler(app); + +startExpressServerAndSendPortToRunner(app); diff --git a/dev-packages/node-integration-tests/suites/express-v5/sentry-trace/trace-header-assign/server.ts b/dev-packages/node-integration-tests/suites/express-v5/sentry-trace/trace-header-assign/server.ts new file mode 100644 index 000000000000..1cc4a0dcc639 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/express-v5/sentry-trace/trace-header-assign/server.ts @@ -0,0 +1,32 @@ +import { loggingTransport } from '@sentry-internal/node-integration-tests'; +import * as Sentry from '@sentry/node'; + +export type TestAPIResponse = { test_data: { host: string; 'sentry-trace': string; baggage: string } }; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', + environment: 'prod', + tracesSampleRate: 1.0, + transport: loggingTransport, +}); + +import http from 'http'; +import { startExpressServerAndSendPortToRunner } from '@sentry-internal/node-integration-tests'; +import cors from 'cors'; +import express from 'express'; + +const app = express(); + +app.use(cors()); + +app.get('/test/express', (_req, res) => { + const headers = http.get('http://somewhere.not.sentry/').getHeaders(); + + // Responding with the headers outgoing request headers back to the assertions. + res.send({ test_data: headers }); +}); + +Sentry.setupExpressErrorHandler(app); + +startExpressServerAndSendPortToRunner(app); diff --git a/dev-packages/node-integration-tests/suites/express-v5/sentry-trace/trace-header-assign/test.ts b/dev-packages/node-integration-tests/suites/express-v5/sentry-trace/trace-header-assign/test.ts new file mode 100644 index 000000000000..40bbb03f8d50 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/express-v5/sentry-trace/trace-header-assign/test.ts @@ -0,0 +1,27 @@ +import { TRACEPARENT_REGEXP } from '@sentry/core'; +import { cleanupChildProcesses, createRunner } from '../../../../utils/runner'; +import type { TestAPIResponse } from '../server'; + +afterAll(() => { + cleanupChildProcesses(); +}); + +test('Should assign `sentry-trace` header which sets parent trace id of an outgoing request.', async () => { + const runner = createRunner(__dirname, 'server.ts').start(); + + const response = await runner.makeRequest('get', '/test/express', { + headers: { + 'sentry-trace': '12312012123120121231201212312012-1121201211212012-0', + }, + }); + + expect(response).toBeDefined(); + expect(response).toMatchObject({ + test_data: { + host: 'somewhere.not.sentry', + 'sentry-trace': expect.stringContaining('12312012123120121231201212312012-'), + }, + }); + + expect(TRACEPARENT_REGEXP.test(response?.test_data['sentry-trace'] || '')).toBe(true); +}); diff --git a/dev-packages/node-integration-tests/suites/express-v5/sentry-trace/trace-header-out/test.ts b/dev-packages/node-integration-tests/suites/express-v5/sentry-trace/trace-header-out/test.ts new file mode 100644 index 000000000000..db46bb491904 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/express-v5/sentry-trace/trace-header-out/test.ts @@ -0,0 +1,23 @@ +import { TRACEPARENT_REGEXP } from '@sentry/core'; +import { cleanupChildProcesses, createRunner } from '../../../../utils/runner'; +import type { TestAPIResponse } from '../server'; + +afterAll(() => { + cleanupChildProcesses(); +}); + +test('should attach a `sentry-trace` header to an outgoing request.', async () => { + const runner = createRunner(__dirname, '..', 'server.ts').start(); + + const response = await runner.makeRequest('get', '/test/express'); + + expect(response).toBeDefined(); + expect(response).toMatchObject({ + test_data: { + host: 'somewhere.not.sentry', + 'sentry-trace': expect.any(String), + }, + }); + + expect(TRACEPARENT_REGEXP.test(response?.test_data['sentry-trace'] || '')).toBe(true); +}); diff --git a/dev-packages/node-integration-tests/suites/express-v5/setupExpressErrorHandler/server.js b/dev-packages/node-integration-tests/suites/express-v5/setupExpressErrorHandler/server.js new file mode 100644 index 000000000000..0e73923cf88a --- /dev/null +++ b/dev-packages/node-integration-tests/suites/express-v5/setupExpressErrorHandler/server.js @@ -0,0 +1,33 @@ +const { loggingTransport } = require('@sentry-internal/node-integration-tests'); +const Sentry = require('@sentry/node'); + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', + transport: loggingTransport, +}); + +// express must be required after Sentry is initialized +const express = require('express'); +const cors = require('cors'); +const { startExpressServerAndSendPortToRunner } = require('@sentry-internal/node-integration-tests'); + +const app = express(); + +app.use(cors()); + +app.get('/test1', (_req, _res) => { + throw new Error('error_1'); +}); + +app.get('/test2', (_req, _res) => { + throw new Error('error_2'); +}); + +Sentry.setupExpressErrorHandler(app, { + shouldHandleError: error => { + return error.message === 'error_2'; + }, +}); + +startExpressServerAndSendPortToRunner(app); diff --git a/dev-packages/node-integration-tests/suites/express-v5/setupExpressErrorHandler/test.ts b/dev-packages/node-integration-tests/suites/express-v5/setupExpressErrorHandler/test.ts new file mode 100644 index 000000000000..ffc702d63057 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/express-v5/setupExpressErrorHandler/test.ts @@ -0,0 +1,30 @@ +import { cleanupChildProcesses, createRunner } from '../../../utils/runner'; + +describe('express setupExpressErrorHandler', () => { + afterAll(() => { + cleanupChildProcesses(); + }); + + describe('CJS', () => { + test('allows to pass options to setupExpressErrorHandler', done => { + const runner = createRunner(__dirname, 'server.js') + .expect({ + event: { + exception: { + values: [ + { + value: 'error_2', + }, + ], + }, + }, + }) + .start(done); + + // this error is filtered & ignored + runner.makeRequest('get', '/test1', { expectError: true }); + // this error is actually captured + runner.makeRequest('get', '/test2', { expectError: true }); + }); + }); +}); diff --git a/dev-packages/node-integration-tests/suites/express-v5/span-isolationScope/server.ts b/dev-packages/node-integration-tests/suites/express-v5/span-isolationScope/server.ts new file mode 100644 index 000000000000..99a9c53e932e --- /dev/null +++ b/dev-packages/node-integration-tests/suites/express-v5/span-isolationScope/server.ts @@ -0,0 +1,29 @@ +import { loggingTransport } from '@sentry-internal/node-integration-tests'; +import * as Sentry from '@sentry/node'; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', + tracesSampleRate: 1.0, + transport: loggingTransport, +}); + +import { startExpressServerAndSendPortToRunner } from '@sentry-internal/node-integration-tests'; +import express from 'express'; + +const app = express(); + +Sentry.setTag('global', 'tag'); + +app.get('/test/isolationScope', (_req, res) => { + // eslint-disable-next-line no-console + console.log('This is a test log.'); + Sentry.addBreadcrumb({ message: 'manual breadcrumb' }); + Sentry.setTag('isolation-scope', 'tag'); + + res.send({}); +}); + +Sentry.setupExpressErrorHandler(app); + +startExpressServerAndSendPortToRunner(app); diff --git a/dev-packages/node-integration-tests/suites/express-v5/span-isolationScope/test.ts b/dev-packages/node-integration-tests/suites/express-v5/span-isolationScope/test.ts new file mode 100644 index 000000000000..2e2b6945526e --- /dev/null +++ b/dev-packages/node-integration-tests/suites/express-v5/span-isolationScope/test.ts @@ -0,0 +1,38 @@ +import { cleanupChildProcesses, createRunner } from '../../../utils/runner'; + +afterAll(() => { + cleanupChildProcesses(); +}); + +test('correctly applies isolation scope to span', done => { + createRunner(__dirname, 'server.ts') + .expect({ + transaction: { + transaction: 'GET /test/isolationScope', + breadcrumbs: [ + { + category: 'console', + level: 'log', + message: expect.stringMatching(/\{"port":(\d+)\}/), + timestamp: expect.any(Number), + }, + { + category: 'console', + level: 'log', + message: 'This is a test log.', + timestamp: expect.any(Number), + }, + { + message: 'manual breadcrumb', + timestamp: expect.any(Number), + }, + ], + tags: { + global: 'tag', + 'isolation-scope': 'tag', + }, + }, + }) + .start(done) + .makeRequest('get', '/test/isolationScope'); +}); diff --git a/dev-packages/node-integration-tests/suites/express-v5/tracing/server.js b/dev-packages/node-integration-tests/suites/express-v5/tracing/server.js new file mode 100644 index 000000000000..f9b4ae24b339 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/express-v5/tracing/server.js @@ -0,0 +1,48 @@ +const { loggingTransport } = require('@sentry-internal/node-integration-tests'); +const Sentry = require('@sentry/node'); + +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, +}); + +// 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-integration-tests'); + +const app = express(); + +app.use(cors()); +app.use(bodyParser.json()); +app.use(bodyParser.text()); +app.use(bodyParser.raw()); + +app.get('/test/express', (_req, res) => { + res.send({ response: 'response 1' }); +}); + +app.get(/\/test\/regex/, (_req, res) => { + res.send({ response: 'response 2' }); +}); + +app.get(['/test/array1', /\/test\/array[2-9]/], (_req, res) => { + res.send({ response: 'response 3' }); +}); + +app.get(['/test/arr/:id', /\/test\/arr[0-9]*\/required(path)?(\/optionalPath)?\/(lastParam)?/], (_req, res) => { + res.send({ response: 'response 4' }); +}); + +app.post('/test-post', function (req, res) { + res.send({ status: 'ok', body: req.body }); +}); + +Sentry.setupExpressErrorHandler(app); + +startExpressServerAndSendPortToRunner(app); diff --git a/dev-packages/node-integration-tests/suites/express-v5/tracing/test.ts b/dev-packages/node-integration-tests/suites/express-v5/tracing/test.ts new file mode 100644 index 000000000000..6f87fdd89f76 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/express-v5/tracing/test.ts @@ -0,0 +1,240 @@ +import { cleanupChildProcesses, createRunner } from '../../../utils/runner'; + +describe('express tracing', () => { + afterAll(() => { + cleanupChildProcesses(); + }); + + describe('CJS', () => { + test('should create and send transactions for Express routes and spans for middlewares.', done => { + createRunner(__dirname, 'server.js') + .expect({ + transaction: { + contexts: { + trace: { + span_id: expect.stringMatching(/[a-f0-9]{16}/), + trace_id: expect.stringMatching(/[a-f0-9]{32}/), + data: { + url: expect.stringMatching(/\/test\/express$/), + 'http.response.status_code': 200, + }, + op: 'http.server', + status: 'ok', + }, + }, + spans: expect.arrayContaining([ + expect.objectContaining({ + data: expect.objectContaining({ + 'express.name': 'corsMiddleware', + 'express.type': 'middleware', + }), + description: 'corsMiddleware', + op: 'middleware.express', + origin: 'auto.http.otel.express', + }), + expect.objectContaining({ + data: expect.objectContaining({ + 'express.name': '/test/express', + 'express.type': 'request_handler', + }), + description: '/test/express', + op: 'request_handler.express', + origin: 'auto.http.otel.express', + }), + ]), + }, + }) + .start(done) + .makeRequest('get', '/test/express'); + }); + + test('should set a correct transaction name for routes specified in RegEx', done => { + createRunner(__dirname, 'server.js') + .expect({ + transaction: { + transaction: 'GET /\\/test\\/regex/', + transaction_info: { + source: 'route', + }, + contexts: { + trace: { + trace_id: expect.stringMatching(/[a-f0-9]{32}/), + span_id: expect.stringMatching(/[a-f0-9]{16}/), + data: { + url: expect.stringMatching(/\/test\/regex$/), + 'http.response.status_code': 200, + }, + op: 'http.server', + status: 'ok', + }, + }, + }, + }) + .start(done) + .makeRequest('get', '/test/regex'); + }); + + test.each([['array1'], ['array5']])( + 'should set a correct transaction name for routes consisting of arrays of routes for %p', + ((segment: string, done: () => void) => { + createRunner(__dirname, 'server.js') + .expect({ + transaction: { + transaction: 'GET /test/array1,/\\/test\\/array[2-9]/', + transaction_info: { + source: 'route', + }, + contexts: { + trace: { + trace_id: expect.stringMatching(/[a-f0-9]{32}/), + span_id: expect.stringMatching(/[a-f0-9]{16}/), + data: { + url: expect.stringMatching(`/test/${segment}$`), + 'http.response.status_code': 200, + }, + op: 'http.server', + status: 'ok', + }, + }, + }, + }) + .start(done) + .makeRequest('get', `/test/${segment}`); + }) as any, + ); + + test.each([ + ['arr/545'], + ['arr/required'], + ['arr/required'], + ['arr/requiredPath'], + ['arr/required/lastParam'], + ['arr55/required/lastParam'], + ])('should handle more complex regexes in route arrays correctly for %p', ((segment: string, done: () => void) => { + createRunner(__dirname, 'server.js') + .expect({ + transaction: { + transaction: 'GET /test/arr/:id,/\\/test\\/arr[0-9]*\\/required(path)?(\\/optionalPath)?\\/(lastParam)?/', + transaction_info: { + source: 'route', + }, + contexts: { + trace: { + trace_id: expect.stringMatching(/[a-f0-9]{32}/), + span_id: expect.stringMatching(/[a-f0-9]{16}/), + data: { + url: expect.stringMatching(`/test/${segment}$`), + 'http.response.status_code': 200, + }, + op: 'http.server', + status: 'ok', + }, + }, + }, + }) + .start(done) + .makeRequest('get', `/test/${segment}`); + }) as any); + + describe('request data', () => { + test('correctly captures JSON request data', done => { + const runner = createRunner(__dirname, 'server.js') + .expect({ + transaction: { + transaction: 'POST /test-post', + request: { + url: expect.stringMatching(/^http:\/\/localhost:(\d+)\/test-post$/), + method: 'POST', + headers: { + 'user-agent': expect.stringContaining(''), + 'content-type': 'application/json', + }, + data: JSON.stringify({ + foo: 'bar', + other: 1, + }), + }, + }, + }) + .start(done); + + runner.makeRequest('post', '/test-post', { data: { foo: 'bar', other: 1 } }); + }); + + test('correctly captures plain text request data', done => { + const runner = createRunner(__dirname, 'server.js') + .expect({ + transaction: { + transaction: 'POST /test-post', + request: { + url: expect.stringMatching(/^http:\/\/localhost:(\d+)\/test-post$/), + method: 'POST', + headers: { + 'user-agent': expect.stringContaining(''), + 'content-type': 'text/plain', + }, + data: 'some plain text', + }, + }, + }) + .start(done); + + runner.makeRequest('post', '/test-post', { + headers: { 'Content-Type': 'text/plain' }, + data: 'some plain text', + }); + }); + + test('correctly captures text buffer request data', done => { + const runner = createRunner(__dirname, 'server.js') + .expect({ + transaction: { + transaction: 'POST /test-post', + request: { + url: expect.stringMatching(/^http:\/\/localhost:(\d+)\/test-post$/), + method: 'POST', + headers: { + 'user-agent': expect.stringContaining(''), + 'content-type': 'application/octet-stream', + }, + data: 'some plain text in buffer', + }, + }, + }) + .start(done); + + runner.makeRequest('post', '/test-post', { + headers: { 'Content-Type': 'application/octet-stream' }, + data: Buffer.from('some plain text in buffer'), + }); + }); + + test('correctly captures non-text buffer request data', done => { + const runner = createRunner(__dirname, 'server.js') + .expect({ + transaction: { + transaction: 'POST /test-post', + request: { + url: expect.stringMatching(/^http:\/\/localhost:(\d+)\/test-post$/), + method: 'POST', + headers: { + 'user-agent': expect.stringContaining(''), + 'content-type': 'application/octet-stream', + }, + // This is some non-ascii string representation + data: expect.any(String), + }, + }, + }) + .start(done); + + const body = new Uint8Array([1, 2, 3, 4, 5]).buffer; + + runner.makeRequest('post', '/test-post', { + headers: { 'Content-Type': 'application/octet-stream' }, + data: body, + }); + }); + }); + }); +}); diff --git a/dev-packages/node-integration-tests/suites/express-v5/tracing/tracesSampler/scenario-normalizedRequest.js b/dev-packages/node-integration-tests/suites/express-v5/tracing/tracesSampler/scenario-normalizedRequest.js new file mode 100644 index 000000000000..da31780f2c5f --- /dev/null +++ b/dev-packages/node-integration-tests/suites/express-v5/tracing/tracesSampler/scenario-normalizedRequest.js @@ -0,0 +1,34 @@ +const { loggingTransport } = require('@sentry-internal/node-integration-tests'); +const Sentry = require('@sentry/node'); + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', + transport: loggingTransport, + tracesSampler: samplingContext => { + // The sampling decision is based on whether the data in `normalizedRequest` is available --> this is what we want to test for + return ( + samplingContext.normalizedRequest.url.includes('/test-normalized-request?query=123') && + samplingContext.normalizedRequest.method && + samplingContext.normalizedRequest.query_string === 'query=123' && + !!samplingContext.normalizedRequest.headers + ); + }, +}); + +// express must be required after Sentry is initialized +const express = require('express'); +const cors = require('cors'); +const { startExpressServerAndSendPortToRunner } = require('@sentry-internal/node-integration-tests'); + +const app = express(); + +app.use(cors()); + +app.get('/test-normalized-request', (_req, res) => { + res.send('Success'); +}); + +Sentry.setupExpressErrorHandler(app); + +startExpressServerAndSendPortToRunner(app); diff --git a/dev-packages/node-integration-tests/suites/express-v5/tracing/tracesSampler/server.js b/dev-packages/node-integration-tests/suites/express-v5/tracing/tracesSampler/server.js new file mode 100644 index 000000000000..b60ea07b636f --- /dev/null +++ b/dev-packages/node-integration-tests/suites/express-v5/tracing/tracesSampler/server.js @@ -0,0 +1,39 @@ +const { loggingTransport } = require('@sentry-internal/node-integration-tests'); +const Sentry = require('@sentry/node'); + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', + transport: loggingTransport, + tracesSampler: samplingContext => { + // The name we get here is inferred at span creation time + // At this point, we sadly do not have a http.route attribute yet, + // so we infer the name from the unparameterized route instead + return ( + samplingContext.name === 'GET /test/123' && + samplingContext.attributes['sentry.op'] === 'http.server' && + samplingContext.attributes['http.method'] === 'GET' + ); + }, +}); + +// express must be required after Sentry is initialized +const express = require('express'); +const cors = require('cors'); +const { startExpressServerAndSendPortToRunner } = require('@sentry-internal/node-integration-tests'); + +const app = express(); + +app.use(cors()); + +app.get('/test/:id', (_req, res) => { + res.send('Success'); +}); + +app.get('/test2', (_req, res) => { + res.send('Success'); +}); + +Sentry.setupExpressErrorHandler(app); + +startExpressServerAndSendPortToRunner(app); diff --git a/dev-packages/node-integration-tests/suites/express-v5/tracing/tracesSampler/test.ts b/dev-packages/node-integration-tests/suites/express-v5/tracing/tracesSampler/test.ts new file mode 100644 index 000000000000..07cc8d094d8f --- /dev/null +++ b/dev-packages/node-integration-tests/suites/express-v5/tracing/tracesSampler/test.ts @@ -0,0 +1,44 @@ +import { cleanupChildProcesses, createRunner } from '../../../../utils/runner'; + +describe('express tracesSampler', () => { + afterAll(() => { + cleanupChildProcesses(); + }); + + describe('CJS', () => { + test('correctly samples & passes data to tracesSampler', done => { + const runner = createRunner(__dirname, 'server.js') + .expect({ + transaction: { + transaction: 'GET /test/:id', + }, + }) + .start(done); + + // This is not sampled + runner.makeRequest('get', '/test2?q=1'); + // This is sampled + runner.makeRequest('get', '/test/123?q=1'); + }); + }); +}); + +describe('express tracesSampler includes normalizedRequest data', () => { + afterAll(() => { + cleanupChildProcesses(); + }); + + describe('CJS', () => { + test('correctly samples & passes data to tracesSampler', done => { + const runner = createRunner(__dirname, 'scenario-normalizedRequest.js') + .expect({ + transaction: { + transaction: 'GET /test-normalized-request', + }, + }) + .start(done); + + runner.makeRequest('get', '/test-normalized-request?query=123'); + }); + }); +}); diff --git a/dev-packages/node-integration-tests/suites/express-v5/tracing/updateName/server.js b/dev-packages/node-integration-tests/suites/express-v5/tracing/updateName/server.js new file mode 100644 index 000000000000..c98e17276d92 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/express-v5/tracing/updateName/server.js @@ -0,0 +1,58 @@ +const { loggingTransport } = require('@sentry-internal/node-integration-tests'); +const Sentry = require('@sentry/node'); + +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, +}); + +// 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-integration-tests'); + +const app = express(); + +app.use(cors()); +app.use(bodyParser.json()); +app.use(bodyParser.text()); +app.use(bodyParser.raw()); + +app.get('/test/:id/span-updateName', (_req, res) => { + const span = Sentry.getActiveSpan(); + const rootSpan = Sentry.getRootSpan(span); + rootSpan.updateName('new-name'); + res.send({ response: 'response 1' }); +}); + +app.get('/test/:id/span-updateName-source', (_req, res) => { + const span = Sentry.getActiveSpan(); + const rootSpan = Sentry.getRootSpan(span); + rootSpan.updateName('new-name'); + rootSpan.setAttribute(Sentry.SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, 'custom'); + res.send({ response: 'response 2' }); +}); + +app.get('/test/:id/updateSpanName', (_req, res) => { + const span = Sentry.getActiveSpan(); + const rootSpan = Sentry.getRootSpan(span); + Sentry.updateSpanName(rootSpan, 'new-name'); + res.send({ response: 'response 3' }); +}); + +app.get('/test/:id/updateSpanNameAndSource', (_req, res) => { + const span = Sentry.getActiveSpan(); + const rootSpan = Sentry.getRootSpan(span); + Sentry.updateSpanName(rootSpan, 'new-name'); + rootSpan.setAttribute(Sentry.SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, 'component'); + res.send({ response: 'response 4' }); +}); + +Sentry.setupExpressErrorHandler(app); + +startExpressServerAndSendPortToRunner(app); diff --git a/dev-packages/node-integration-tests/suites/express-v5/tracing/updateName/test.ts b/dev-packages/node-integration-tests/suites/express-v5/tracing/updateName/test.ts new file mode 100644 index 000000000000..c6345713fd7e --- /dev/null +++ b/dev-packages/node-integration-tests/suites/express-v5/tracing/updateName/test.ts @@ -0,0 +1,94 @@ +import { SEMANTIC_ATTRIBUTE_SENTRY_CUSTOM_SPAN_NAME } from '@sentry/core'; +import { SEMANTIC_ATTRIBUTE_SENTRY_SOURCE } from '@sentry/node'; +import { cleanupChildProcesses, createRunner } from '../../../../utils/runner'; + +describe('express tracing', () => { + afterAll(() => { + cleanupChildProcesses(); + }); + + describe('CJS', () => { + // This test documents the unfortunate behaviour of using `span.updateName` on the server-side. + // For http.server root spans (which is the root span on the server 99% of the time), Otel's http instrumentation + // calls `span.updateName` and overwrites whatever the name was set to before (by us or by users). + test("calling just `span.updateName` doesn't update the final name in express (missing source)", done => { + createRunner(__dirname, 'server.js') + .expect({ + transaction: { + transaction: 'GET /test/:id/span-updateName', + transaction_info: { + source: 'route', + }, + }, + }) + .start(done) + .makeRequest('get', '/test/123/span-updateName'); + }); + + // Also calling `updateName` AND setting a source doesn't change anything - Otel has no concept of source, this is sentry-internal. + // Therefore, only the source is updated but the name is still overwritten by Otel. + test("calling `span.updateName` and setting attribute source doesn't update the final name in express but it updates the source", done => { + createRunner(__dirname, 'server.js') + .expect({ + transaction: { + transaction: 'GET /test/:id/span-updateName-source', + transaction_info: { + source: 'custom', + }, + }, + }) + .start(done) + .makeRequest('get', '/test/123/span-updateName-source'); + }); + + // This test documents the correct way to update the span name (and implicitly the source) in Node: + test('calling `Sentry.updateSpanName` updates the final name and source in express', done => { + createRunner(__dirname, 'server.js') + .expect({ + transaction: txnEvent => { + expect(txnEvent).toMatchObject({ + transaction: 'new-name', + transaction_info: { + source: 'custom', + }, + contexts: { + trace: { + op: 'http.server', + data: { [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'custom' }, + }, + }, + }); + // ensure we delete the internal attribute once we're done with it + expect(txnEvent.contexts?.trace?.data?.[SEMANTIC_ATTRIBUTE_SENTRY_CUSTOM_SPAN_NAME]).toBeUndefined(); + }, + }) + .start(done) + .makeRequest('get', '/test/123/updateSpanName'); + }); + }); + + // This test documents the correct way to update the span name (and implicitly the source) in Node: + test('calling `Sentry.updateSpanName` and setting source subsequently updates the final name and sets correct source', done => { + createRunner(__dirname, 'server.js') + .expect({ + transaction: txnEvent => { + expect(txnEvent).toMatchObject({ + transaction: 'new-name', + transaction_info: { + source: 'component', + }, + contexts: { + trace: { + op: 'http.server', + data: { [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'component' }, + }, + }, + }); + // ensure we delete the internal attribute once we're done with it + expect(txnEvent.contexts?.trace?.data?.[SEMANTIC_ATTRIBUTE_SENTRY_CUSTOM_SPAN_NAME]).toBeUndefined(); + }, + }) + .start(done) + .makeRequest('get', '/test/123/updateSpanNameAndSource'); + }); +}); diff --git a/dev-packages/node-integration-tests/suites/express-v5/tracing/withError/server.js b/dev-packages/node-integration-tests/suites/express-v5/tracing/withError/server.js new file mode 100644 index 000000000000..d9ccc80fb7ad --- /dev/null +++ b/dev-packages/node-integration-tests/suites/express-v5/tracing/withError/server.js @@ -0,0 +1,30 @@ +const { loggingTransport } = require('@sentry-internal/node-integration-tests'); +const Sentry = require('@sentry/node'); + +Sentry.init({ + debug: true, + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', + // disable attaching headers to /test/* endpoints + tracePropagationTargets: [/^(?!.*test).*$/], + tracesSampleRate: 1.0, + transport: loggingTransport, +}); + +// express must be required after Sentry is initialized +const express = require('express'); +const cors = require('cors'); +const { startExpressServerAndSendPortToRunner } = require('@sentry-internal/node-integration-tests'); + +const app = express(); + +app.use(cors()); + +app.get('/test/:id1/:id2', (_req, res) => { + Sentry.captureException(new Error('error_1')); + res.send('Success'); +}); + +Sentry.setupExpressErrorHandler(app); + +startExpressServerAndSendPortToRunner(app); diff --git a/dev-packages/node-integration-tests/suites/express-v5/tracing/withError/test.ts b/dev-packages/node-integration-tests/suites/express-v5/tracing/withError/test.ts new file mode 100644 index 000000000000..4dd004ad2239 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/express-v5/tracing/withError/test.ts @@ -0,0 +1,28 @@ +import { cleanupChildProcesses, createRunner } from '../../../../utils/runner'; + +describe('express tracing experimental', () => { + afterAll(() => { + cleanupChildProcesses(); + }); + + describe('CJS', () => { + test('should apply the scope transactionName to error events', done => { + createRunner(__dirname, 'server.js') + .ignore('transaction') + .expect({ + event: { + exception: { + values: [ + { + value: 'error_1', + }, + ], + }, + transaction: 'GET /test/:id1/:id2', + }, + }) + .start(done) + .makeRequest('get', '/test/123/abc?q=1'); + }); + }); +}); diff --git a/dev-packages/node-integration-tests/suites/express-v5/without-tracing/server.ts b/dev-packages/node-integration-tests/suites/express-v5/without-tracing/server.ts new file mode 100644 index 000000000000..5b96e8b1a2a3 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/express-v5/without-tracing/server.ts @@ -0,0 +1,40 @@ +import { loggingTransport } from '@sentry-internal/node-integration-tests'; +import * as Sentry from '@sentry/node'; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', + transport: loggingTransport, +}); + +import { startExpressServerAndSendPortToRunner } from '@sentry-internal/node-integration-tests'; +import bodyParser from 'body-parser'; +import express from 'express'; + +const app = express(); + +app.use(bodyParser.json()); +app.use(bodyParser.text()); +app.use(bodyParser.raw()); + +Sentry.setTag('global', 'tag'); + +app.get('/test/isolationScope/:id', (req, res) => { + const id = req.params.id; + Sentry.setTag('isolation-scope', 'tag'); + Sentry.setTag(`isolation-scope-${id}`, id); + + Sentry.captureException(new Error('This is an exception')); + + res.send({}); +}); + +app.post('/test-post', function (req, res) { + Sentry.captureException(new Error('This is an exception')); + + res.send({ status: 'ok', body: req.body }); +}); + +Sentry.setupExpressErrorHandler(app); + +startExpressServerAndSendPortToRunner(app); diff --git a/dev-packages/node-integration-tests/suites/express-v5/without-tracing/test.ts b/dev-packages/node-integration-tests/suites/express-v5/without-tracing/test.ts new file mode 100644 index 000000000000..fdd63ad4aa4b --- /dev/null +++ b/dev-packages/node-integration-tests/suites/express-v5/without-tracing/test.ts @@ -0,0 +1,132 @@ +import { cleanupChildProcesses, createRunner } from '../../../utils/runner'; + +afterAll(() => { + cleanupChildProcesses(); +}); + +describe('express without tracing', () => { + test('correctly applies isolation scope even without tracing', done => { + const runner = createRunner(__dirname, 'server.ts') + .expect({ + event: { + transaction: 'GET /test/isolationScope/1', + tags: { + global: 'tag', + 'isolation-scope': 'tag', + 'isolation-scope-1': '1', + }, + // Request is correctly set + request: { + url: expect.stringMatching(/^http:\/\/localhost:(\d+)\/test\/isolationScope\/1$/), + method: 'GET', + headers: { + 'user-agent': expect.stringContaining(''), + }, + }, + }, + }) + .start(done); + + runner.makeRequest('get', '/test/isolationScope/1'); + }); + + describe('request data', () => { + test('correctly captures JSON request data', done => { + const runner = createRunner(__dirname, 'server.ts') + .expect({ + event: { + transaction: 'POST /test-post', + request: { + url: expect.stringMatching(/^http:\/\/localhost:(\d+)\/test-post$/), + method: 'POST', + headers: { + 'user-agent': expect.stringContaining(''), + 'content-type': 'application/json', + }, + data: JSON.stringify({ + foo: 'bar', + other: 1, + }), + }, + }, + }) + .start(done); + + runner.makeRequest('post', '/test-post', { data: { foo: 'bar', other: 1 } }); + }); + + test('correctly captures plain text request data', done => { + const runner = createRunner(__dirname, 'server.ts') + .expect({ + event: { + transaction: 'POST /test-post', + request: { + url: expect.stringMatching(/^http:\/\/localhost:(\d+)\/test-post$/), + method: 'POST', + headers: { + 'user-agent': expect.stringContaining(''), + 'content-type': 'text/plain', + }, + data: 'some plain text', + }, + }, + }) + .start(done); + + runner.makeRequest('post', '/test-post', { + headers: { + 'Content-Type': 'text/plain', + }, + data: 'some plain text', + }); + }); + + test('correctly captures text buffer request data', done => { + const runner = createRunner(__dirname, 'server.ts') + .expect({ + event: { + transaction: 'POST /test-post', + request: { + url: expect.stringMatching(/^http:\/\/localhost:(\d+)\/test-post$/), + method: 'POST', + headers: { + 'user-agent': expect.stringContaining(''), + 'content-type': 'application/octet-stream', + }, + data: 'some plain text in buffer', + }, + }, + }) + .start(done); + + runner.makeRequest('post', '/test-post', { + headers: { 'Content-Type': 'application/octet-stream' }, + data: Buffer.from('some plain text in buffer'), + }); + }); + + test('correctly captures non-text buffer request data', done => { + const runner = createRunner(__dirname, 'server.ts') + .expect({ + event: { + transaction: 'POST /test-post', + request: { + url: expect.stringMatching(/^http:\/\/localhost:(\d+)\/test-post$/), + method: 'POST', + headers: { + 'user-agent': expect.stringContaining(''), + 'content-type': 'application/octet-stream', + }, + // This is some non-ascii string representation + data: expect.any(String), + }, + }, + }) + .start(done); + + const body = new Uint8Array([1, 2, 3, 4, 5]).buffer; + + runner.makeRequest('post', '/test-post', { headers: { 'Content-Type': 'application/octet-stream' }, data: body }); + }); + }); +}); diff --git a/packages/node/src/integrations/tracing/express-v5/enums/AttributeNames.ts b/packages/node/src/integrations/tracing/express-v5/enums/AttributeNames.ts new file mode 100644 index 000000000000..f6a83e31b073 --- /dev/null +++ b/packages/node/src/integrations/tracing/express-v5/enums/AttributeNames.ts @@ -0,0 +1,19 @@ +/* + * 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. + */ +export enum AttributeNames { + EXPRESS_TYPE = 'express.type', + EXPRESS_NAME = 'express.name', +} diff --git a/packages/node/src/integrations/tracing/express-v5/enums/ExpressLayerType.ts b/packages/node/src/integrations/tracing/express-v5/enums/ExpressLayerType.ts new file mode 100644 index 000000000000..5cfc47c555d9 --- /dev/null +++ b/packages/node/src/integrations/tracing/express-v5/enums/ExpressLayerType.ts @@ -0,0 +1,20 @@ +/* + * 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. + */ +export enum ExpressLayerType { + ROUTER = 'router', + MIDDLEWARE = 'middleware', + REQUEST_HANDLER = 'request_handler', +} diff --git a/packages/node/src/integrations/tracing/express-v5/instrumentation.ts b/packages/node/src/integrations/tracing/express-v5/instrumentation.ts new file mode 100644 index 000000000000..bf2acb26c67d --- /dev/null +++ b/packages/node/src/integrations/tracing/express-v5/instrumentation.ts @@ -0,0 +1,324 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/member-ordering */ +/* eslint-disable guard-for-in */ +/* eslint-disable @typescript-eslint/ban-types */ +/* eslint-disable prefer-rest-params */ +/* eslint-disable @typescript-eslint/no-this-alias */ +/* eslint-disable jsdoc/require-jsdoc */ +/* eslint-disable @typescript-eslint/explicit-function-return-type */ +/* eslint-disable @typescript-eslint/explicit-member-accessibility */ + +/* + * 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. + */ + +import type { Attributes } from '@opentelemetry/api'; +import { SpanStatusCode, context, diag, trace } from '@opentelemetry/api'; +import { RPCType, getRPCMetadata } from '@opentelemetry/core'; +import { + InstrumentationBase, + InstrumentationNodeModuleDefinition, + isWrapped, + safeExecuteInTheMiddle, +} from '@opentelemetry/instrumentation'; +import { SEMATTRS_HTTP_ROUTE } from '@opentelemetry/semantic-conventions'; +import type * as express from 'express'; +import { AttributeNames } from './enums/AttributeNames'; +import { ExpressLayerType } from './enums/ExpressLayerType'; +import type { ExpressLayer, ExpressRouter, PatchedRequest } from './internal-types'; +import { _LAYERS_STORE_PROPERTY, kLayerPatched } from './internal-types'; +import type { ExpressInstrumentationConfig, ExpressRequestInfo } from './types'; +import { asErrorAndMessage, getLayerMetadata, getLayerPath, isLayerIgnored, storeLayerPath } from './utils'; + +export const PACKAGE_VERSION = '0.1.0'; +export const PACKAGE_NAME = '@sentry/instrumentation-express-v5'; + +/** Express instrumentation for OpenTelemetry */ +export class ExpressInstrumentationV5 extends InstrumentationBase { + constructor(config: ExpressInstrumentationConfig = {}) { + super(PACKAGE_NAME, PACKAGE_VERSION, config); + } + + init() { + return [ + new InstrumentationNodeModuleDefinition( + 'express', + ['>=5.0.0'], + moduleExports => this._setup(moduleExports), + moduleExports => this._tearDown(moduleExports), + ), + ]; + } + + private _setup(moduleExports: any) { + const routerProto = moduleExports.Router.prototype; + // patch express.Router.route + if (isWrapped(routerProto.route)) { + this._unwrap(routerProto, 'route'); + } + this._wrap(routerProto, 'route', this._getRoutePatch()); + // patch express.Router.use + if (isWrapped(routerProto.use)) { + this._unwrap(routerProto, 'use'); + } + this._wrap(routerProto, 'use', this._getRouterUsePatch() as any); + // patch express.Application.use + if (isWrapped(moduleExports.application.use)) { + this._unwrap(moduleExports.application, 'use'); + } + this._wrap(moduleExports.application, 'use', this._getAppUsePatch() as any); + return moduleExports; + } + + private _tearDown(moduleExports: any) { + if (moduleExports === undefined) return; + const routerProto = moduleExports.Router.prototype; + this._unwrap(routerProto, 'route'); + this._unwrap(routerProto, 'use'); + this._unwrap(moduleExports.application, 'use'); + } + + /** + * Get the patch for Router.route function + */ + private _getRoutePatch() { + const instrumentation = this; + return function (original: express.Router['route']) { + return function route_trace(this: ExpressRouter, ...args: Parameters) { + const route = original.apply(this, args); + const layer = this.stack[this.stack.length - 1] as ExpressLayer; + instrumentation._applyPatch(layer, getLayerPath(args)); + return route; + }; + }; + } + + /** + * Get the patch for Router.use function + */ + private _getRouterUsePatch() { + const instrumentation = this; + return function (original: express.Router['use']) { + return function use(this: express.Application, ...args: Parameters) { + const route = original.apply(this, args); + const layer = this.stack[this.stack.length - 1] as ExpressLayer; + instrumentation._applyPatch(layer, getLayerPath(args)); + return route; + }; + }; + } + + /** + * Get the patch for Application.use function + */ + private _getAppUsePatch() { + const instrumentation = this; + return function (original: express.Application['use']) { + return function use( + // In express 5.x the router is stored in `router` whereas in 4.x it's stored in `_router` + this: { _router?: ExpressRouter; router?: ExpressRouter }, + ...args: Parameters + ) { + // if we access app.router in express 4.x we trigger an assertion error + // This property existed in v3, was removed in v4 and then re-added in v5 + const router = this.router; + const route = original.apply(this, args); + if (router) { + const layer = router.stack[router.stack.length - 1] as ExpressLayer; + instrumentation._applyPatch(layer, getLayerPath(args)); + } + return route; + }; + }; + } + + /** Patch each express layer to create span and propagate context */ + private _applyPatch(this: ExpressInstrumentationV5, layer: ExpressLayer, layerPath?: string) { + const instrumentation = this; + // avoid patching multiple times the same layer + if (layer[kLayerPatched] === true) return; + layer[kLayerPatched] = true; + + this._wrap(layer, 'handle', original => { + // TODO: instrument error handlers + if (original.length === 4) return original; + + const patched = function (this: ExpressLayer, req: PatchedRequest, res: express.Response) { + storeLayerPath(req, layerPath); + const route = (req[_LAYERS_STORE_PROPERTY] as string[]) + .filter(path => path !== '/' && path !== '/*') + .join('') + // remove duplicate slashes to normalize route + .replace(/\/{2,}/g, '/'); + + const attributes: Attributes = { + // eslint-disable-next-line deprecation/deprecation + [SEMATTRS_HTTP_ROUTE]: route.length > 0 ? route : '/', + }; + const metadata = getLayerMetadata(route, layer, layerPath); + const type = metadata.attributes[AttributeNames.EXPRESS_TYPE] as ExpressLayerType; + + const rpcMetadata = getRPCMetadata(context.active()); + if (rpcMetadata?.type === RPCType.HTTP) { + rpcMetadata.route = route || '/'; + } + + // verify against the config if the layer should be ignored + if (isLayerIgnored(metadata.name, type, instrumentation.getConfig())) { + if (type === ExpressLayerType.MIDDLEWARE) { + (req[_LAYERS_STORE_PROPERTY] as string[]).pop(); + } + return original.apply(this, arguments); + } + + if (trace.getSpan(context.active()) === undefined) { + return original.apply(this, arguments); + } + + const spanName = instrumentation._getSpanName( + { + request: req, + layerType: type, + route, + }, + metadata.name, + ); + const span = instrumentation.tracer.startSpan(spanName, { + attributes: Object.assign(attributes, metadata.attributes), + }); + + const { requestHook } = instrumentation.getConfig(); + if (requestHook) { + safeExecuteInTheMiddle( + () => + requestHook(span, { + request: req, + layerType: type, + route, + }), + e => { + if (e) { + diag.error('express instrumentation: request hook failed', e); + } + }, + true, + ); + } + + let spanHasEnded = false; + if (metadata.attributes[AttributeNames.EXPRESS_TYPE] !== ExpressLayerType.MIDDLEWARE) { + span.end(); + spanHasEnded = true; + } + // listener for response.on('finish') + const onResponseFinish = () => { + if (spanHasEnded === false) { + spanHasEnded = true; + span.end(); + } + }; + + // verify we have a callback + const args = Array.from(arguments); + const callbackIdx = args.findIndex(arg => typeof arg === 'function'); + if (callbackIdx >= 0) { + arguments[callbackIdx] = function () { + // express considers anything but an empty value, "route" or "router" + // passed to its callback to be an error + const maybeError = arguments[0]; + const isError = ![undefined, null, 'route', 'router'].includes(maybeError); + if (!spanHasEnded && isError) { + const [error, message] = asErrorAndMessage(maybeError); + span.recordException(error); + span.setStatus({ + code: SpanStatusCode.ERROR, + message, + }); + } + + if (spanHasEnded === false) { + spanHasEnded = true; + req.res?.removeListener('finish', onResponseFinish); + span.end(); + } + if (!(req.route && isError)) { + (req[_LAYERS_STORE_PROPERTY] as string[]).pop(); + } + const callback = args[callbackIdx] as Function; + return callback.apply(this, arguments); + }; + } + + try { + return original.apply(this, arguments); + } catch (anyError) { + const [error, message] = asErrorAndMessage(anyError); + span.recordException(error); + span.setStatus({ + code: SpanStatusCode.ERROR, + message, + }); + throw anyError; + } finally { + /** + * At this point if the callback wasn't called, that means either the + * layer is asynchronous (so it will call the callback later on) or that + * the layer directly end the http response, so we'll hook into the "finish" + * event to handle the later case. + */ + if (!spanHasEnded) { + res.once('finish', onResponseFinish); + } + } + }; + + // `handle` isn't just a regular function in some cases. It also contains + // some properties holding metadata and state so we need to proxy them + // through through patched function + // ref: https://github.com/open-telemetry/opentelemetry-js-contrib/issues/1950 + // Also some apps/libs do their own patching before OTEL and have these properties + // in the proptotype. So we use a `for...in` loop to get own properties and also + // any enumerable prop in the prototype chain + // ref: https://github.com/open-telemetry/opentelemetry-js-contrib/issues/2271 + for (const key in original) { + Object.defineProperty(patched, key, { + get() { + return original[key]; + }, + set(value) { + original[key] = value; + }, + }); + } + return patched; + }); + } + + _getSpanName(info: ExpressRequestInfo, defaultName: string) { + const { spanNameHook } = this.getConfig(); + + if (!(spanNameHook instanceof Function)) { + return defaultName; + } + + try { + return spanNameHook(info, defaultName) ?? defaultName; + } catch (err) { + diag.error('express instrumentation: error calling span name rewrite hook', err); + return defaultName; + } + } +} diff --git a/packages/node/src/integrations/tracing/express-v5/internal-types.ts b/packages/node/src/integrations/tracing/express-v5/internal-types.ts new file mode 100644 index 000000000000..482dc0b6b4ea --- /dev/null +++ b/packages/node/src/integrations/tracing/express-v5/internal-types.ts @@ -0,0 +1,63 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +/* eslint-disable @typescript-eslint/ban-types */ + +/* + * 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. + */ + +import type { Request } from 'express'; + +/** + * This symbol is used to mark express layer as being already instrumented + * since its possible to use a given layer multiple times (ex: middlewares) + */ +export const kLayerPatched: unique symbol = Symbol('express-layer-patched'); + +/** + * This const define where on the `request` object the Instrumentation will mount the + * current stack of express layer. + * + * It is necessary because express doesn't store the different layers + * (ie: middleware, router etc) that it called to get to the current layer. + * Given that, the only way to know the route of a given layer is to + * store the path of where each previous layer has been mounted. + * + * ex: bodyParser > auth middleware > /users router > get /:id + * in this case the stack would be: ["/users", "/:id"] + * + * ex2: bodyParser > /api router > /v1 router > /users router > get /:id + * stack: ["/api", "/v1", "/users", ":id"] + * + */ +export const _LAYERS_STORE_PROPERTY = '__ot_middlewares'; + +export type PatchedRequest = { + [_LAYERS_STORE_PROPERTY]?: string[]; +} & Request; +export type PathParams = string | RegExp | Array; + +// https://github.com/expressjs/express/blob/main/lib/router/index.js#L53 +export type ExpressRouter = { + stack: ExpressLayer[]; +}; + +// https://github.com/expressjs/express/blob/main/lib/router/layer.js#L33 +export type ExpressLayer = { + handle: Function & Record; + [kLayerPatched]?: boolean; + name: string; + path: string; + route?: ExpressLayer; +}; diff --git a/packages/node/src/integrations/tracing/express-v5/types.ts b/packages/node/src/integrations/tracing/express-v5/types.ts new file mode 100644 index 000000000000..0623cac1cbc5 --- /dev/null +++ b/packages/node/src/integrations/tracing/express-v5/types.ts @@ -0,0 +1,65 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ + +/* + * 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. + */ + +import type { Span } from '@opentelemetry/api'; +import type { InstrumentationConfig } from '@opentelemetry/instrumentation'; +import type { ExpressLayerType } from './enums/ExpressLayerType'; + +export type LayerPathSegment = string | RegExp | number; + +export type IgnoreMatcher = string | RegExp | ((name: string) => boolean); + +export type ExpressRequestInfo = { + /** An express request object */ + request: T; + route: string; + layerType: ExpressLayerType; +}; + +export type SpanNameHook = ( + info: ExpressRequestInfo, + /** + * If no decision is taken based on RequestInfo, the default name + * supplied by the instrumentation can be used instead. + */ + defaultName: string, +) => string; + +/** + * Function that can be used to add custom attributes to the current span or the root span on + * a Express request + * @param span - The Express middleware layer span. + * @param info - An instance of ExpressRequestInfo that contains info about the request such as the route, and the layer type. + */ +export interface ExpressRequestCustomAttributeFunction { + (span: Span, info: ExpressRequestInfo): void; +} + +/** + * Options available for the Express Instrumentation (see [documentation](https://github.com/open-telemetry/opentelemetry-js-contrib/tree/main/plugins/node/opentelemetry-instrumentation-express#express-instrumentation-options)) + */ +export interface ExpressInstrumentationConfig extends InstrumentationConfig { + /** Ignore specific based on their name */ + ignoreLayers?: IgnoreMatcher[]; + /** Ignore specific layers based on their type */ + ignoreLayersType?: ExpressLayerType[]; + spanNameHook?: SpanNameHook; + + /** Function for adding custom attributes on Express request */ + requestHook?: ExpressRequestCustomAttributeFunction; +} diff --git a/packages/node/src/integrations/tracing/express-v5/utils.ts b/packages/node/src/integrations/tracing/express-v5/utils.ts new file mode 100644 index 000000000000..45ef61ed7eb6 --- /dev/null +++ b/packages/node/src/integrations/tracing/express-v5/utils.ts @@ -0,0 +1,191 @@ +/* eslint-disable @typescript-eslint/no-non-null-assertion */ +/* eslint-disable @typescript-eslint/explicit-function-return-type */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ + +/* + * 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. + */ + +import type { Attributes } from '@opentelemetry/api'; +import { AttributeNames } from './enums/AttributeNames'; +import { ExpressLayerType } from './enums/ExpressLayerType'; +import type { ExpressLayer, PatchedRequest } from './internal-types'; +import { _LAYERS_STORE_PROPERTY } from './internal-types'; +import type { ExpressInstrumentationConfig, IgnoreMatcher, LayerPathSegment } from './types'; + +/** + * Store layers path in the request to be able to construct route later + * @param request The request where + * @param [value] the value to push into the array + */ +export const storeLayerPath = (request: PatchedRequest, value?: string): void => { + if (Array.isArray(request[_LAYERS_STORE_PROPERTY]) === false) { + Object.defineProperty(request, _LAYERS_STORE_PROPERTY, { + enumerable: false, + value: [], + }); + } + if (value === undefined) return; + (request[_LAYERS_STORE_PROPERTY] as string[]).push(value); +}; + +/** + * Recursively search the router path from layer stack + * @param path The path to reconstruct + * @param layer The layer to reconstruct from + * @returns The reconstructed path + */ +export const getRouterPath = (path: string, layer: ExpressLayer): string => { + const stackLayer = layer.handle?.stack?.[0]; + + if (stackLayer?.route?.path) { + return `${path}${stackLayer.route.path}`; + } + + if (stackLayer?.handle?.stack) { + return getRouterPath(path, stackLayer); + } + + return path; +}; + +/** + * Parse express layer context to retrieve a name and attributes. + * @param route The route of the layer + * @param layer Express layer + * @param [layerPath] if present, the path on which the layer has been mounted + */ +export const getLayerMetadata = ( + route: string, + layer: ExpressLayer, + layerPath?: string, +): { + attributes: Attributes; + name: string; +} => { + if (layer.name === 'router') { + const maybeRouterPath = getRouterPath('', layer); + const extractedRouterPath = maybeRouterPath ? maybeRouterPath : layerPath || route || '/'; + + return { + attributes: { + [AttributeNames.EXPRESS_NAME]: extractedRouterPath, + [AttributeNames.EXPRESS_TYPE]: ExpressLayerType.ROUTER, + }, + name: `router - ${extractedRouterPath}`, + }; + } else if (layer.name === 'bound dispatch' || layer.name === 'handle') { + return { + attributes: { + [AttributeNames.EXPRESS_NAME]: (route || layerPath) ?? 'request handler', + [AttributeNames.EXPRESS_TYPE]: ExpressLayerType.REQUEST_HANDLER, + }, + name: `request handler${layer.path ? ` - ${route || layerPath}` : ''}`, + }; + } else { + return { + attributes: { + [AttributeNames.EXPRESS_NAME]: layer.name, + [AttributeNames.EXPRESS_TYPE]: ExpressLayerType.MIDDLEWARE, + }, + name: `middleware - ${layer.name}`, + }; + } +}; + +/** + * Check whether the given obj match pattern + * @param constant e.g URL of request + * @param obj obj to inspect + * @param pattern Match pattern + */ +const satisfiesPattern = (constant: string, pattern: IgnoreMatcher): boolean => { + if (typeof pattern === 'string') { + return pattern === constant; + } else if (pattern instanceof RegExp) { + return pattern.test(constant); + } else if (typeof pattern === 'function') { + return pattern(constant); + } else { + throw new TypeError('Pattern is in unsupported datatype'); + } +}; + +/** + * Check whether the given request is ignored by configuration + * It will not re-throw exceptions from `list` provided by the client + * @param constant e.g URL of request + * @param [list] List of ignore patterns + * @param [onException] callback for doing something when an exception has + * occurred + */ +export const isLayerIgnored = ( + name: string, + type: ExpressLayerType, + config?: ExpressInstrumentationConfig, +): boolean => { + if (Array.isArray(config?.ignoreLayersType) && config?.ignoreLayersType?.includes(type)) { + return true; + } + if (Array.isArray(config?.ignoreLayers) === false) return false; + try { + for (const pattern of config!.ignoreLayers!) { + if (satisfiesPattern(name, pattern)) { + return true; + } + } + } catch (e) { + /* catch block */ + } + + return false; +}; + +/** + * Converts a user-provided error value into an error and error message pair + * + * @param error - User-provided error value + * @returns Both an Error or string representation of the value and an error message + */ +export const asErrorAndMessage = (error: unknown): [error: string | Error, message: string] => + error instanceof Error ? [error, error.message] : [String(error), String(error)]; + +/** + * Extracts the layer path from the route arguments + * + * @param args - Arguments of the route + * @returns The layer path + */ +export const getLayerPath = (args: [LayerPathSegment | LayerPathSegment[], ...unknown[]]): string | undefined => { + const firstArg = args[0]; + + if (Array.isArray(firstArg)) { + return firstArg.map(arg => extractLayerPathSegment(arg) || '').join(','); + } + + return extractLayerPathSegment(firstArg); +}; + +const extractLayerPathSegment = (arg: LayerPathSegment) => { + if (typeof arg === 'string') { + return arg; + } + + if (arg instanceof RegExp || typeof arg === 'number') { + return arg.toString(); + } + + return; +}; diff --git a/packages/node/src/integrations/tracing/express.ts b/packages/node/src/integrations/tracing/express.ts index 13f50cff0202..cf6f6b233cdf 100644 --- a/packages/node/src/integrations/tracing/express.ts +++ b/packages/node/src/integrations/tracing/express.ts @@ -1,4 +1,6 @@ import type * as http from 'node:http'; +import type { Span } from '@opentelemetry/api'; +import type { ExpressRequestInfo } from '@opentelemetry/instrumentation-express'; import { ExpressInstrumentation } from '@opentelemetry/instrumentation-express'; import type { IntegrationFn } from '@sentry/core'; import { @@ -14,44 +16,58 @@ 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'; +const INTEGRATION_NAME_V5 = 'Express-V5'; + +function requestHook(span: Span): void { + addOriginToSpan(span, 'auto.http.otel.express'); + + const attributes = spanToJSON(span).data; + // this is one of: middleware, request_handler, router + const type = attributes['express.type']; + + if (type) { + span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_OP, `${type}.express`); + } + + // Also update the name, we don't need to "middleware - " prefix + const name = attributes['express.name']; + if (typeof name === 'string') { + span.updateName(name); + } +} + +function spanNameHook(info: ExpressRequestInfo, defaultName: string): string { + if (getIsolationScope() === getDefaultIsolationScope()) { + DEBUG_BUILD && logger.warn('Isolation scope is still default isolation scope - skipping setting transactionName'); + return defaultName; + } + if (info.layerType === 'request_handler') { + // type cast b/c Otel unfortunately types info.request as any :( + const req = info.request as { method?: string }; + const method = req.method ? req.method.toUpperCase() : 'GET'; + getIsolationScope().setTransactionName(`${method} ${info.route}`); + } + return defaultName; +} export const instrumentExpress = generateInstrumentOnce( INTEGRATION_NAME, () => new ExpressInstrumentation({ - requestHook(span) { - addOriginToSpan(span, 'auto.http.otel.express'); - - const attributes = spanToJSON(span).data; - // this is one of: middleware, request_handler, router - const type = attributes['express.type']; - - if (type) { - span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_OP, `${type}.express`); - } - - // Also update the name, we don't need to "middleware - " prefix - const name = attributes['express.name']; - if (typeof name === 'string') { - span.updateName(name); - } - }, - spanNameHook(info, defaultName) { - if (getIsolationScope() === getDefaultIsolationScope()) { - DEBUG_BUILD && - logger.warn('Isolation scope is still default isolation scope - skipping setting transactionName'); - return defaultName; - } - if (info.layerType === 'request_handler') { - // type cast b/c Otel unfortunately types info.request as any :( - const req = info.request as { method?: string }; - const method = req.method ? req.method.toUpperCase() : 'GET'; - getIsolationScope().setTransactionName(`${method} ${info.route}`); - } - return defaultName; - }, + requestHook: span => requestHook(span), + spanNameHook: (info, defaultName) => spanNameHook(info, defaultName), + }), +); + +export const instrumentExpressV5 = generateInstrumentOnce( + INTEGRATION_NAME_V5, + () => + new ExpressInstrumentationV5({ + requestHook: span => requestHook(span), + spanNameHook: (info, defaultName) => spanNameHook(info, defaultName), }), ); @@ -60,6 +76,7 @@ const _expressIntegration = (() => { name: INTEGRATION_NAME, setupOnce() { instrumentExpress(); + instrumentExpressV5(); }, }; }) satisfies IntegrationFn; diff --git a/packages/node/src/integrations/tracing/index.ts b/packages/node/src/integrations/tracing/index.ts index 7d06689f250d..2873a2643617 100644 --- a/packages/node/src/integrations/tracing/index.ts +++ b/packages/node/src/integrations/tracing/index.ts @@ -3,7 +3,7 @@ import { instrumentOtelHttp } from '../http'; import { amqplibIntegration, instrumentAmqplib } from './amqplib'; import { connectIntegration, instrumentConnect } from './connect'; -import { expressIntegration, instrumentExpress } from './express'; +import { expressIntegration, instrumentExpress, instrumentExpressV5 } from './express'; import { fastifyIntegration, instrumentFastify } from './fastify'; import { genericPoolIntegration, instrumentGenericPool } from './genericPool'; import { graphqlIntegration, instrumentGraphql } from './graphql'; @@ -58,6 +58,7 @@ export function getOpenTelemetryInstrumentationToPreload(): (((options?: any) => return [ instrumentOtelHttp, instrumentExpress, + instrumentExpressV5, instrumentConnect, instrumentFastify, instrumentHapi, From e5520aa404d10e67308c9038d4effec2105ca65b Mon Sep 17 00:00:00 2001 From: Eric Kim <6farer@users.noreply.github.com> Date: Thu, 20 Feb 2025 12:05:51 -0500 Subject: [PATCH 22/32] chore(docs): Update Remix docs to use recent integrations (#15346) Update remix readme to use the proper integrations reference for Prisma. --- packages/remix/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/remix/README.md b/packages/remix/README.md index 7914555728f1..fbf8480d5e25 100644 --- a/packages/remix/README.md +++ b/packages/remix/README.md @@ -48,7 +48,7 @@ import * as Sentry from '@sentry/remix'; Sentry.init({ dsn: '__DSN__', tracesSampleRate: 1, - integrations: [new Sentry.Integrations.Prisma({ client: prisma })], + integrations: [Sentry.prismaIntegration()], // ... }); ``` From e08c64018d518afa65e63761d79637e97c9ae07f Mon Sep 17 00:00:00 2001 From: Tim Fish Date: Thu, 20 Feb 2025 18:28:20 +0100 Subject: [PATCH 23/32] fix(bun): Includes correct sdk metadata (#15459) Extends the basic SDK test to ensure the metadata is correct and that we get the expected exception. --- packages/bun/src/client.ts | 5 +-- packages/bun/src/index.ts | 1 + packages/bun/src/sdk.ts | 15 +++++-- packages/bun/src/types.ts | 9 ---- .../bun/test/integrations/bunserver.test.ts | 21 +++++----- packages/bun/test/sdk.test.ts | 41 ++++++++++++++++--- 6 files changed, 60 insertions(+), 32 deletions(-) diff --git a/packages/bun/src/client.ts b/packages/bun/src/client.ts index 40e430dc2545..a96e0c04e264 100644 --- a/packages/bun/src/client.ts +++ b/packages/bun/src/client.ts @@ -5,10 +5,7 @@ import { ServerRuntimeClient, applySdkMetadata } from '@sentry/core'; import type { BunClientOptions } from './types'; /** - * The Sentry Bun SDK Client. - * - * @see BunClientOptions for documentation on configuration options. - * @see SentryClient for usage documentation. + * @deprecated This client is no longer used in v9. */ export class BunClient extends ServerRuntimeClient { /** diff --git a/packages/bun/src/index.ts b/packages/bun/src/index.ts index 770cf2eb2ebe..c25c65487a47 100644 --- a/packages/bun/src/index.ts +++ b/packages/bun/src/index.ts @@ -141,6 +141,7 @@ export { export type { BunOptions } from './types'; +// eslint-disable-next-line deprecation/deprecation export { BunClient } from './client'; export { getDefaultIntegrations, init } from './sdk'; export { bunServerIntegration } from './integrations/bunserver'; diff --git a/packages/bun/src/sdk.ts b/packages/bun/src/sdk.ts index 8bded2d492af..96f1b63f902d 100644 --- a/packages/bun/src/sdk.ts +++ b/packages/bun/src/sdk.ts @@ -1,4 +1,6 @@ +import * as os from 'node:os'; import { + applySdkMetadata, functionToStringIntegration, inboundFiltersIntegration, linkedErrorsIntegration, @@ -18,7 +20,6 @@ import { onUnhandledRejectionIntegration, } from '@sentry/node'; -import { BunClient } from './client'; import { bunServerIntegration } from './integrations/bunserver'; import { makeFetchTransport } from './transports'; import type { BunOptions } from './types'; @@ -92,8 +93,16 @@ export function getDefaultIntegrations(_options: Options): Integration[] { * * @see {@link BunOptions} for documentation on configuration options. */ -export function init(options: BunOptions = {}): NodeClient | undefined { - options.clientClass = BunClient; +export function init(userOptions: BunOptions = {}): NodeClient | undefined { + applySdkMetadata(userOptions, 'bun'); + + const options = { + ...userOptions, + platform: 'javascript', + runtime: { name: 'bun', version: Bun.version }, + serverName: userOptions.serverName || global.process.env.SENTRY_NAME || os.hostname(), + }; + options.transport = options.transport || makeFetchTransport; if (options.defaultIntegrations === undefined) { diff --git a/packages/bun/src/types.ts b/packages/bun/src/types.ts index fa0e2171214f..6cc08edd495d 100644 --- a/packages/bun/src/types.ts +++ b/packages/bun/src/types.ts @@ -1,6 +1,5 @@ import type { ClientOptions, Options, TracePropagationTargets } from '@sentry/core'; -import type { BunClient } from './client'; import type { BunTransportOptions } from './transports'; export interface BaseBunOptions { @@ -25,14 +24,6 @@ export interface BaseBunOptions { /** Sets an optional server name (device name) */ serverName?: string; - /** - * Specify a custom BunClient to be used. Must extend BunClient! - * This is not a public, supported API, but used internally only. - * - * @hidden - * */ - clientClass?: typeof BunClient; - /** Callback that is executed when a fatal global error occurs. */ onFatalError?(this: void, error: Error): void; } diff --git a/packages/bun/test/integrations/bunserver.test.ts b/packages/bun/test/integrations/bunserver.test.ts index e448402c0479..66a66476f78d 100644 --- a/packages/bun/test/integrations/bunserver.test.ts +++ b/packages/bun/test/integrations/bunserver.test.ts @@ -1,13 +1,14 @@ import { afterEach, beforeAll, beforeEach, describe, expect, test } from 'bun:test'; import type { Span } from '@sentry/core'; -import { getDynamicSamplingContextFromSpan, setCurrentClient, spanIsSampled, spanToJSON } from '@sentry/core'; +import { getDynamicSamplingContextFromSpan, spanIsSampled, spanToJSON } from '@sentry/core'; -import { BunClient } from '../../src/client'; +import { init } from '../../src'; +import type { NodeClient } from '../../src'; import { instrumentBunServe } from '../../src/integrations/bunserver'; import { getDefaultBunClientOptions } from '../helpers'; describe('Bun Serve Integration', () => { - let client: BunClient; + let client: NodeClient | undefined; // Fun fact: Bun = 2 21 14 :) let port: number = 22114; @@ -17,9 +18,7 @@ describe('Bun Serve Integration', () => { beforeEach(() => { const options = getDefaultBunClientOptions({ tracesSampleRate: 1 }); - client = new BunClient(options); - setCurrentClient(client); - client.init(); + client = init(options); }); afterEach(() => { @@ -31,7 +30,7 @@ describe('Bun Serve Integration', () => { test('generates a transaction around a request', async () => { let generatedSpan: Span | undefined; - client.on('spanEnd', span => { + client?.on('spanEnd', span => { generatedSpan = span; }); @@ -66,7 +65,7 @@ describe('Bun Serve Integration', () => { test('generates a post transaction', async () => { let generatedSpan: Span | undefined; - client.on('spanEnd', span => { + client?.on('spanEnd', span => { generatedSpan = span; }); @@ -103,7 +102,7 @@ describe('Bun Serve Integration', () => { let generatedSpan: Span | undefined; - client.on('spanEnd', span => { + client?.on('spanEnd', span => { generatedSpan = span; }); @@ -139,7 +138,7 @@ describe('Bun Serve Integration', () => { test('does not create transactions for OPTIONS or HEAD requests', async () => { let generatedSpan: Span | undefined; - client.on('spanEnd', span => { + client?.on('spanEnd', span => { generatedSpan = span; }); @@ -165,7 +164,7 @@ describe('Bun Serve Integration', () => { test('intruments the server again if it is reloaded', async () => { let serverWasInstrumented = false; - client.on('spanEnd', () => { + client?.on('spanEnd', () => { serverWasInstrumented = true; }); diff --git a/packages/bun/test/sdk.test.ts b/packages/bun/test/sdk.test.ts index 11870f30c101..c4f55ddbb3bb 100644 --- a/packages/bun/test/sdk.test.ts +++ b/packages/bun/test/sdk.test.ts @@ -1,20 +1,51 @@ import { describe, expect, test } from 'bun:test'; +import type { BaseTransportOptions, Envelope, Event, Transport, TransportMakeRequestResponse } from '@sentry/core'; +import type { NodeClient } from '../src/index'; import { init } from '../src/index'; +const envelopes: Envelope[] = []; + +function testTransport(_options: BaseTransportOptions): Transport { + return { + send(request: Envelope): Promise { + envelopes.push(request); + return Promise.resolve({ statusCode: 200 }); + }, + flush(): PromiseLike { + return new Promise(resolve => setTimeout(() => resolve(true), 100)); + }, + }; +} + describe('Bun SDK', () => { const initOptions = { dsn: 'https://00000000000000000000000000000000@o000000.ingest.sentry.io/0000000', tracesSampleRate: 1, + transport: testTransport, }; - test("calling init shouldn't fail", () => { + test('SDK works as expected', async () => { + let client: NodeClient | undefined; expect(() => { - init(initOptions); + client = init(initOptions); }).not.toThrow(); - }); - test('should return client from init', () => { - expect(init(initOptions)).not.toBeUndefined(); + expect(client).not.toBeUndefined(); + + client?.captureException(new Error('test')); + client?.flush(); + + await new Promise(resolve => setTimeout(resolve, 1000)); + + expect(envelopes.length).toBe(1); + + const envelope = envelopes[0]; + const event = envelope?.[1][0][1] as Event; + + expect(event.sdk?.name).toBe('sentry.javascript.bun'); + + expect(event.exception?.values?.[0]?.type).toBe('Error'); + expect(event.exception?.values?.[0]?.value).toBe('test'); }); }); From 9789f324c1118060ce022da6f1a90818096f89bc Mon Sep 17 00:00:00 2001 From: Daniel Griesser Date: Thu, 20 Feb 2025 18:28:37 +0100 Subject: [PATCH 24/32] chore: Add external contributor to CHANGELOG.md (#15460) This PR adds the external contributor to the CHANGELOG.md file, so that they are credited for their contribution. See #15346 Co-authored-by: AbhiPrasad <18689448+AbhiPrasad@users.noreply.github.com> --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d91d3da02552..bddbe3762189 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,8 @@ - "You miss 100 percent of the chances you don't take. — Wayne Gretzky" — Michael Scott +Work in this release was contributed by @6farer. Thank you for your contribution! + ## 9.1.0 - feat(browser): Add `graphqlClientIntegration` ([#13783](https://github.com/getsentry/sentry-javascript/pull/13783)) From e01a4283a18df8a3a73a00b1ee1e5e2c05b1311f Mon Sep 17 00:00:00 2001 From: Onur Temizkan Date: Thu, 20 Feb 2025 19:21:22 +0000 Subject: [PATCH 25/32] test(node): Add `pg-native` integration tests (#15206) Co-authored-by: Abhijeet Prasad --- .github/workflows/build.yml | 4 + .../node-integration-tests/package.json | 4 +- .../tracing/postgres/scenario-native.js | 47 ++++++ .../suites/tracing/postgres/test.ts | 50 ++++++ yarn.lock | 145 +++++++++++++----- 5 files changed, 208 insertions(+), 42 deletions(-) create mode 100644 dev-packages/node-integration-tests/suites/tracing/postgres/scenario-native.js diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index aae090f76188..f7341976d043 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -713,6 +713,10 @@ jobs: with: dependency_cache_key: ${{ needs.job_build.outputs.dependency_cache_key }} + - name: Build `libpq` + run: yarn libpq:build + working-directory: dev-packages/node-integration-tests + - name: Overwrite typescript version if: matrix.typescript == '3.8' run: node ./scripts/use-ts-3_8.js diff --git a/dev-packages/node-integration-tests/package.json b/dev-packages/node-integration-tests/package.json index bbb7e300ecee..08ede11390e7 100644 --- a/dev-packages/node-integration-tests/package.json +++ b/dev-packages/node-integration-tests/package.json @@ -16,6 +16,7 @@ "build:types": "tsc -p tsconfig.types.json", "clean": "rimraf -g **/node_modules && run-p clean:script", "clean:script": "node scripts/clean.js", + "libpq:build": "npm rebuild libpq", "express-v5-install": "cd suites/express-v5 && yarn --no-lockfile", "lint": "eslint . --format stylish", "fix": "eslint . --format stylish --fix", @@ -62,7 +63,8 @@ "nock": "^13.5.5", "node-cron": "^3.0.3", "node-schedule": "^2.1.1", - "pg": "^8.7.3", + "pg": "^8.13.1", + "pg-native": "3.2.0", "proxy": "^2.1.1", "redis-4": "npm:redis@^4.6.14", "reflect-metadata": "0.2.1", diff --git a/dev-packages/node-integration-tests/suites/tracing/postgres/scenario-native.js b/dev-packages/node-integration-tests/suites/tracing/postgres/scenario-native.js new file mode 100644 index 000000000000..ec4768217c42 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/tracing/postgres/scenario-native.js @@ -0,0 +1,47 @@ +const { loggingTransport } = require('@sentry-internal/node-integration-tests'); +const Sentry = require('@sentry/node'); + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', + tracesSampleRate: 1.0, + transport: loggingTransport, +}); + +// Stop the process from exiting before the transaction is sent +setInterval(() => {}, 1000); + +const { native } = require('pg'); +const { Client } = native; + +const client = new Client({ port: 5444, user: 'test', password: 'test', database: 'tests' }); + +async function run() { + await Sentry.startSpan( + { + name: 'Test Transaction', + op: 'transaction', + }, + async () => { + try { + await client.connect(); + + await client + .query( + 'CREATE TABLE "NativeUser" ("id" SERIAL NOT NULL,"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,"email" TEXT NOT NULL,"name" TEXT,CONSTRAINT "User_pkey" PRIMARY KEY ("id"));', + ) + .catch(() => { + // if this is not a fresh database, the table might already exist + }); + + await client.query('INSERT INTO "NativeUser" ("email", "name") VALUES ($1, $2)', ['tim', 'tim@domain.com']); + await client.query('SELECT * FROM "NativeUser"'); + } finally { + await client.end(); + } + }, + ); +} + +// eslint-disable-next-line @typescript-eslint/no-floating-promises +run(); diff --git a/dev-packages/node-integration-tests/suites/tracing/postgres/test.ts b/dev-packages/node-integration-tests/suites/tracing/postgres/test.ts index f2549c70eb90..9f8ba1449784 100644 --- a/dev-packages/node-integration-tests/suites/tracing/postgres/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/postgres/test.ts @@ -53,4 +53,54 @@ describe('postgres auto instrumentation', () => { .expect({ transaction: EXPECTED_TRANSACTION }) .start(done); }); + + test('should auto-instrument `pg-native` package', done => { + const EXPECTED_TRANSACTION = { + transaction: 'Test Transaction', + spans: expect.arrayContaining([ + expect.objectContaining({ + data: expect.objectContaining({ + 'db.system': 'postgresql', + 'db.name': 'tests', + 'sentry.origin': 'manual', + 'sentry.op': 'db', + }), + description: 'pg.connect', + op: 'db', + status: 'ok', + }), + expect.objectContaining({ + data: expect.objectContaining({ + 'db.system': 'postgresql', + 'db.name': 'tests', + 'db.statement': 'INSERT INTO "NativeUser" ("email", "name") VALUES ($1, $2)', + 'sentry.origin': 'auto.db.otel.postgres', + 'sentry.op': 'db', + }), + description: 'INSERT INTO "NativeUser" ("email", "name") VALUES ($1, $2)', + op: 'db', + status: 'ok', + origin: 'auto.db.otel.postgres', + }), + expect.objectContaining({ + data: expect.objectContaining({ + 'db.system': 'postgresql', + 'db.name': 'tests', + 'db.statement': 'SELECT * FROM "NativeUser"', + 'sentry.origin': 'auto.db.otel.postgres', + 'sentry.op': 'db', + }), + description: 'SELECT * FROM "NativeUser"', + op: 'db', + status: 'ok', + origin: 'auto.db.otel.postgres', + }), + ]), + }; + + createRunner(__dirname, 'scenario-native.js') + .withDockerCompose({ workingDirectory: [__dirname], readyMatches: ['port 5432'] }) + .expect({ transaction: EXPECTED_TRANSACTION }) + .start(done); + }); }); diff --git a/yarn.lock b/yarn.lock index 0c7b60c816e1..3243b6980af1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10688,7 +10688,7 @@ binary@^0.3.0: resolved "https://registry.yarnpkg.com/binaryextensions/-/binaryextensions-2.3.0.tgz#1d269cbf7e6243ea886aa41453c3651ccbe13c22" integrity sha512-nAihlQsYGyc5Bwq6+EsubvANYGExeJKHDO3RjnvwU042fawQTQfM3Kxn7IHUXQOz4bzfwsGYYHGSvXyW4zOGLg== -bindings@^1.4.0: +bindings@1.5.0, bindings@^1.4.0: version "1.5.0" resolved "https://registry.yarnpkg.com/bindings/-/bindings-1.5.0.tgz#10353c9e945334bc0511a6d90b38fbc7c9c504df" integrity sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ== @@ -11400,11 +11400,6 @@ buffer-more-ints@~1.0.0: resolved "https://registry.yarnpkg.com/buffer-more-ints/-/buffer-more-ints-1.0.0.tgz#ef4f8e2dddbad429ed3828a9c55d44f05c611422" integrity sha512-EMetuGFz5SLsT0QTnXzINh4Ksr+oo4i+UGTXEshiGCQWnsgSs7ZhJ8fzlwQ+OzEMs0MpDAMr1hxnblp5a4vcHg== -buffer-writer@2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/buffer-writer/-/buffer-writer-2.0.0.tgz#ce7eb81a38f7829db09c873f2fbb792c0c98ec04" - integrity sha512-a7ZpuTZU1TRtnwyCNW3I5dc0wWNC3VR9S++Ewyk2HHZdrO3CQJqSpd+95Us590V6AL7JqUAH2IwZ/398PmNFgw== - buffer@^5.5.0, buffer@^5.6.0: version "5.7.1" resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.7.1.tgz#ba62e7c13133053582197160851a8f648e99eed0" @@ -15603,11 +15598,6 @@ expect@^27.5.1: jest-matcher-utils "^27.5.1" jest-message-util "^27.5.1" -exponential-backoff@^3.1.1: - version "3.1.1" - resolved "https://registry.yarnpkg.com/exponential-backoff/-/exponential-backoff-3.1.1.tgz#64ac7526fe341ab18a39016cd22c787d01e00bf6" - integrity sha512-dX7e/LHVJ6W3DE1MHWi9S1EYzDESENfLrYohG2G++ovZrYOkm4Knwa0mc1cn84xJOR4KEU0WSchhLbd0UklbHw== - express@4.21.1, express@^4.10.7, express@^4.16.4, express@^4.17.1, express@^4.17.3, express@^4.18.1, express@^4.21.1: version "4.21.1" resolved "https://registry.yarnpkg.com/express/-/express-4.21.1.tgz#9dae5dda832f16b4eec941a4e44aa89ec481b281" @@ -19961,6 +19951,14 @@ libnpmpublish@7.3.0: sigstore "^1.4.0" ssri "^10.0.1" +libpq@1.8.13: + version "1.8.13" + resolved "https://registry.yarnpkg.com/libpq/-/libpq-1.8.13.tgz#d48af53c88defa7a20f958ef51bbbc0f58747355" + integrity sha512-t1wpnGVgwRIFSKoe4RFUllAFj953kNMcdXhGvFJwI0r6lJQqgSwTeiIciaCinjOmHk0HnFeWQSMC6Uw2591G4A== + dependencies: + bindings "1.5.0" + nan "2.19.0" + license-webpack-plugin@4.0.2: version "4.0.2" resolved "https://registry.yarnpkg.com/license-webpack-plugin/-/license-webpack-plugin-4.0.2.tgz#1e18442ed20b754b82f1adeff42249b81d11aec6" @@ -20470,7 +20468,12 @@ lru-cache@6.0.0, lru-cache@^6.0.0: dependencies: yallist "^4.0.0" -lru-cache@^10.2.0, lru-cache@^10.4.3: +lru-cache@^10.2.0: + version "10.2.2" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-10.2.2.tgz#48206bc114c1252940c41b25b41af5b545aca878" + integrity sha512-9hp3Vp2/hFQUiIwKo8XCeFVnrg8Pk3TYNPIR7tJADKi5YfcF7vEaK7avFHTlSy3kOKYaJQaalfEo6YuXdceBOQ== + +lru-cache@^10.4.3: version "10.4.3" resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-10.4.3.tgz#410fc8a17b70e598013df257c2446b7f3383f119" integrity sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ== @@ -21611,7 +21614,12 @@ minipass@^5.0.0: resolved "https://registry.yarnpkg.com/minipass/-/minipass-5.0.0.tgz#3e9788ffb90b694a5d0ec94479a45b5d8738133d" integrity sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ== -"minipass@^5.0.0 || ^6.0.2 || ^7.0.0", minipass@^7.1.2: +"minipass@^5.0.0 || ^6.0.2 || ^7.0.0": + version "7.0.3" + resolved "https://registry.yarnpkg.com/minipass/-/minipass-7.0.3.tgz#05ea638da44e475037ed94d1c7efcc76a25e1974" + integrity sha512-LhbbwCfz3vsb12j/WkWQPZfKTsgqIe1Nf/ti1pKjYESGLHIVjWU96G9/ljLH4F9mWNVhlQOm0VySdAWzf05dpg== + +minipass@^7.1.2: version "7.1.2" resolved "https://registry.yarnpkg.com/minipass/-/minipass-7.1.2.tgz#93a9626ce5e5e66bd4db86849e7515e92340a707" integrity sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw== @@ -21961,6 +21969,11 @@ named-placeholders@^1.1.3: dependencies: lru-cache "^7.14.1" +nan@2.19.0: + version "2.19.0" + resolved "https://registry.yarnpkg.com/nan/-/nan-2.19.0.tgz#bb58122ad55a6c5bc973303908d5b16cfdd5a8c0" + integrity sha512-nO1xXxfh/RWNxfd/XPfbIfFk5vgLsAxUR9y5O0cHMJu/AW9U95JLXqthYHjEp+8gQ5p96K9jUp8nbVOxCdRbtw== + nanoid@^3.3.3, nanoid@^3.3.4, nanoid@^3.3.6, nanoid@^3.3.7: version "3.3.7" resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.7.tgz#d0c301a691bc8d54efa0a2226ccf3fe2fd656bd8" @@ -22304,18 +22317,22 @@ node-forge@^1, node-forge@^1.3.1: resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-1.3.1.tgz#be8da2af243b2417d5f646a770663a92b7e9ded3" integrity sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA== -node-gyp-build@^4.2.2, node-gyp-build@^4.3.0: +node-gyp-build@^4.2.2: version "4.6.0" resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-4.6.0.tgz#0c52e4cbf54bbd28b709820ef7b6a3c2d6209055" integrity sha512-NTZVKn9IylLwUzaKjkas1e4u2DLNcV4rdYagA4PWdPwW87Bi7z+BznyKSRwS/761tV/lzCGXplWsiaMjLqP2zQ== +node-gyp-build@^4.3.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-4.5.0.tgz#7a64eefa0b21112f89f58379da128ac177f20e40" + integrity sha512-2iGbaQBV+ITgCz76ZEjmhUKAKVf7xfY1sRl4UiKQspfZMH2h06SyhNsnSVy50cwkFQDGLyif6m/6uFXHkOZ6rg== + node-gyp@^9.0.0: - version "9.4.1" - resolved "https://registry.yarnpkg.com/node-gyp/-/node-gyp-9.4.1.tgz#8a1023e0d6766ecb52764cc3a734b36ff275e185" - integrity sha512-OQkWKbjQKbGkMf/xqI1jjy3oCTgMKJac58G2+bjZb3fza6gW2YrCSdMQYaoTb70crvE//Gngr4f0AgVHmqHvBQ== + version "9.3.0" + resolved "https://registry.yarnpkg.com/node-gyp/-/node-gyp-9.3.0.tgz#f8eefe77f0ad8edb3b3b898409b53e697642b319" + integrity sha512-A6rJWfXFz7TQNjpldJ915WFb1LnhO4lIve3ANPbWreuEoLoKlFT3sxIepPBkLhM27crW8YmN+pjlgbasH6cH/Q== dependencies: env-paths "^2.2.0" - exponential-backoff "^3.1.1" glob "^7.1.4" graceful-fs "^4.2.6" make-fetch-happen "^10.0.3" @@ -23426,11 +23443,6 @@ package-name-regex@~2.0.6: resolved "https://registry.yarnpkg.com/package-name-regex/-/package-name-regex-2.0.6.tgz#b54bcb04d950e38082b7bb38fa558e01c1679334" integrity sha512-gFL35q7kbE/zBaPA3UKhp2vSzcPYx2ecbYuwv1ucE9Il6IIgBDweBlH8D68UFGZic2MkllKa2KHCfC1IQBQUYA== -packet-reader@1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/packet-reader/-/packet-reader-1.0.0.tgz#9238e5480dedabacfe1fe3f2771063f164157d74" - integrity sha512-HAKu/fG3HpHFO0AA8WE8q2g+gBJaZ9MG7fcKk+IJPLTGAD6Psw4443l+9DGRbOIh3/aXr7Phy0TjilYivJo5XQ== - pacote@13.6.2: version "13.6.2" resolved "https://registry.yarnpkg.com/pacote/-/pacote-13.6.2.tgz#0d444ba3618ab3e5cd330b451c22967bbd0ca48a" @@ -23780,31 +23792,65 @@ periscopic@^3.1.0: estree-walker "^3.0.0" is-reference "^3.0.0" -pg-connection-string@2.6.1, pg-connection-string@^2.5.0: +pg-cloudflare@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/pg-cloudflare/-/pg-cloudflare-1.1.1.tgz#e6d5833015b170e23ae819e8c5d7eaedb472ca98" + integrity sha512-xWPagP/4B6BgFO+EKz3JONXv3YDgvkbVrGw2mTo3D6tVDQRh1e7cqVGvyR3BE+eQgAvx1XhW/iEASj4/jCWl3Q== + +pg-connection-string@2.6.1: version "2.6.1" resolved "https://registry.yarnpkg.com/pg-connection-string/-/pg-connection-string-2.6.1.tgz#78c23c21a35dd116f48e12e23c0965e8d9e2cbfb" integrity sha512-w6ZzNu6oMmIzEAYVw+RLK0+nqHPt8K3ZnknKi+g48Ak2pr3dtljJW3o+D/n2zzCG07Zoe9VOX3aiKpj+BN0pjg== +pg-connection-string@^2.7.0: + version "2.7.0" + resolved "https://registry.yarnpkg.com/pg-connection-string/-/pg-connection-string-2.7.0.tgz#f1d3489e427c62ece022dba98d5262efcb168b37" + integrity sha512-PI2W9mv53rXJQEOb8xNR8lH7Hr+EKa6oJa38zsK0S/ky2er16ios1wLKhZyxzD7jUReiWokc9WK5nxSnC7W1TA== + pg-int8@1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/pg-int8/-/pg-int8-1.0.1.tgz#943bd463bf5b71b4170115f80f8efc9a0c0eb78c" integrity sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw== +pg-native@3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/pg-native/-/pg-native-3.2.0.tgz#1183a549c00741040f1f47f9167a6bf378206826" + integrity sha512-9q9I6RmT285DiRc0xkYb8e+bwOIIbnfVLddnzzXW35K1sZc74dR+symo2oeuzSW/sDQ8n24gWAvlGWK/GDJ3+Q== + dependencies: + libpq "1.8.13" + pg-types "^1.12.1" + pg-numeric@1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/pg-numeric/-/pg-numeric-1.0.2.tgz#816d9a44026086ae8ae74839acd6a09b0636aa3a" integrity sha512-BM/Thnrw5jm2kKLE5uJkXqqExRUY/toLHda65XgFTBTFYZyopbKjBe29Ii3RbkvlsMoFwD+tHeGaCjjv0gHlyw== -pg-pool@^3.5.1: - version "3.5.1" - resolved "https://registry.yarnpkg.com/pg-pool/-/pg-pool-3.5.1.tgz#f499ce76f9bf5097488b3b83b19861f28e4ed905" - integrity sha512-6iCR0wVrro6OOHFsyavV+i6KYL4lVNyYAB9RD18w66xSzN+d8b66HiwuP30Gp1SH5O9T82fckkzsRjlrhD0ioQ== +pg-pool@^3.7.0: + version "3.7.0" + resolved "https://registry.yarnpkg.com/pg-pool/-/pg-pool-3.7.0.tgz#d4d3c7ad640f8c6a2245adc369bafde4ebb8cbec" + integrity sha512-ZOBQForurqh4zZWjrgSwwAtzJ7QiRX0ovFkZr2klsen3Nm0aoh33Ls0fzfv3imeH/nw/O27cjdz5kzYJfeGp/g== -pg-protocol@*, pg-protocol@^1.5.0: +pg-protocol@*: version "1.5.0" resolved "https://registry.yarnpkg.com/pg-protocol/-/pg-protocol-1.5.0.tgz#b5dd452257314565e2d54ab3c132adc46565a6a0" integrity sha512-muRttij7H8TqRNu/DxrAJQITO4Ac7RmX3Klyr/9mJEOBeIpgnF8f9jAfRz5d3XwQZl5qBjF9gLsUtMPJE0vezQ== +pg-protocol@^1.7.0: + version "1.7.0" + resolved "https://registry.yarnpkg.com/pg-protocol/-/pg-protocol-1.7.0.tgz#ec037c87c20515372692edac8b63cf4405448a93" + integrity sha512-hTK/mE36i8fDDhgDFjy6xNOG+LCorxLG3WO17tku+ij6sVHXh1jQUJ8hYAnRhNla4QVD2H8er/FOjc/+EgC6yQ== + +pg-types@^1.12.1: + version "1.13.0" + resolved "https://registry.yarnpkg.com/pg-types/-/pg-types-1.13.0.tgz#75f490b8a8abf75f1386ef5ec4455ecf6b345c63" + integrity sha512-lfKli0Gkl/+za/+b6lzENajczwZHc7D5kiUCZfgm914jipD2kIOIvEkAhZ8GrW3/TUoP9w8FHjwpPObBye5KQQ== + dependencies: + pg-int8 "1.0.1" + postgres-array "~1.0.0" + postgres-bytea "~1.0.0" + postgres-date "~1.0.0" + postgres-interval "^1.1.0" + pg-types@^2.1.0, pg-types@^2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/pg-types/-/pg-types-2.2.0.tgz#2d0250d636454f7cfa3b6ae0382fdfa8063254a3" @@ -23829,18 +23875,18 @@ pg-types@^4.0.1: postgres-interval "^3.0.0" postgres-range "^1.1.1" -pg@^8.7.3: - version "8.7.3" - resolved "https://registry.yarnpkg.com/pg/-/pg-8.7.3.tgz#8a5bdd664ca4fda4db7997ec634c6e5455b27c44" - integrity sha512-HPmH4GH4H3AOprDJOazoIcpI49XFsHCe8xlrjHkWiapdbHK+HLtbm/GQzXYAZwmPju/kzKhjaSfMACG+8cgJcw== - dependencies: - buffer-writer "2.0.0" - packet-reader "1.0.0" - pg-connection-string "^2.5.0" - pg-pool "^3.5.1" - pg-protocol "^1.5.0" +pg@^8.13.1: + version "8.13.1" + resolved "https://registry.yarnpkg.com/pg/-/pg-8.13.1.tgz#6498d8b0a87ff76c2df7a32160309d3168c0c080" + integrity sha512-OUir1A0rPNZlX//c7ksiu7crsGZTKSOXJPgtNiHGIlC9H0lO+NC6ZDYksSgBYY/thSWhnSRBv8w1lieNNGATNQ== + dependencies: + pg-connection-string "^2.7.0" + pg-pool "^3.7.0" + pg-protocol "^1.7.0" pg-types "^2.1.0" pgpass "1.x" + optionalDependencies: + pg-cloudflare "^1.1.1" pgpass@1.x: version "1.0.5" @@ -24601,6 +24647,11 @@ postcss@^8.1.10, postcss@^8.2.14, postcss@^8.2.15, postcss@^8.3.7, postcss@^8.4. picocolors "^1.1.1" source-map-js "^1.2.1" +postgres-array@~1.0.0: + version "1.0.3" + resolved "https://registry.yarnpkg.com/postgres-array/-/postgres-array-1.0.3.tgz#c561fc3b266b21451fc6555384f4986d78ec80f5" + integrity sha512-5wClXrAP0+78mcsNX3/ithQ5exKvCyK5lr5NEEEeGwwM6NJdQgzIJBVxLvRW+huFpX92F2QnZ5CcokH0VhK2qQ== + postgres-array@~2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/postgres-array/-/postgres-array-2.0.0.tgz#48f8fce054fbc69671999329b8834b772652d82e" @@ -24623,7 +24674,7 @@ postgres-bytea@~3.0.0: dependencies: obuf "~1.1.2" -postgres-date@~1.0.4: +postgres-date@~1.0.0, postgres-date@~1.0.4: version "1.0.7" resolved "https://registry.yarnpkg.com/postgres-date/-/postgres-date-1.0.7.tgz#51bc086006005e5061c591cee727f2531bf641a8" integrity sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q== @@ -27980,7 +28031,19 @@ tar@6.1.11: mkdirp "^1.0.3" yallist "^4.0.0" -tar@^6.1.11, tar@^6.1.2, tar@^6.2.0: +tar@^6.1.11, tar@^6.1.2: + version "6.1.12" + resolved "https://registry.yarnpkg.com/tar/-/tar-6.1.12.tgz#3b742fb05669b55671fb769ab67a7791ea1a62e6" + integrity sha512-jU4TdemS31uABHd+Lt5WEYJuzn+TJTCBLljvIAHZOz6M9Os5pJ4dD+vRFLxPa/n3T0iEFzpi+0x1UfuDZYbRMw== + dependencies: + chownr "^2.0.0" + fs-minipass "^2.0.0" + minipass "^3.0.0" + minizlib "^2.1.1" + mkdirp "^1.0.3" + yallist "^4.0.0" + +tar@^6.2.0: version "6.2.1" resolved "https://registry.yarnpkg.com/tar/-/tar-6.2.1.tgz#717549c541bc3c2af15751bea94b1dd068d4b03a" integrity sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A== From 8f9f42caa3ed6bcb55fbd6cd990f55c4ce8f2b21 Mon Sep 17 00:00:00 2001 From: Sam Greening <2552620+SG60@users.noreply.github.com> Date: Fri, 21 Feb 2025 10:39:37 +0000 Subject: [PATCH 26/32] feat(sveltekit): Add Support for Cloudflare (#14672) This patch adds a different set of package exports for workers. To use this, you have to use the new `initCloudflareSentryHandle` SvelteKit handler function, before your call to `sentryHandle()`. --------- Co-authored-by: Lukas Stracke --- .../sveltekit-cloudflare-pages/.gitignore | 24 ++ .../sveltekit-cloudflare-pages/.npmrc | 2 + .../sveltekit-cloudflare-pages/README.md | 38 ++++ .../sveltekit-cloudflare-pages/package.json | 31 +++ .../playwright.config.ts | 10 + .../sveltekit-cloudflare-pages/src/app.d.ts | 13 ++ .../sveltekit-cloudflare-pages/src/app.html | 12 + .../src/hooks.client.ts | 8 + .../src/hooks.server.ts | 13 ++ .../src/routes/+page.server.ts | 7 + .../src/routes/+page.svelte | 10 + .../static/favicon.png | Bin 0 -> 1571 bytes .../svelte.config.js | 21 ++ .../tests/demo.test.ts | 6 + .../sveltekit-cloudflare-pages/tsconfig.json | 19 ++ .../sveltekit-cloudflare-pages/vite.config.ts | 7 + .../sveltekit-cloudflare-pages/wrangler.toml | 2 + packages/sveltekit/package.json | 5 + packages/sveltekit/rollup.npm.config.mjs | 9 +- packages/sveltekit/src/common/utils.ts | 2 + packages/sveltekit/src/index.types.ts | 6 + packages/sveltekit/src/index.worker.ts | 2 + .../sveltekit/src/server-common/handle.ts | 208 +++++++++++++++++ .../{server => server-common}/handleError.ts | 5 +- .../src/{server => server-common}/load.ts | 8 +- .../rewriteFramesIntegration.ts | 2 +- .../{server => server-common}/serverRoute.ts | 8 +- .../src/{server => server-common}/utils.ts | 3 +- packages/sveltekit/src/server/handle.ts | 212 ++---------------- packages/sveltekit/src/server/index.ts | 9 +- packages/sveltekit/src/server/sdk.ts | 4 +- packages/sveltekit/src/vite/autoInstrument.ts | 3 +- packages/sveltekit/src/vite/sourceMaps.ts | 2 +- packages/sveltekit/src/worker/cloudflare.ts | 43 ++++ packages/sveltekit/src/worker/index.ts | 90 ++++++++ packages/sveltekit/test/server/handle.test.ts | 27 ++- .../sveltekit/test/server/handleError.test.ts | 6 +- packages/sveltekit/test/server/load.test.ts | 6 +- .../server/rewriteFramesIntegration.test.ts | 2 +- .../sveltekit/test/server/serverRoute.test.ts | 6 +- packages/sveltekit/test/server/utils.test.ts | 2 +- 41 files changed, 660 insertions(+), 233 deletions(-) create mode 100644 dev-packages/e2e-tests/test-applications/sveltekit-cloudflare-pages/.gitignore create mode 100644 dev-packages/e2e-tests/test-applications/sveltekit-cloudflare-pages/.npmrc create mode 100644 dev-packages/e2e-tests/test-applications/sveltekit-cloudflare-pages/README.md create mode 100644 dev-packages/e2e-tests/test-applications/sveltekit-cloudflare-pages/package.json create mode 100644 dev-packages/e2e-tests/test-applications/sveltekit-cloudflare-pages/playwright.config.ts create mode 100644 dev-packages/e2e-tests/test-applications/sveltekit-cloudflare-pages/src/app.d.ts create mode 100644 dev-packages/e2e-tests/test-applications/sveltekit-cloudflare-pages/src/app.html create mode 100644 dev-packages/e2e-tests/test-applications/sveltekit-cloudflare-pages/src/hooks.client.ts create mode 100644 dev-packages/e2e-tests/test-applications/sveltekit-cloudflare-pages/src/hooks.server.ts create mode 100644 dev-packages/e2e-tests/test-applications/sveltekit-cloudflare-pages/src/routes/+page.server.ts create mode 100644 dev-packages/e2e-tests/test-applications/sveltekit-cloudflare-pages/src/routes/+page.svelte create mode 100644 dev-packages/e2e-tests/test-applications/sveltekit-cloudflare-pages/static/favicon.png create mode 100644 dev-packages/e2e-tests/test-applications/sveltekit-cloudflare-pages/svelte.config.js create mode 100644 dev-packages/e2e-tests/test-applications/sveltekit-cloudflare-pages/tests/demo.test.ts create mode 100644 dev-packages/e2e-tests/test-applications/sveltekit-cloudflare-pages/tsconfig.json create mode 100644 dev-packages/e2e-tests/test-applications/sveltekit-cloudflare-pages/vite.config.ts create mode 100644 dev-packages/e2e-tests/test-applications/sveltekit-cloudflare-pages/wrangler.toml create mode 100644 packages/sveltekit/src/index.worker.ts create mode 100644 packages/sveltekit/src/server-common/handle.ts rename packages/sveltekit/src/{server => server-common}/handleError.ts (95%) rename packages/sveltekit/src/{server => server-common}/load.ts (96%) rename packages/sveltekit/src/{server => server-common}/rewriteFramesIntegration.ts (97%) rename packages/sveltekit/src/{server => server-common}/serverRoute.ts (92%) rename packages/sveltekit/src/{server => server-common}/utils.ts (95%) create mode 100644 packages/sveltekit/src/worker/cloudflare.ts create mode 100644 packages/sveltekit/src/worker/index.ts diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-cloudflare-pages/.gitignore b/dev-packages/e2e-tests/test-applications/sveltekit-cloudflare-pages/.gitignore new file mode 100644 index 000000000000..bff793d5eae7 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/sveltekit-cloudflare-pages/.gitignore @@ -0,0 +1,24 @@ +test-results +node_modules + +# Output +.output +.vercel +.netlify +.wrangler +/.svelte-kit +/build + +# OS +.DS_Store +Thumbs.db + +# Env +.env +.env.* +!.env.example +!.env.test + +# Vite +vite.config.js.timestamp-* +vite.config.ts.timestamp-* diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-cloudflare-pages/.npmrc b/dev-packages/e2e-tests/test-applications/sveltekit-cloudflare-pages/.npmrc new file mode 100644 index 000000000000..070f80f05092 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/sveltekit-cloudflare-pages/.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/sveltekit-cloudflare-pages/README.md b/dev-packages/e2e-tests/test-applications/sveltekit-cloudflare-pages/README.md new file mode 100644 index 000000000000..b5b295070b44 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/sveltekit-cloudflare-pages/README.md @@ -0,0 +1,38 @@ +# sv + +Everything you need to build a Svelte project, powered by [`sv`](https://github.com/sveltejs/cli). + +## Creating a project + +If you're seeing this, you've probably already done this step. Congrats! + +```bash +# create a new project in the current directory +npx sv create + +# create a new project in my-app +npx sv create my-app +``` + +## Developing + +Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server: + +```bash +npm run dev + +# or start the server and open the app in a new browser tab +npm run dev -- --open +``` + +## Building + +To create a production version of your app: + +```bash +npm run build +``` + +You can preview the production build with `npm run preview`. + +> To deploy your app, you may need to install an [adapter](https://svelte.dev/docs/kit/adapters) for your target environment. diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-cloudflare-pages/package.json b/dev-packages/e2e-tests/test-applications/sveltekit-cloudflare-pages/package.json new file mode 100644 index 000000000000..51fe00136f06 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/sveltekit-cloudflare-pages/package.json @@ -0,0 +1,31 @@ +{ + "name": "sveltekit-cloudflare-pages", + "private": true, + "version": "0.0.1", + "type": "module", + "scripts": { + "dev": "vite dev", + "build": "vite build", + "preview": "wrangler pages dev ./.svelte-kit/cloudflare --port 4173", + "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", + "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", + "test:e2e": "playwright test", + "test": "pnpm run test:e2e", + "test:build": "pnpm install && pnpm build", + "test:assert": "pnpm run test:e2e" + }, + "dependencies": { + "@sentry/sveltekit": "latest || *" + }, + "devDependencies": { + "@playwright/test": "^1.45.3", + "@sveltejs/adapter-cloudflare": "^5.0.3", + "@sveltejs/kit": "^2.17.2", + "@sveltejs/vite-plugin-svelte": "^5.0.3", + "svelte": "^5.20.2", + "svelte-check": "^4.1.4", + "typescript": "^5.0.0", + "vite": "^6.1.1", + "wrangler": "3.105.0" + } +} diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-cloudflare-pages/playwright.config.ts b/dev-packages/e2e-tests/test-applications/sveltekit-cloudflare-pages/playwright.config.ts new file mode 100644 index 000000000000..18bda456025e --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/sveltekit-cloudflare-pages/playwright.config.ts @@ -0,0 +1,10 @@ +import { defineConfig } from '@playwright/test'; + +export default defineConfig({ + webServer: { + command: 'pnpm run build && pnpm run preview', + port: 4173, + }, + + testDir: 'tests', +}); diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-cloudflare-pages/src/app.d.ts b/dev-packages/e2e-tests/test-applications/sveltekit-cloudflare-pages/src/app.d.ts new file mode 100644 index 000000000000..520c4217a10c --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/sveltekit-cloudflare-pages/src/app.d.ts @@ -0,0 +1,13 @@ +// See https://svelte.dev/docs/kit/types#app.d.ts +// for information about these interfaces +declare global { + namespace App { + // interface Error {} + // interface Locals {} + // interface PageData {} + // interface PageState {} + // interface Platform {} + } +} + +export {}; diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-cloudflare-pages/src/app.html b/dev-packages/e2e-tests/test-applications/sveltekit-cloudflare-pages/src/app.html new file mode 100644 index 000000000000..77a5ff52c923 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/sveltekit-cloudflare-pages/src/app.html @@ -0,0 +1,12 @@ + + + + + + + %sveltekit.head% + + +
%sveltekit.body%
+ + diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-cloudflare-pages/src/hooks.client.ts b/dev-packages/e2e-tests/test-applications/sveltekit-cloudflare-pages/src/hooks.client.ts new file mode 100644 index 000000000000..4dc12acebc45 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/sveltekit-cloudflare-pages/src/hooks.client.ts @@ -0,0 +1,8 @@ +import { env } from '$env/dynamic/public'; +import * as Sentry from '@sentry/sveltekit'; + +Sentry.init({ + dsn: env.PUBLIC_E2E_TEST_DSN, +}); + +export const handleError = Sentry.handleErrorWithSentry(); diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-cloudflare-pages/src/hooks.server.ts b/dev-packages/e2e-tests/test-applications/sveltekit-cloudflare-pages/src/hooks.server.ts new file mode 100644 index 000000000000..d5067459d565 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/sveltekit-cloudflare-pages/src/hooks.server.ts @@ -0,0 +1,13 @@ +import { E2E_TEST_DSN } from '$env/static/private'; +import { handleErrorWithSentry, initCloudflareSentryHandle, sentryHandle } from '@sentry/sveltekit'; +import { sequence } from '@sveltejs/kit/hooks'; + +export const handleError = handleErrorWithSentry(); + +export const handle = sequence( + initCloudflareSentryHandle({ + dsn: E2E_TEST_DSN, + tracesSampleRate: 1.0, + }), + sentryHandle(), +); diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-cloudflare-pages/src/routes/+page.server.ts b/dev-packages/e2e-tests/test-applications/sveltekit-cloudflare-pages/src/routes/+page.server.ts new file mode 100644 index 000000000000..3cbde33753a2 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/sveltekit-cloudflare-pages/src/routes/+page.server.ts @@ -0,0 +1,7 @@ +import type { PageServerLoad } from './$types'; + +export const load: PageServerLoad = async function load() { + return { + message: 'From server load function.', + }; +}; diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-cloudflare-pages/src/routes/+page.svelte b/dev-packages/e2e-tests/test-applications/sveltekit-cloudflare-pages/src/routes/+page.svelte new file mode 100644 index 000000000000..e17881ceaca9 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/sveltekit-cloudflare-pages/src/routes/+page.svelte @@ -0,0 +1,10 @@ + + +

Welcome to SvelteKit

+

Visit svelte.dev/docs/kit to read the documentation

+ +prerender test + +

{data.message}

diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-cloudflare-pages/static/favicon.png b/dev-packages/e2e-tests/test-applications/sveltekit-cloudflare-pages/static/favicon.png new file mode 100644 index 0000000000000000000000000000000000000000..825b9e65af7c104cfb07089bb28659393b4f2097 GIT binary patch literal 1571 zcmV+;2Hg3HP)Px)-AP12RCwC$UE6KzI1p6{F2N z1VK2vi|pOpn{~#djwYcWXTI_im_u^TJgMZ4JMOsSj!0ma>B?-(Hr@X&W@|R-$}W@Z zgj#$x=!~7LGqHW?IO8+*oE1MyDp!G=L0#^lUx?;!fXv@l^6SvTnf^ac{5OurzC#ZMYc20lI%HhX816AYVs1T3heS1*WaWH z%;x>)-J}YB5#CLzU@GBR6sXYrD>Vw(Fmt#|JP;+}<#6b63Ike{Fuo!?M{yEffez;| zp!PfsuaC)>h>-AdbnwN13g*1LowNjT5?+lFVd#9$!8Z9HA|$*6dQ8EHLu}U|obW6f z2%uGv?vr=KNq7YYa2Roj;|zooo<)lf=&2yxM@e`kM$CmCR#x>gI>I|*Ubr({5Y^rb zghxQU22N}F51}^yfDSt786oMTc!W&V;d?76)9KXX1 z+6Okem(d}YXmmOiZq$!IPk5t8nnS{%?+vDFz3BevmFNgpIod~R{>@#@5x9zJKEHLHv!gHeK~n)Ld!M8DB|Kfe%~123&Hz1Z(86nU7*G5chmyDe ziV7$pB7pJ=96hpxHv9rCR29%bLOXlKU<_13_M8x)6;P8E1Kz6G<&P?$P^%c!M5`2` zfY2zg;VK5~^>TJGQzc+33-n~gKt{{of8GzUkWmU110IgI0DLxRIM>0US|TsM=L|@F z0Bun8U!cRB7-2apz=y-7*UxOxz@Z0)@QM)9wSGki1AZ38ceG7Q72z5`i;i=J`ILzL z@iUO?SBBG-0cQuo+an4TsLy-g-x;8P4UVwk|D8{W@U1Zi z!M)+jqy@nQ$p?5tsHp-6J304Q={v-B>66$P0IDx&YT(`IcZ~bZfmn11#rXd7<5s}y zBi9eim&zQc0Dk|2>$bs0PnLmDfMP5lcXRY&cvJ=zKxI^f0%-d$tD!`LBf9^jMSYUA zI8U?CWdY@}cRq6{5~y+)#h1!*-HcGW@+gZ4B};0OnC~`xQOyH19z*TA!!BJ%9s0V3F?CAJ{hTd#*tf+ur-W9MOURF-@B77_-OshsY}6 zOXRY=5%C^*26z?l)1=$bz30!so5tfABdSYzO+H=CpV~aaUefmjvfZ3Ttu9W&W3Iu6 zROlh0MFA5h;my}8lB0tAV-Rvc2Zs_CCSJnx@d`**$idgy-iMob4dJWWw|21b4NB=LfsYp0Aeh{Ov)yztQi;eL4y5 zMi>8^SzKqk8~k?UiQK^^-5d8c%bV?$F8%X~czyiaKCI2=UH { + await page.goto('/'); + await expect(page.locator('h1')).toBeVisible(); +}); diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-cloudflare-pages/tsconfig.json b/dev-packages/e2e-tests/test-applications/sveltekit-cloudflare-pages/tsconfig.json new file mode 100644 index 000000000000..0b2d8865f4ef --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/sveltekit-cloudflare-pages/tsconfig.json @@ -0,0 +1,19 @@ +{ + "extends": "./.svelte-kit/tsconfig.json", + "compilerOptions": { + "allowJs": true, + "checkJs": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "skipLibCheck": true, + "sourceMap": true, + "strict": true, + "moduleResolution": "bundler" + } + // Path aliases are handled by https://svelte.dev/docs/kit/configuration#alias + // except $lib which is handled by https://svelte.dev/docs/kit/configuration#files + // + // If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes + // from the referenced tsconfig.json - TypeScript does not merge them in +} diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-cloudflare-pages/vite.config.ts b/dev-packages/e2e-tests/test-applications/sveltekit-cloudflare-pages/vite.config.ts new file mode 100644 index 000000000000..706faf25f2b5 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/sveltekit-cloudflare-pages/vite.config.ts @@ -0,0 +1,7 @@ +import { sentrySvelteKit } from '@sentry/sveltekit'; +import { sveltekit } from '@sveltejs/kit/vite'; +import { defineConfig } from 'vite'; + +export default defineConfig({ + plugins: [sentrySvelteKit({ autoUploadSourceMaps: false }), sveltekit()], +}); diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-cloudflare-pages/wrangler.toml b/dev-packages/e2e-tests/test-applications/sveltekit-cloudflare-pages/wrangler.toml new file mode 100644 index 000000000000..d31d2fc7f225 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/sveltekit-cloudflare-pages/wrangler.toml @@ -0,0 +1,2 @@ +compatibility_date = "2024-12-17" +compatibility_flags = ["nodejs_compat"] diff --git a/packages/sveltekit/package.json b/packages/sveltekit/package.json index 664f057ee808..dbcc01675dc3 100644 --- a/packages/sveltekit/package.json +++ b/packages/sveltekit/package.json @@ -18,6 +18,10 @@ "./package.json": "./package.json", ".": { "types": "./build/types/index.types.d.ts", + "worker": { + "import": "./build/esm/index.worker.js", + "require": "./build/cjs/index.worker.js" + }, "browser": { "import": "./build/esm/index.client.js", "require": "./build/cjs/index.client.js" @@ -38,6 +42,7 @@ } }, "dependencies": { + "@sentry/cloudflare": "9.1.0", "@sentry/core": "9.1.0", "@sentry/node": "9.1.0", "@sentry/opentelemetry": "9.1.0", diff --git a/packages/sveltekit/rollup.npm.config.mjs b/packages/sveltekit/rollup.npm.config.mjs index b0a19e091ad8..ca0792cb4868 100644 --- a/packages/sveltekit/rollup.npm.config.mjs +++ b/packages/sveltekit/rollup.npm.config.mjs @@ -2,7 +2,14 @@ import { makeBaseNPMConfig, makeNPMConfigVariants } from '@sentry-internal/rollu export default makeNPMConfigVariants( makeBaseNPMConfig({ - entrypoints: ['src/index.server.ts', 'src/index.client.ts', 'src/client/index.ts', 'src/server/index.ts'], + entrypoints: [ + 'src/index.server.ts', + 'src/index.client.ts', + 'src/index.worker.ts', + 'src/client/index.ts', + 'src/server/index.ts', + 'src/worker/index.ts', + ], packageSpecificConfig: { external: ['$app/stores'], output: { diff --git a/packages/sveltekit/src/common/utils.ts b/packages/sveltekit/src/common/utils.ts index 84b384861dff..1362ee82293c 100644 --- a/packages/sveltekit/src/common/utils.ts +++ b/packages/sveltekit/src/common/utils.ts @@ -1,5 +1,7 @@ import type { HttpError, Redirect } from '@sveltejs/kit'; +export const WRAPPED_MODULE_SUFFIX = '?sentry-auto-wrap'; + export type SentryWrappedFlag = { /** * If this flag is set, we know that the load event was already wrapped once diff --git a/packages/sveltekit/src/index.types.ts b/packages/sveltekit/src/index.types.ts index 3ad8b728bb5f..bf2edbfb0a0f 100644 --- a/packages/sveltekit/src/index.types.ts +++ b/packages/sveltekit/src/index.types.ts @@ -4,6 +4,12 @@ export * from './client'; export * from './vite'; export * from './server'; +export * from './worker'; + +// Use the ./server version of some functions that are also exported from ./worker +export { sentryHandle } from './server'; +// Use the ./worker version of some functions that are also exported from ./server +export { initCloudflareSentryHandle } from './worker'; import type { Client, Integration, Options, StackParser } from '@sentry/core'; import type { HandleClientError, HandleServerError } from '@sveltejs/kit'; diff --git a/packages/sveltekit/src/index.worker.ts b/packages/sveltekit/src/index.worker.ts new file mode 100644 index 000000000000..016e36c8a289 --- /dev/null +++ b/packages/sveltekit/src/index.worker.ts @@ -0,0 +1,2 @@ +export * from './worker'; +// export * from './vite'; diff --git a/packages/sveltekit/src/server-common/handle.ts b/packages/sveltekit/src/server-common/handle.ts new file mode 100644 index 000000000000..48167066c6d7 --- /dev/null +++ b/packages/sveltekit/src/server-common/handle.ts @@ -0,0 +1,208 @@ +import type { Span } from '@sentry/core'; +import { + SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, + SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, + continueTrace, + getCurrentScope, + getDefaultIsolationScope, + getIsolationScope, + getTraceMetaTags, + logger, + setHttpStatus, + startSpan, + winterCGRequestToRequestData, + withIsolationScope, +} from '@sentry/core'; +import type { Handle, ResolveOptions } from '@sveltejs/kit'; + +import { DEBUG_BUILD } from '../common/debug-build'; +import { flushIfServerless, getTracePropagationData, sendErrorToSentry } from './utils'; + +export type SentryHandleOptions = { + /** + * Controls whether the SDK should capture errors and traces in requests that don't belong to a + * route defined in your SvelteKit application. + * + * By default, this option is set to `false` to reduce noise (e.g. bots sending random requests to your server). + * + * Set this option to `true` if you want to monitor requests events without a route. This might be useful in certain + * scenarios, for instance if you registered other handlers that handle these requests. + * If you set this option, you might want adjust the the transaction name in the `beforeSendTransaction` + * callback of your server-side `Sentry.init` options. You can also use `beforeSendTransaction` to filter out + * transactions that you still don't want to be sent to Sentry. + * + * @default false + */ + handleUnknownRoutes?: boolean; + + /** + * Controls if `sentryHandle` should inject a script tag into the page that enables instrumentation + * of `fetch` calls in `load` functions. + * + * @default true + */ + injectFetchProxyScript?: boolean; +}; + +export const FETCH_PROXY_SCRIPT = ` + const f = window.fetch; + if(f){ + window._sentryFetchProxy = function(...a){return f(...a)} + window.fetch = function(...a){return window._sentryFetchProxy(...a)} + } +`; +/** + * Adds Sentry tracing tags to the returned html page. + * Adds Sentry fetch proxy script to the returned html page if enabled in options. + * + * Exported only for testing + */ +export function addSentryCodeToPage(options: { injectFetchProxyScript: boolean }): NonNullable< + ResolveOptions['transformPageChunk'] +> { + return ({ html }) => { + const metaTags = getTraceMetaTags(); + const headWithMetaTags = metaTags ? `\n${metaTags}` : ''; + + const headWithFetchScript = options.injectFetchProxyScript ? `\n` : ''; + + const modifiedHead = `${headWithMetaTags}${headWithFetchScript}`; + + return html.replace('', modifiedHead); + }; +} + +/** + * We only need to inject the fetch proxy script for SvelteKit versions < 2.16.0. + * Exported only for testing. + */ +export function isFetchProxyRequired(version: string): boolean { + try { + const [major, minor] = version.trim().replace(/-.*/, '').split('.').map(Number); + if (major != null && minor != null && (major > 2 || (major === 2 && minor >= 16))) { + return false; + } + } catch { + // ignore + } + return true; +} + +async function instrumentHandle( + { event, resolve }: Parameters[0], + options: SentryHandleOptions, +): Promise { + if (!event.route?.id && !options.handleUnknownRoutes) { + return resolve(event); + } + + // caching the result of the version check in `options.injectFetchProxyScript` + // to avoid doing the dynamic import on every request + if (options.injectFetchProxyScript == null) { + try { + // @ts-expect-error - the dynamic import is fine here + const { VERSION } = await import('@sveltejs/kit'); + options.injectFetchProxyScript = isFetchProxyRequired(VERSION); + } catch { + options.injectFetchProxyScript = true; + } + } + + const routeName = `${event.request.method} ${event.route?.id || event.url.pathname}`; + + if (getIsolationScope() !== getDefaultIsolationScope()) { + getIsolationScope().setTransactionName(routeName); + } else { + DEBUG_BUILD && logger.warn('Isolation scope is default isolation scope - skipping setting transactionName'); + } + + try { + const resolveResult = await startSpan( + { + op: 'http.server', + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.sveltekit', + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: event.route?.id ? 'route' : 'url', + 'http.method': event.request.method, + }, + name: routeName, + }, + async (span?: Span) => { + getCurrentScope().setSDKProcessingMetadata({ + // We specifically avoid cloning the request here to avoid double read errors. + // We only read request headers so we're not consuming the body anyway. + // Note to future readers: This sounds counter-intuitive but please read + // https://github.com/getsentry/sentry-javascript/issues/14583 + normalizedRequest: winterCGRequestToRequestData(event.request), + }); + const res = await resolve(event, { + transformPageChunk: addSentryCodeToPage({ injectFetchProxyScript: options.injectFetchProxyScript ?? true }), + }); + if (span) { + setHttpStatus(span, res.status); + } + return res; + }, + ); + return resolveResult; + } catch (e: unknown) { + sendErrorToSentry(e, 'handle'); + throw e; + } finally { + await flushIfServerless(); + } +} + +/** + * A SvelteKit handle function that wraps the request for Sentry error and + * performance monitoring. + * + * Usage: + * ``` + * // src/hooks.server.ts + * import { sentryHandle } from '@sentry/sveltekit'; + * + * export const handle = sentryHandle(); + * + * // Optionally use the `sequence` function to add additional handlers. + * // export const handle = sequence(sentryHandle(), yourCustomHandler); + * ``` + */ +export function sentryHandle(handlerOptions?: SentryHandleOptions): Handle { + const { handleUnknownRoutes, ...rest } = handlerOptions ?? {}; + const options = { + handleUnknownRoutes: handleUnknownRoutes ?? false, + ...rest, + }; + + const sentryRequestHandler: Handle = input => { + // Escape hatch to suppress request isolation and trace continuation (see initCloudflareSentryHandle) + const skipIsolation = + '_sentrySkipRequestIsolation' in input.event.locals && input.event.locals._sentrySkipRequestIsolation; + + // In case of a same-origin `fetch` call within a server`load` function, + // SvelteKit will actually just re-enter the `handle` function and set `isSubRequest` + // to `true` so that no additional network call is made. + // We want the `http.server` span of that nested call to be a child span of the + // currently active span instead of a new root span to correctly reflect this + // behavior. + if (skipIsolation || input.event.isSubRequest) { + return instrumentHandle(input, options); + } + + return withIsolationScope(isolationScope => { + // We only call continueTrace in the initial top level request to avoid + // creating a new root span for the sub request. + isolationScope.setSDKProcessingMetadata({ + // We specifically avoid cloning the request here to avoid double read errors. + // We only read request headers so we're not consuming the body anyway. + // Note to future readers: This sounds counter-intuitive but please read + // https://github.com/getsentry/sentry-javascript/issues/14583 + normalizedRequest: winterCGRequestToRequestData(input.event.request), + }); + return continueTrace(getTracePropagationData(input.event), () => instrumentHandle(input, options)); + }); + }; + + return sentryRequestHandler; +} diff --git a/packages/sveltekit/src/server/handleError.ts b/packages/sveltekit/src/server-common/handleError.ts similarity index 95% rename from packages/sveltekit/src/server/handleError.ts rename to packages/sveltekit/src/server-common/handleError.ts index 30ca4e28de1a..0f9782282e48 100644 --- a/packages/sveltekit/src/server/handleError.ts +++ b/packages/sveltekit/src/server-common/handleError.ts @@ -1,8 +1,7 @@ -import { consoleSandbox } from '@sentry/core'; -import { captureException } from '@sentry/node'; +import { captureException, consoleSandbox } from '@sentry/core'; import type { HandleServerError } from '@sveltejs/kit'; -import { flushIfServerless } from './utils'; +import { flushIfServerless } from '../server-common/utils'; // The SvelteKit default error handler just logs the error's stack trace to the console // see: https://github.com/sveltejs/kit/blob/369e7d6851f543a40c947e033bfc4a9506fdc0a8/packages/kit/src/runtime/server/index.js#L43 diff --git a/packages/sveltekit/src/server/load.ts b/packages/sveltekit/src/server-common/load.ts similarity index 96% rename from packages/sveltekit/src/server/load.ts rename to packages/sveltekit/src/server-common/load.ts index 30fab345e05b..49160a65b4a5 100644 --- a/packages/sveltekit/src/server/load.ts +++ b/packages/sveltekit/src/server-common/load.ts @@ -1,5 +1,9 @@ -import { addNonEnumerableProperty } from '@sentry/core'; -import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, startSpan } from '@sentry/node'; +import { + SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, + SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, + addNonEnumerableProperty, + startSpan, +} from '@sentry/core'; import type { LoadEvent, ServerLoadEvent } from '@sveltejs/kit'; import type { SentryWrappedFlag } from '../common/utils'; diff --git a/packages/sveltekit/src/server/rewriteFramesIntegration.ts b/packages/sveltekit/src/server-common/rewriteFramesIntegration.ts similarity index 97% rename from packages/sveltekit/src/server/rewriteFramesIntegration.ts rename to packages/sveltekit/src/server-common/rewriteFramesIntegration.ts index 44afbca2d6df..d5928f8974b0 100644 --- a/packages/sveltekit/src/server/rewriteFramesIntegration.ts +++ b/packages/sveltekit/src/server-common/rewriteFramesIntegration.ts @@ -7,7 +7,7 @@ import { join, rewriteFramesIntegration as originalRewriteFramesIntegration, } from '@sentry/core'; -import { WRAPPED_MODULE_SUFFIX } from '../vite/autoInstrument'; +import { WRAPPED_MODULE_SUFFIX } from '../common/utils'; import type { GlobalWithSentryValues } from '../vite/injectGlobalValues'; type StackFrameIteratee = (frame: StackFrame) => StackFrame; diff --git a/packages/sveltekit/src/server/serverRoute.ts b/packages/sveltekit/src/server-common/serverRoute.ts similarity index 92% rename from packages/sveltekit/src/server/serverRoute.ts rename to packages/sveltekit/src/server-common/serverRoute.ts index 9d2cba3dbcdc..1b2169c58b8c 100644 --- a/packages/sveltekit/src/server/serverRoute.ts +++ b/packages/sveltekit/src/server-common/serverRoute.ts @@ -1,5 +1,9 @@ -import { addNonEnumerableProperty } from '@sentry/core'; -import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, startSpan } from '@sentry/node'; +import { + SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, + SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, + addNonEnumerableProperty, + startSpan, +} from '@sentry/core'; import type { RequestEvent } from '@sveltejs/kit'; import { flushIfServerless, sendErrorToSentry } from './utils'; diff --git a/packages/sveltekit/src/server/utils.ts b/packages/sveltekit/src/server-common/utils.ts similarity index 95% rename from packages/sveltekit/src/server/utils.ts rename to packages/sveltekit/src/server-common/utils.ts index 8eae93d531ab..d6f09093b74d 100644 --- a/packages/sveltekit/src/server/utils.ts +++ b/packages/sveltekit/src/server-common/utils.ts @@ -1,5 +1,4 @@ -import { logger, objectify } from '@sentry/core'; -import { captureException, flush } from '@sentry/node'; +import { captureException, flush, logger, objectify } from '@sentry/core'; import type { RequestEvent } from '@sveltejs/kit'; import { DEBUG_BUILD } from '../common/debug-build'; diff --git a/packages/sveltekit/src/server/handle.ts b/packages/sveltekit/src/server/handle.ts index 84f29a2c70c5..da429bc1040f 100644 --- a/packages/sveltekit/src/server/handle.ts +++ b/packages/sveltekit/src/server/handle.ts @@ -1,208 +1,24 @@ -import type { Span } from '@sentry/core'; -import { - SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, - SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, - continueTrace, - getCurrentScope, - getDefaultIsolationScope, - getIsolationScope, - getTraceMetaTags, - logger, - setHttpStatus, - startSpan, - winterCGRequestToRequestData, - withIsolationScope, -} from '@sentry/core'; -import type { Handle, ResolveOptions } from '@sveltejs/kit'; - -import { DEBUG_BUILD } from '../common/debug-build'; -import { flushIfServerless, getTracePropagationData, sendErrorToSentry } from './utils'; - -export type SentryHandleOptions = { - /** - * Controls whether the SDK should capture errors and traces in requests that don't belong to a - * route defined in your SvelteKit application. - * - * By default, this option is set to `false` to reduce noise (e.g. bots sending random requests to your server). - * - * Set this option to `true` if you want to monitor requests events without a route. This might be useful in certain - * scenarios, for instance if you registered other handlers that handle these requests. - * If you set this option, you might want adjust the the transaction name in the `beforeSendTransaction` - * callback of your server-side `Sentry.init` options. You can also use `beforeSendTransaction` to filter out - * transactions that you still don't want to be sent to Sentry. - * - * @default false - */ - handleUnknownRoutes?: boolean; - - /** - * Controls if `sentryHandle` should inject a script tag into the page that enables instrumentation - * of `fetch` calls in `load` functions. - * - * @default true - */ - injectFetchProxyScript?: boolean; -}; +import type { CloudflareOptions } from '@sentry/cloudflare'; +import type { Handle } from '@sveltejs/kit'; +import { init } from './sdk'; /** - * Exported only for testing - */ -export const FETCH_PROXY_SCRIPT = ` - const f = window.fetch; - if(f){ - window._sentryFetchProxy = function(...a){return f(...a)} - window.fetch = function(...a){return window._sentryFetchProxy(...a)} - } -`; - -/** - * Adds Sentry tracing tags to the returned html page. - * Adds Sentry fetch proxy script to the returned html page if enabled in options. - * - * Exported only for testing - */ -export function addSentryCodeToPage(options: { injectFetchProxyScript: boolean }): NonNullable< - ResolveOptions['transformPageChunk'] -> { - return ({ html }) => { - const metaTags = getTraceMetaTags(); - const headWithMetaTags = metaTags ? `\n${metaTags}` : ''; - - const headWithFetchScript = options.injectFetchProxyScript ? `\n` : ''; - - const modifiedHead = `${headWithMetaTags}${headWithFetchScript}`; - - return html.replace('', modifiedHead); - }; -} - -/** - * A SvelteKit handle function that wraps the request for Sentry error and - * performance monitoring. - * - * Usage: - * ``` - * // src/hooks.server.ts - * import { sentryHandle } from '@sentry/sveltekit'; + * Actual implementation in ../worker/handle.ts * - * export const handle = sentryHandle(); + * This handler initializes the Sentry Node(!) SDK with the passed options. This is necessary to get + * the SDK configured for cloudflare working in dev mode. * - * // Optionally use the `sequence` function to add additional handlers. - * // export const handle = sequence(sentryHandle(), yourCustomHandler); - * ``` + * @return version of initCLoudflareSentryHandle that is called via node/server entry point */ -export function sentryHandle(handlerOptions?: SentryHandleOptions): Handle { - const { handleUnknownRoutes, ...rest } = handlerOptions ?? {}; - const options = { - handleUnknownRoutes: handleUnknownRoutes ?? false, - ...rest, - }; +export function initCloudflareSentryHandle(options: CloudflareOptions): Handle { + let sentryInitialized = false; - const sentryRequestHandler: Handle = input => { - // In case of a same-origin `fetch` call within a server`load` function, - // SvelteKit will actually just re-enter the `handle` function and set `isSubRequest` - // to `true` so that no additional network call is made. - // We want the `http.server` span of that nested call to be a child span of the - // currently active span instead of a new root span to correctly reflect this - // behavior. - if (input.event.isSubRequest) { - return instrumentHandle(input, options); + return ({ event, resolve }) => { + if (!sentryInitialized) { + sentryInitialized = true; + init(options); } - return withIsolationScope(isolationScope => { - // We only call continueTrace in the initial top level request to avoid - // creating a new root span for the sub request. - isolationScope.setSDKProcessingMetadata({ - // We specifically avoid cloning the request here to avoid double read errors. - // We only read request headers so we're not consuming the body anyway. - // Note to future readers: This sounds counter-intuitive but please read - // https://github.com/getsentry/sentry-javascript/issues/14583 - normalizedRequest: winterCGRequestToRequestData(input.event.request), - }); - return continueTrace(getTracePropagationData(input.event), () => instrumentHandle(input, options)); - }); - }; - - return sentryRequestHandler; -} - -async function instrumentHandle( - { event, resolve }: Parameters[0], - options: SentryHandleOptions, -): Promise { - if (!event.route?.id && !options.handleUnknownRoutes) { return resolve(event); - } - - // caching the result of the version check in `options.injectFetchProxyScript` - // to avoid doing the dynamic import on every request - if (options.injectFetchProxyScript == null) { - try { - // @ts-expect-error - the dynamic import is fine here - const { VERSION } = await import('@sveltejs/kit'); - options.injectFetchProxyScript = isFetchProxyRequired(VERSION); - } catch { - options.injectFetchProxyScript = true; - } - } - - const routeName = `${event.request.method} ${event.route?.id || event.url.pathname}`; - - if (getIsolationScope() !== getDefaultIsolationScope()) { - getIsolationScope().setTransactionName(routeName); - } else { - DEBUG_BUILD && logger.warn('Isolation scope is default isolation scope - skipping setting transactionName'); - } - - try { - const resolveResult = await startSpan( - { - op: 'http.server', - attributes: { - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.sveltekit', - [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: event.route?.id ? 'route' : 'url', - 'http.method': event.request.method, - }, - name: routeName, - }, - async (span?: Span) => { - getCurrentScope().setSDKProcessingMetadata({ - // We specifically avoid cloning the request here to avoid double read errors. - // We only read request headers so we're not consuming the body anyway. - // Note to future readers: This sounds counter-intuitive but please read - // https://github.com/getsentry/sentry-javascript/issues/14583 - normalizedRequest: winterCGRequestToRequestData(event.request), - }); - const res = await resolve(event, { - transformPageChunk: addSentryCodeToPage({ injectFetchProxyScript: options.injectFetchProxyScript ?? true }), - }); - if (span) { - setHttpStatus(span, res.status); - } - return res; - }, - ); - return resolveResult; - } catch (e: unknown) { - sendErrorToSentry(e, 'handle'); - throw e; - } finally { - await flushIfServerless(); - } -} - -/** - * We only need to inject the fetch proxy script for SvelteKit versions < 2.16.0. - * Exported only for testing. - */ -export function isFetchProxyRequired(version: string): boolean { - try { - const [major, minor] = version.trim().replace(/-.*/, '').split('.').map(Number); - if (major != null && minor != null && (major > 2 || (major === 2 && minor >= 16))) { - return false; - } - } catch { - // ignore - } - return true; + }; } diff --git a/packages/sveltekit/src/server/index.ts b/packages/sveltekit/src/server/index.ts index 232e0562eb22..ccd09570b674 100644 --- a/packages/sveltekit/src/server/index.ts +++ b/packages/sveltekit/src/server/index.ts @@ -123,10 +123,11 @@ export * from '@sentry/node'; // ------------------------- // SvelteKit SDK exports: export { init } from './sdk'; -export { handleErrorWithSentry } from './handleError'; -export { wrapLoadWithSentry, wrapServerLoadWithSentry } from './load'; -export { sentryHandle } from './handle'; -export { wrapServerRouteWithSentry } from './serverRoute'; +export { handleErrorWithSentry } from '../server-common/handleError'; +export { wrapLoadWithSentry, wrapServerLoadWithSentry } from '../server-common/load'; +export { sentryHandle } from '../server-common/handle'; +export { initCloudflareSentryHandle } from './handle'; +export { wrapServerRouteWithSentry } from '../server-common/serverRoute'; /** * Tracks the Svelte component's initialization and mounting operation as well as diff --git a/packages/sveltekit/src/server/sdk.ts b/packages/sveltekit/src/server/sdk.ts index 7f3acbf57fbd..66362e96a729 100644 --- a/packages/sveltekit/src/server/sdk.ts +++ b/packages/sveltekit/src/server/sdk.ts @@ -3,10 +3,10 @@ import type { NodeClient, NodeOptions } from '@sentry/node'; import { getDefaultIntegrations as getDefaultNodeIntegrations } from '@sentry/node'; import { init as initNodeSdk } from '@sentry/node'; -import { rewriteFramesIntegration } from './rewriteFramesIntegration'; +import { rewriteFramesIntegration } from '../server-common/rewriteFramesIntegration'; /** - * + * Initialize the Server-side Sentry SDK * @param options */ export function init(options: NodeOptions): NodeClient | undefined { diff --git a/packages/sveltekit/src/vite/autoInstrument.ts b/packages/sveltekit/src/vite/autoInstrument.ts index 1e11f2f61500..8303af502f90 100644 --- a/packages/sveltekit/src/vite/autoInstrument.ts +++ b/packages/sveltekit/src/vite/autoInstrument.ts @@ -3,8 +3,7 @@ import * as path from 'path'; import type { ExportNamedDeclaration } from '@babel/types'; import { parseModule } from 'magicast'; import type { Plugin } from 'vite'; - -export const WRAPPED_MODULE_SUFFIX = '?sentry-auto-wrap'; +import { WRAPPED_MODULE_SUFFIX } from '../common/utils'; export type AutoInstrumentSelection = { /** diff --git a/packages/sveltekit/src/vite/sourceMaps.ts b/packages/sveltekit/src/vite/sourceMaps.ts index 78ee6389c5da..69fa4f1b2121 100644 --- a/packages/sveltekit/src/vite/sourceMaps.ts +++ b/packages/sveltekit/src/vite/sourceMaps.ts @@ -9,7 +9,7 @@ import { sentryVitePlugin } from '@sentry/vite-plugin'; import type { Plugin, UserConfig } from 'vite'; import MagicString from 'magic-string'; -import { WRAPPED_MODULE_SUFFIX } from './autoInstrument'; +import { WRAPPED_MODULE_SUFFIX } from '../common/utils'; import type { GlobalSentryValues } from './injectGlobalValues'; import { VIRTUAL_GLOBAL_VALUES_FILE, getGlobalValueInjectionCode } from './injectGlobalValues'; import { getAdapterOutputDir, getHooksFileName, loadSvelteConfig } from './svelteConfig'; diff --git a/packages/sveltekit/src/worker/cloudflare.ts b/packages/sveltekit/src/worker/cloudflare.ts new file mode 100644 index 000000000000..0d26c566ea10 --- /dev/null +++ b/packages/sveltekit/src/worker/cloudflare.ts @@ -0,0 +1,43 @@ +import { type CloudflareOptions, wrapRequestHandler } from '@sentry/cloudflare'; +import { getDefaultIntegrations as getDefaultCloudflareIntegrations } from '@sentry/cloudflare'; +import type { Handle } from '@sveltejs/kit'; + +import { addNonEnumerableProperty } from '@sentry/core'; +import { rewriteFramesIntegration } from '../server-common/rewriteFramesIntegration'; + +/** + * Initializes Sentry SvelteKit Cloudflare SDK + * This should be before the sentryHandle() call. + * + * In the Node export, this is a stub that does nothing. + */ +export function initCloudflareSentryHandle(options: CloudflareOptions): Handle { + const opts: CloudflareOptions = { + defaultIntegrations: [...getDefaultCloudflareIntegrations(options), rewriteFramesIntegration()], + ...options, + }; + + const handleInitSentry: Handle = ({ event, resolve }) => { + // if event.platform exists (should be there in a cloudflare worker), then do the cloudflare sentry init + if (event.platform) { + // This is an optional local that the `sentryHandle` handler checks for to avoid double isolation + // In Cloudflare the `wrapRequestHandler` function already takes care of + // - request isolation + // - trace continuation + // -setting the request onto the scope + addNonEnumerableProperty(event.locals, '_sentrySkipRequestIsolation', true); + return wrapRequestHandler( + { + options: opts, + request: event.request, + // @ts-expect-error This will exist in Cloudflare + context: event.platform.context, + }, + () => resolve(event), + ); + } + return resolve(event); + }; + + return handleInitSentry; +} diff --git a/packages/sveltekit/src/worker/index.ts b/packages/sveltekit/src/worker/index.ts new file mode 100644 index 000000000000..a74989b7d28e --- /dev/null +++ b/packages/sveltekit/src/worker/index.ts @@ -0,0 +1,90 @@ +// For use in cloudflare workers and other edge environments +// +// These are essentially the same as the node server exports, but using imports from @sentry/core +// instead of @sentry/node. +// +// This is expected to be used together with something like the @sentry/cloudflare package, to initialize Sentry +// in the worker. +// +// ------------------------- +// SvelteKit SDK exports: +export { handleErrorWithSentry } from '../server-common/handleError'; +export { wrapLoadWithSentry, wrapServerLoadWithSentry } from '../server-common/load'; +export { sentryHandle } from '../server-common/handle'; +export { initCloudflareSentryHandle } from './cloudflare'; +export { wrapServerRouteWithSentry } from '../server-common/serverRoute'; + +// Re-export some functions from Cloudflare SDK +export { + addBreadcrumb, + addEventProcessor, + addIntegration, + captureCheckIn, + captureConsoleIntegration, + captureEvent, + captureException, + captureFeedback, + captureMessage, + close, + continueTrace, + createTransport, + dedupeIntegration, + extraErrorDataIntegration, + flush, + functionToStringIntegration, + getActiveSpan, + getClient, + getCurrentScope, + getDefaultIntegrations, + getGlobalScope, + getIsolationScope, + getRootSpan, + getSpanDescendants, + getSpanStatusFromHttpCode, + getTraceData, + getTraceMetaTags, + inboundFiltersIntegration, + isInitialized, + lastEventId, + linkedErrorsIntegration, + requestDataIntegration, + rewriteFramesIntegration, + Scope, + SDK_VERSION, + SEMANTIC_ATTRIBUTE_SENTRY_OP, + SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, + SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE, + SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, + setContext, + setCurrentClient, + setExtra, + setExtras, + setHttpStatus, + setMeasurement, + setTag, + setTags, + setUser, + spanToBaggageHeader, + spanToJSON, + spanToTraceHeader, + startInactiveSpan, + startNewTrace, + suppressTracing, + startSpan, + startSpanManual, + trpcMiddleware, + withActiveSpan, + withIsolationScope, + withMonitor, + withScope, + zodErrorsIntegration, +} from '@sentry/cloudflare'; + +/** + * Tracks the Svelte component's initialization and mounting operation as well as + * updates and records them as spans. These spans are only recorded on the client-side. + * Sever-side, during SSR, this function will not record any spans. + */ +export function trackComponent(_options?: unknown): void { + // no-op on the server side +} diff --git a/packages/sveltekit/test/server/handle.test.ts b/packages/sveltekit/test/server/handle.test.ts index b2adb50d91b8..9c6e2b71d330 100644 --- a/packages/sveltekit/test/server/handle.test.ts +++ b/packages/sveltekit/test/server/handle.test.ts @@ -8,16 +8,17 @@ import { spanToJSON, } from '@sentry/core'; import type { EventEnvelopeHeaders, Span } from '@sentry/core'; +import * as SentryCore from '@sentry/core'; import { NodeClient, setCurrentClient } from '@sentry/node'; -import * as SentryNode from '@sentry/node'; import type { Handle } from '@sveltejs/kit'; import { redirect } from '@sveltejs/kit'; import { vi } from 'vitest'; -import { FETCH_PROXY_SCRIPT, addSentryCodeToPage, isFetchProxyRequired, sentryHandle } from '../../src/server/handle'; +import { FETCH_PROXY_SCRIPT, addSentryCodeToPage, isFetchProxyRequired } from '../../src/server-common/handle'; +import { sentryHandle } from '../../src/server-common/handle'; import { getDefaultNodeClientOptions } from '../utils'; -const mockCaptureException = vi.spyOn(SentryNode, 'captureException').mockImplementation(() => 'xx'); +const mockCaptureException = vi.spyOn(SentryCore, 'captureException').mockImplementation(() => 'xx'); function mockEvent(override: Record = {}): Parameters[0]['event'] { const event: Parameters[0]['event'] = { @@ -98,6 +99,7 @@ beforeEach(() => { client.init(); mockCaptureException.mockClear(); + vi.clearAllMocks(); }); describe('sentryHandle', () => { @@ -366,6 +368,23 @@ describe('sentryHandle', () => { expect(_span!).toBeDefined(); }); + + it("doesn't create an isolation scope when the `_sentrySkipRequestIsolation` local is set", async () => { + const withIsolationScopeSpy = vi.spyOn(SentryCore, 'withIsolationScope'); + const continueTraceSpy = vi.spyOn(SentryCore, 'continueTrace'); + + try { + await sentryHandle({ handleUnknownRoutes: true })({ + event: { ...mockEvent({ route: undefined }), locals: { _sentrySkipRequestIsolation: true } }, + resolve: resolve(type, isError), + }); + } catch { + // + } + + expect(withIsolationScopeSpy).not.toHaveBeenCalled(); + expect(continueTraceSpy).not.toHaveBeenCalled(); + }); }); }); @@ -394,7 +413,7 @@ describe('addSentryCodeToPage', () => { it('adds meta tags and the fetch proxy script if there is an active transaction', () => { const transformPageChunk = addSentryCodeToPage({ injectFetchProxyScript: true }); - SentryNode.startSpan({ name: 'test' }, () => { + SentryCore.startSpan({ name: 'test' }, () => { const transformed = transformPageChunk({ html, done: true }) as string; expect(transformed).toContain(' 'xx'); +const mockCaptureException = vi.spyOn(SentryCore, 'captureException').mockImplementation(() => 'xx'); const captureExceptionEventHint = { mechanism: { handled: false, type: 'sveltekit' }, diff --git a/packages/sveltekit/test/server/load.test.ts b/packages/sveltekit/test/server/load.test.ts index 1001d8464ad4..8530208347a4 100644 --- a/packages/sveltekit/test/server/load.test.ts +++ b/packages/sveltekit/test/server/load.test.ts @@ -6,15 +6,15 @@ import { SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, } from '@sentry/core'; import type { Event } from '@sentry/core'; +import * as SentryCore from '@sentry/core'; import { NodeClient, getCurrentScope, getIsolationScope, setCurrentClient } from '@sentry/node'; -import * as SentryNode from '@sentry/node'; import type { Load, ServerLoad } from '@sveltejs/kit'; import { error, redirect } from '@sveltejs/kit'; -import { wrapLoadWithSentry, wrapServerLoadWithSentry } from '../../src/server/load'; +import { wrapLoadWithSentry, wrapServerLoadWithSentry } from '../../src/server-common/load'; import { getDefaultNodeClientOptions } from '../utils'; -const mockCaptureException = vi.spyOn(SentryNode, 'captureException').mockImplementation(() => 'xx'); +const mockCaptureException = vi.spyOn(SentryCore, 'captureException').mockImplementation(() => 'xx'); const mockStartSpan = vi.fn(); diff --git a/packages/sveltekit/test/server/rewriteFramesIntegration.test.ts b/packages/sveltekit/test/server/rewriteFramesIntegration.test.ts index 3dfd5d3e460e..1d5ca8d4d695 100644 --- a/packages/sveltekit/test/server/rewriteFramesIntegration.test.ts +++ b/packages/sveltekit/test/server/rewriteFramesIntegration.test.ts @@ -2,7 +2,7 @@ import { rewriteFramesIntegration } from '@sentry/browser'; import { basename } from '@sentry/core'; import type { Event, StackFrame } from '@sentry/core'; -import { rewriteFramesIteratee } from '../../src/server/rewriteFramesIntegration'; +import { rewriteFramesIteratee } from '../../src/server-common/rewriteFramesIntegration'; import type { GlobalWithSentryValues } from '../../src/vite/injectGlobalValues'; describe('rewriteFramesIteratee', () => { diff --git a/packages/sveltekit/test/server/serverRoute.test.ts b/packages/sveltekit/test/server/serverRoute.test.ts index de99db5a548e..046c3673a8c7 100644 --- a/packages/sveltekit/test/server/serverRoute.test.ts +++ b/packages/sveltekit/test/server/serverRoute.test.ts @@ -1,4 +1,4 @@ -import * as SentryNode from '@sentry/node'; +import * as SentryCore from '@sentry/core'; import type { NumericRange } from '@sveltejs/kit'; import { type RequestEvent, error, redirect } from '@sveltejs/kit'; import { beforeEach, describe, expect, it, vi } from 'vitest'; @@ -26,7 +26,7 @@ describe('wrapServerRouteWithSentry', () => { }); describe('wraps a server route span around the original server route handler', () => { - const startSpanSpy = vi.spyOn(SentryNode, 'startSpan'); + const startSpanSpy = vi.spyOn(SentryCore, 'startSpan'); it('assigns the route id as name if available', () => { const wrappedRouteHandler = wrapServerRouteWithSentry(originalRouteHandler); @@ -71,7 +71,7 @@ describe('wrapServerRouteWithSentry', () => { }); }); - const captureExceptionSpy = vi.spyOn(SentryNode, 'captureException'); + const captureExceptionSpy = vi.spyOn(SentryCore, 'captureException'); describe('captures server route errors', () => { it('captures and rethrows normal server route error', async () => { const error = new Error('Server Route Error'); diff --git a/packages/sveltekit/test/server/utils.test.ts b/packages/sveltekit/test/server/utils.test.ts index 5e8b9b2b99a3..53e588d683ec 100644 --- a/packages/sveltekit/test/server/utils.test.ts +++ b/packages/sveltekit/test/server/utils.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from 'vitest'; -import { getTracePropagationData } from '../../src/server/utils'; +import { getTracePropagationData } from '../../src/server-common/utils'; const MOCK_REQUEST_EVENT: any = { request: { From 08569e68235c5b16ff0ab48f135b546a262a3c20 Mon Sep 17 00:00:00 2001 From: Abhijeet Prasad Date: Fri, 21 Feb 2025 11:00:17 -0500 Subject: [PATCH 27/32] Revert "test(node): Add `pg-native` integration tests" (#15464) Reverts getsentry/sentry-javascript#15206 This PR requires `libpq` for `yarn install` to be complete, so going to revert this. Let's put this requirement into the docker image instead. --- .github/workflows/build.yml | 4 - .../node-integration-tests/package.json | 4 +- .../tracing/postgres/scenario-native.js | 47 ------ .../suites/tracing/postgres/test.ts | 50 ------ yarn.lock | 145 +++++------------- 5 files changed, 42 insertions(+), 208 deletions(-) delete mode 100644 dev-packages/node-integration-tests/suites/tracing/postgres/scenario-native.js diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f7341976d043..aae090f76188 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -713,10 +713,6 @@ jobs: with: dependency_cache_key: ${{ needs.job_build.outputs.dependency_cache_key }} - - name: Build `libpq` - run: yarn libpq:build - working-directory: dev-packages/node-integration-tests - - name: Overwrite typescript version if: matrix.typescript == '3.8' run: node ./scripts/use-ts-3_8.js diff --git a/dev-packages/node-integration-tests/package.json b/dev-packages/node-integration-tests/package.json index 08ede11390e7..bbb7e300ecee 100644 --- a/dev-packages/node-integration-tests/package.json +++ b/dev-packages/node-integration-tests/package.json @@ -16,7 +16,6 @@ "build:types": "tsc -p tsconfig.types.json", "clean": "rimraf -g **/node_modules && run-p clean:script", "clean:script": "node scripts/clean.js", - "libpq:build": "npm rebuild libpq", "express-v5-install": "cd suites/express-v5 && yarn --no-lockfile", "lint": "eslint . --format stylish", "fix": "eslint . --format stylish --fix", @@ -63,8 +62,7 @@ "nock": "^13.5.5", "node-cron": "^3.0.3", "node-schedule": "^2.1.1", - "pg": "^8.13.1", - "pg-native": "3.2.0", + "pg": "^8.7.3", "proxy": "^2.1.1", "redis-4": "npm:redis@^4.6.14", "reflect-metadata": "0.2.1", diff --git a/dev-packages/node-integration-tests/suites/tracing/postgres/scenario-native.js b/dev-packages/node-integration-tests/suites/tracing/postgres/scenario-native.js deleted file mode 100644 index ec4768217c42..000000000000 --- a/dev-packages/node-integration-tests/suites/tracing/postgres/scenario-native.js +++ /dev/null @@ -1,47 +0,0 @@ -const { loggingTransport } = require('@sentry-internal/node-integration-tests'); -const Sentry = require('@sentry/node'); - -Sentry.init({ - dsn: 'https://public@dsn.ingest.sentry.io/1337', - release: '1.0', - tracesSampleRate: 1.0, - transport: loggingTransport, -}); - -// Stop the process from exiting before the transaction is sent -setInterval(() => {}, 1000); - -const { native } = require('pg'); -const { Client } = native; - -const client = new Client({ port: 5444, user: 'test', password: 'test', database: 'tests' }); - -async function run() { - await Sentry.startSpan( - { - name: 'Test Transaction', - op: 'transaction', - }, - async () => { - try { - await client.connect(); - - await client - .query( - 'CREATE TABLE "NativeUser" ("id" SERIAL NOT NULL,"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,"email" TEXT NOT NULL,"name" TEXT,CONSTRAINT "User_pkey" PRIMARY KEY ("id"));', - ) - .catch(() => { - // if this is not a fresh database, the table might already exist - }); - - await client.query('INSERT INTO "NativeUser" ("email", "name") VALUES ($1, $2)', ['tim', 'tim@domain.com']); - await client.query('SELECT * FROM "NativeUser"'); - } finally { - await client.end(); - } - }, - ); -} - -// eslint-disable-next-line @typescript-eslint/no-floating-promises -run(); diff --git a/dev-packages/node-integration-tests/suites/tracing/postgres/test.ts b/dev-packages/node-integration-tests/suites/tracing/postgres/test.ts index 9f8ba1449784..f2549c70eb90 100644 --- a/dev-packages/node-integration-tests/suites/tracing/postgres/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing/postgres/test.ts @@ -53,54 +53,4 @@ describe('postgres auto instrumentation', () => { .expect({ transaction: EXPECTED_TRANSACTION }) .start(done); }); - - test('should auto-instrument `pg-native` package', done => { - const EXPECTED_TRANSACTION = { - transaction: 'Test Transaction', - spans: expect.arrayContaining([ - expect.objectContaining({ - data: expect.objectContaining({ - 'db.system': 'postgresql', - 'db.name': 'tests', - 'sentry.origin': 'manual', - 'sentry.op': 'db', - }), - description: 'pg.connect', - op: 'db', - status: 'ok', - }), - expect.objectContaining({ - data: expect.objectContaining({ - 'db.system': 'postgresql', - 'db.name': 'tests', - 'db.statement': 'INSERT INTO "NativeUser" ("email", "name") VALUES ($1, $2)', - 'sentry.origin': 'auto.db.otel.postgres', - 'sentry.op': 'db', - }), - description: 'INSERT INTO "NativeUser" ("email", "name") VALUES ($1, $2)', - op: 'db', - status: 'ok', - origin: 'auto.db.otel.postgres', - }), - expect.objectContaining({ - data: expect.objectContaining({ - 'db.system': 'postgresql', - 'db.name': 'tests', - 'db.statement': 'SELECT * FROM "NativeUser"', - 'sentry.origin': 'auto.db.otel.postgres', - 'sentry.op': 'db', - }), - description: 'SELECT * FROM "NativeUser"', - op: 'db', - status: 'ok', - origin: 'auto.db.otel.postgres', - }), - ]), - }; - - createRunner(__dirname, 'scenario-native.js') - .withDockerCompose({ workingDirectory: [__dirname], readyMatches: ['port 5432'] }) - .expect({ transaction: EXPECTED_TRANSACTION }) - .start(done); - }); }); diff --git a/yarn.lock b/yarn.lock index 3243b6980af1..0c7b60c816e1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10688,7 +10688,7 @@ binary@^0.3.0: resolved "https://registry.yarnpkg.com/binaryextensions/-/binaryextensions-2.3.0.tgz#1d269cbf7e6243ea886aa41453c3651ccbe13c22" integrity sha512-nAihlQsYGyc5Bwq6+EsubvANYGExeJKHDO3RjnvwU042fawQTQfM3Kxn7IHUXQOz4bzfwsGYYHGSvXyW4zOGLg== -bindings@1.5.0, bindings@^1.4.0: +bindings@^1.4.0: version "1.5.0" resolved "https://registry.yarnpkg.com/bindings/-/bindings-1.5.0.tgz#10353c9e945334bc0511a6d90b38fbc7c9c504df" integrity sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ== @@ -11400,6 +11400,11 @@ buffer-more-ints@~1.0.0: resolved "https://registry.yarnpkg.com/buffer-more-ints/-/buffer-more-ints-1.0.0.tgz#ef4f8e2dddbad429ed3828a9c55d44f05c611422" integrity sha512-EMetuGFz5SLsT0QTnXzINh4Ksr+oo4i+UGTXEshiGCQWnsgSs7ZhJ8fzlwQ+OzEMs0MpDAMr1hxnblp5a4vcHg== +buffer-writer@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/buffer-writer/-/buffer-writer-2.0.0.tgz#ce7eb81a38f7829db09c873f2fbb792c0c98ec04" + integrity sha512-a7ZpuTZU1TRtnwyCNW3I5dc0wWNC3VR9S++Ewyk2HHZdrO3CQJqSpd+95Us590V6AL7JqUAH2IwZ/398PmNFgw== + buffer@^5.5.0, buffer@^5.6.0: version "5.7.1" resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.7.1.tgz#ba62e7c13133053582197160851a8f648e99eed0" @@ -15598,6 +15603,11 @@ expect@^27.5.1: jest-matcher-utils "^27.5.1" jest-message-util "^27.5.1" +exponential-backoff@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/exponential-backoff/-/exponential-backoff-3.1.1.tgz#64ac7526fe341ab18a39016cd22c787d01e00bf6" + integrity sha512-dX7e/LHVJ6W3DE1MHWi9S1EYzDESENfLrYohG2G++ovZrYOkm4Knwa0mc1cn84xJOR4KEU0WSchhLbd0UklbHw== + express@4.21.1, express@^4.10.7, express@^4.16.4, express@^4.17.1, express@^4.17.3, express@^4.18.1, express@^4.21.1: version "4.21.1" resolved "https://registry.yarnpkg.com/express/-/express-4.21.1.tgz#9dae5dda832f16b4eec941a4e44aa89ec481b281" @@ -19951,14 +19961,6 @@ libnpmpublish@7.3.0: sigstore "^1.4.0" ssri "^10.0.1" -libpq@1.8.13: - version "1.8.13" - resolved "https://registry.yarnpkg.com/libpq/-/libpq-1.8.13.tgz#d48af53c88defa7a20f958ef51bbbc0f58747355" - integrity sha512-t1wpnGVgwRIFSKoe4RFUllAFj953kNMcdXhGvFJwI0r6lJQqgSwTeiIciaCinjOmHk0HnFeWQSMC6Uw2591G4A== - dependencies: - bindings "1.5.0" - nan "2.19.0" - license-webpack-plugin@4.0.2: version "4.0.2" resolved "https://registry.yarnpkg.com/license-webpack-plugin/-/license-webpack-plugin-4.0.2.tgz#1e18442ed20b754b82f1adeff42249b81d11aec6" @@ -20468,12 +20470,7 @@ lru-cache@6.0.0, lru-cache@^6.0.0: dependencies: yallist "^4.0.0" -lru-cache@^10.2.0: - version "10.2.2" - resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-10.2.2.tgz#48206bc114c1252940c41b25b41af5b545aca878" - integrity sha512-9hp3Vp2/hFQUiIwKo8XCeFVnrg8Pk3TYNPIR7tJADKi5YfcF7vEaK7avFHTlSy3kOKYaJQaalfEo6YuXdceBOQ== - -lru-cache@^10.4.3: +lru-cache@^10.2.0, lru-cache@^10.4.3: version "10.4.3" resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-10.4.3.tgz#410fc8a17b70e598013df257c2446b7f3383f119" integrity sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ== @@ -21614,12 +21611,7 @@ minipass@^5.0.0: resolved "https://registry.yarnpkg.com/minipass/-/minipass-5.0.0.tgz#3e9788ffb90b694a5d0ec94479a45b5d8738133d" integrity sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ== -"minipass@^5.0.0 || ^6.0.2 || ^7.0.0": - version "7.0.3" - resolved "https://registry.yarnpkg.com/minipass/-/minipass-7.0.3.tgz#05ea638da44e475037ed94d1c7efcc76a25e1974" - integrity sha512-LhbbwCfz3vsb12j/WkWQPZfKTsgqIe1Nf/ti1pKjYESGLHIVjWU96G9/ljLH4F9mWNVhlQOm0VySdAWzf05dpg== - -minipass@^7.1.2: +"minipass@^5.0.0 || ^6.0.2 || ^7.0.0", minipass@^7.1.2: version "7.1.2" resolved "https://registry.yarnpkg.com/minipass/-/minipass-7.1.2.tgz#93a9626ce5e5e66bd4db86849e7515e92340a707" integrity sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw== @@ -21969,11 +21961,6 @@ named-placeholders@^1.1.3: dependencies: lru-cache "^7.14.1" -nan@2.19.0: - version "2.19.0" - resolved "https://registry.yarnpkg.com/nan/-/nan-2.19.0.tgz#bb58122ad55a6c5bc973303908d5b16cfdd5a8c0" - integrity sha512-nO1xXxfh/RWNxfd/XPfbIfFk5vgLsAxUR9y5O0cHMJu/AW9U95JLXqthYHjEp+8gQ5p96K9jUp8nbVOxCdRbtw== - nanoid@^3.3.3, nanoid@^3.3.4, nanoid@^3.3.6, nanoid@^3.3.7: version "3.3.7" resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.7.tgz#d0c301a691bc8d54efa0a2226ccf3fe2fd656bd8" @@ -22317,22 +22304,18 @@ node-forge@^1, node-forge@^1.3.1: resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-1.3.1.tgz#be8da2af243b2417d5f646a770663a92b7e9ded3" integrity sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA== -node-gyp-build@^4.2.2: +node-gyp-build@^4.2.2, node-gyp-build@^4.3.0: version "4.6.0" resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-4.6.0.tgz#0c52e4cbf54bbd28b709820ef7b6a3c2d6209055" integrity sha512-NTZVKn9IylLwUzaKjkas1e4u2DLNcV4rdYagA4PWdPwW87Bi7z+BznyKSRwS/761tV/lzCGXplWsiaMjLqP2zQ== -node-gyp-build@^4.3.0: - version "4.5.0" - resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-4.5.0.tgz#7a64eefa0b21112f89f58379da128ac177f20e40" - integrity sha512-2iGbaQBV+ITgCz76ZEjmhUKAKVf7xfY1sRl4UiKQspfZMH2h06SyhNsnSVy50cwkFQDGLyif6m/6uFXHkOZ6rg== - node-gyp@^9.0.0: - version "9.3.0" - resolved "https://registry.yarnpkg.com/node-gyp/-/node-gyp-9.3.0.tgz#f8eefe77f0ad8edb3b3b898409b53e697642b319" - integrity sha512-A6rJWfXFz7TQNjpldJ915WFb1LnhO4lIve3ANPbWreuEoLoKlFT3sxIepPBkLhM27crW8YmN+pjlgbasH6cH/Q== + version "9.4.1" + resolved "https://registry.yarnpkg.com/node-gyp/-/node-gyp-9.4.1.tgz#8a1023e0d6766ecb52764cc3a734b36ff275e185" + integrity sha512-OQkWKbjQKbGkMf/xqI1jjy3oCTgMKJac58G2+bjZb3fza6gW2YrCSdMQYaoTb70crvE//Gngr4f0AgVHmqHvBQ== dependencies: env-paths "^2.2.0" + exponential-backoff "^3.1.1" glob "^7.1.4" graceful-fs "^4.2.6" make-fetch-happen "^10.0.3" @@ -23443,6 +23426,11 @@ package-name-regex@~2.0.6: resolved "https://registry.yarnpkg.com/package-name-regex/-/package-name-regex-2.0.6.tgz#b54bcb04d950e38082b7bb38fa558e01c1679334" integrity sha512-gFL35q7kbE/zBaPA3UKhp2vSzcPYx2ecbYuwv1ucE9Il6IIgBDweBlH8D68UFGZic2MkllKa2KHCfC1IQBQUYA== +packet-reader@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/packet-reader/-/packet-reader-1.0.0.tgz#9238e5480dedabacfe1fe3f2771063f164157d74" + integrity sha512-HAKu/fG3HpHFO0AA8WE8q2g+gBJaZ9MG7fcKk+IJPLTGAD6Psw4443l+9DGRbOIh3/aXr7Phy0TjilYivJo5XQ== + pacote@13.6.2: version "13.6.2" resolved "https://registry.yarnpkg.com/pacote/-/pacote-13.6.2.tgz#0d444ba3618ab3e5cd330b451c22967bbd0ca48a" @@ -23792,65 +23780,31 @@ periscopic@^3.1.0: estree-walker "^3.0.0" is-reference "^3.0.0" -pg-cloudflare@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/pg-cloudflare/-/pg-cloudflare-1.1.1.tgz#e6d5833015b170e23ae819e8c5d7eaedb472ca98" - integrity sha512-xWPagP/4B6BgFO+EKz3JONXv3YDgvkbVrGw2mTo3D6tVDQRh1e7cqVGvyR3BE+eQgAvx1XhW/iEASj4/jCWl3Q== - -pg-connection-string@2.6.1: +pg-connection-string@2.6.1, pg-connection-string@^2.5.0: version "2.6.1" resolved "https://registry.yarnpkg.com/pg-connection-string/-/pg-connection-string-2.6.1.tgz#78c23c21a35dd116f48e12e23c0965e8d9e2cbfb" integrity sha512-w6ZzNu6oMmIzEAYVw+RLK0+nqHPt8K3ZnknKi+g48Ak2pr3dtljJW3o+D/n2zzCG07Zoe9VOX3aiKpj+BN0pjg== -pg-connection-string@^2.7.0: - version "2.7.0" - resolved "https://registry.yarnpkg.com/pg-connection-string/-/pg-connection-string-2.7.0.tgz#f1d3489e427c62ece022dba98d5262efcb168b37" - integrity sha512-PI2W9mv53rXJQEOb8xNR8lH7Hr+EKa6oJa38zsK0S/ky2er16ios1wLKhZyxzD7jUReiWokc9WK5nxSnC7W1TA== - pg-int8@1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/pg-int8/-/pg-int8-1.0.1.tgz#943bd463bf5b71b4170115f80f8efc9a0c0eb78c" integrity sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw== -pg-native@3.2.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/pg-native/-/pg-native-3.2.0.tgz#1183a549c00741040f1f47f9167a6bf378206826" - integrity sha512-9q9I6RmT285DiRc0xkYb8e+bwOIIbnfVLddnzzXW35K1sZc74dR+symo2oeuzSW/sDQ8n24gWAvlGWK/GDJ3+Q== - dependencies: - libpq "1.8.13" - pg-types "^1.12.1" - pg-numeric@1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/pg-numeric/-/pg-numeric-1.0.2.tgz#816d9a44026086ae8ae74839acd6a09b0636aa3a" integrity sha512-BM/Thnrw5jm2kKLE5uJkXqqExRUY/toLHda65XgFTBTFYZyopbKjBe29Ii3RbkvlsMoFwD+tHeGaCjjv0gHlyw== -pg-pool@^3.7.0: - version "3.7.0" - resolved "https://registry.yarnpkg.com/pg-pool/-/pg-pool-3.7.0.tgz#d4d3c7ad640f8c6a2245adc369bafde4ebb8cbec" - integrity sha512-ZOBQForurqh4zZWjrgSwwAtzJ7QiRX0ovFkZr2klsen3Nm0aoh33Ls0fzfv3imeH/nw/O27cjdz5kzYJfeGp/g== +pg-pool@^3.5.1: + version "3.5.1" + resolved "https://registry.yarnpkg.com/pg-pool/-/pg-pool-3.5.1.tgz#f499ce76f9bf5097488b3b83b19861f28e4ed905" + integrity sha512-6iCR0wVrro6OOHFsyavV+i6KYL4lVNyYAB9RD18w66xSzN+d8b66HiwuP30Gp1SH5O9T82fckkzsRjlrhD0ioQ== -pg-protocol@*: +pg-protocol@*, pg-protocol@^1.5.0: version "1.5.0" resolved "https://registry.yarnpkg.com/pg-protocol/-/pg-protocol-1.5.0.tgz#b5dd452257314565e2d54ab3c132adc46565a6a0" integrity sha512-muRttij7H8TqRNu/DxrAJQITO4Ac7RmX3Klyr/9mJEOBeIpgnF8f9jAfRz5d3XwQZl5qBjF9gLsUtMPJE0vezQ== -pg-protocol@^1.7.0: - version "1.7.0" - resolved "https://registry.yarnpkg.com/pg-protocol/-/pg-protocol-1.7.0.tgz#ec037c87c20515372692edac8b63cf4405448a93" - integrity sha512-hTK/mE36i8fDDhgDFjy6xNOG+LCorxLG3WO17tku+ij6sVHXh1jQUJ8hYAnRhNla4QVD2H8er/FOjc/+EgC6yQ== - -pg-types@^1.12.1: - version "1.13.0" - resolved "https://registry.yarnpkg.com/pg-types/-/pg-types-1.13.0.tgz#75f490b8a8abf75f1386ef5ec4455ecf6b345c63" - integrity sha512-lfKli0Gkl/+za/+b6lzENajczwZHc7D5kiUCZfgm914jipD2kIOIvEkAhZ8GrW3/TUoP9w8FHjwpPObBye5KQQ== - dependencies: - pg-int8 "1.0.1" - postgres-array "~1.0.0" - postgres-bytea "~1.0.0" - postgres-date "~1.0.0" - postgres-interval "^1.1.0" - pg-types@^2.1.0, pg-types@^2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/pg-types/-/pg-types-2.2.0.tgz#2d0250d636454f7cfa3b6ae0382fdfa8063254a3" @@ -23875,18 +23829,18 @@ pg-types@^4.0.1: postgres-interval "^3.0.0" postgres-range "^1.1.1" -pg@^8.13.1: - version "8.13.1" - resolved "https://registry.yarnpkg.com/pg/-/pg-8.13.1.tgz#6498d8b0a87ff76c2df7a32160309d3168c0c080" - integrity sha512-OUir1A0rPNZlX//c7ksiu7crsGZTKSOXJPgtNiHGIlC9H0lO+NC6ZDYksSgBYY/thSWhnSRBv8w1lieNNGATNQ== - dependencies: - pg-connection-string "^2.7.0" - pg-pool "^3.7.0" - pg-protocol "^1.7.0" +pg@^8.7.3: + version "8.7.3" + resolved "https://registry.yarnpkg.com/pg/-/pg-8.7.3.tgz#8a5bdd664ca4fda4db7997ec634c6e5455b27c44" + integrity sha512-HPmH4GH4H3AOprDJOazoIcpI49XFsHCe8xlrjHkWiapdbHK+HLtbm/GQzXYAZwmPju/kzKhjaSfMACG+8cgJcw== + dependencies: + buffer-writer "2.0.0" + packet-reader "1.0.0" + pg-connection-string "^2.5.0" + pg-pool "^3.5.1" + pg-protocol "^1.5.0" pg-types "^2.1.0" pgpass "1.x" - optionalDependencies: - pg-cloudflare "^1.1.1" pgpass@1.x: version "1.0.5" @@ -24647,11 +24601,6 @@ postcss@^8.1.10, postcss@^8.2.14, postcss@^8.2.15, postcss@^8.3.7, postcss@^8.4. picocolors "^1.1.1" source-map-js "^1.2.1" -postgres-array@~1.0.0: - version "1.0.3" - resolved "https://registry.yarnpkg.com/postgres-array/-/postgres-array-1.0.3.tgz#c561fc3b266b21451fc6555384f4986d78ec80f5" - integrity sha512-5wClXrAP0+78mcsNX3/ithQ5exKvCyK5lr5NEEEeGwwM6NJdQgzIJBVxLvRW+huFpX92F2QnZ5CcokH0VhK2qQ== - postgres-array@~2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/postgres-array/-/postgres-array-2.0.0.tgz#48f8fce054fbc69671999329b8834b772652d82e" @@ -24674,7 +24623,7 @@ postgres-bytea@~3.0.0: dependencies: obuf "~1.1.2" -postgres-date@~1.0.0, postgres-date@~1.0.4: +postgres-date@~1.0.4: version "1.0.7" resolved "https://registry.yarnpkg.com/postgres-date/-/postgres-date-1.0.7.tgz#51bc086006005e5061c591cee727f2531bf641a8" integrity sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q== @@ -28031,19 +27980,7 @@ tar@6.1.11: mkdirp "^1.0.3" yallist "^4.0.0" -tar@^6.1.11, tar@^6.1.2: - version "6.1.12" - resolved "https://registry.yarnpkg.com/tar/-/tar-6.1.12.tgz#3b742fb05669b55671fb769ab67a7791ea1a62e6" - integrity sha512-jU4TdemS31uABHd+Lt5WEYJuzn+TJTCBLljvIAHZOz6M9Os5pJ4dD+vRFLxPa/n3T0iEFzpi+0x1UfuDZYbRMw== - dependencies: - chownr "^2.0.0" - fs-minipass "^2.0.0" - minipass "^3.0.0" - minizlib "^2.1.1" - mkdirp "^1.0.3" - yallist "^4.0.0" - -tar@^6.2.0: +tar@^6.1.11, tar@^6.1.2, tar@^6.2.0: version "6.2.1" resolved "https://registry.yarnpkg.com/tar/-/tar-6.2.1.tgz#717549c541bc3c2af15751bea94b1dd068d4b03a" integrity sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A== From bac7387dc64a1de0b7674299f11d0cce23cc976c Mon Sep 17 00:00:00 2001 From: Luca Forstner Date: Mon, 24 Feb 2025 11:04:00 +0100 Subject: [PATCH 28/32] feat(nextjs): Add experimental flag to not strip origin information from different origin stack frames (#15418) --- .size-limit.js | 2 +- .../client/clientNormalizationIntegration.ts | 86 +++++++++++++++---- packages/nextjs/src/client/index.ts | 21 ++++- packages/nextjs/src/config/types.ts | 9 ++ packages/nextjs/src/config/webpack.ts | 4 + .../nextjs/src/config/withSentryConfig.ts | 16 ++++ 6 files changed, 118 insertions(+), 20 deletions(-) diff --git a/.size-limit.js b/.size-limit.js index 157c1243021e..08adf5a80c29 100644 --- a/.size-limit.js +++ b/.size-limit.js @@ -210,7 +210,7 @@ module.exports = [ import: createImport('init'), ignore: ['next/router', 'next/constants'], gzip: true, - limit: '40 KB', + limit: '41 KB', }, // SvelteKit SDK (ESM) { diff --git a/packages/nextjs/src/client/clientNormalizationIntegration.ts b/packages/nextjs/src/client/clientNormalizationIntegration.ts index e4bbb4881bc3..a7cd2c356f4e 100644 --- a/packages/nextjs/src/client/clientNormalizationIntegration.ts +++ b/packages/nextjs/src/client/clientNormalizationIntegration.ts @@ -2,30 +2,84 @@ import { rewriteFramesIntegration } from '@sentry/browser'; import { defineIntegration } from '@sentry/core'; export const nextjsClientStackFrameNormalizationIntegration = defineIntegration( - ({ assetPrefixPath }: { assetPrefixPath: string }) => { + ({ + assetPrefix, + basePath, + rewriteFramesAssetPrefixPath, + experimentalThirdPartyOriginStackFrames, + }: { + assetPrefix?: string; + basePath?: string; + rewriteFramesAssetPrefixPath: string; + experimentalThirdPartyOriginStackFrames: boolean; + }) => { const rewriteFramesInstance = rewriteFramesIntegration({ // Turn `//_next/static/...` into `app:///_next/static/...` iteratee: frame => { - try { - const { origin } = new URL(frame.filename as string); - frame.filename = frame.filename?.replace(origin, 'app://').replace(assetPrefixPath, ''); - } catch (err) { - // Filename wasn't a properly formed URL, so there's nothing we can do + if (experimentalThirdPartyOriginStackFrames) { + // Not sure why but access to global WINDOW from @sentry/Browser causes hideous ci errors + // eslint-disable-next-line no-restricted-globals + const windowOrigin = typeof window !== 'undefined' && window.location ? window.location.origin : ''; + // A filename starting with the local origin and not ending with JS is most likely JS in HTML which we do not want to rewrite + if (frame.filename?.startsWith(windowOrigin) && !frame.filename.endsWith('.js')) { + return frame; + } + + if (assetPrefix) { + // If the user defined an asset prefix, we need to strip it so that we can match it with uploaded sourcemaps. + // assetPrefix always takes priority over basePath. + if (frame.filename?.startsWith(assetPrefix)) { + frame.filename = frame.filename.replace(assetPrefix, 'app://'); + } + } else if (basePath) { + // If the user defined a base path, we need to strip it to match with uploaded sourcemaps. + // We should only do this for same-origin filenames though, so that third party assets are not rewritten. + try { + const { origin: frameOrigin } = new URL(frame.filename as string); + if (frameOrigin === windowOrigin) { + frame.filename = frame.filename?.replace(frameOrigin, 'app://').replace(basePath, ''); + } + } catch (err) { + // Filename wasn't a properly formed URL, so there's nothing we can do + } + } + } else { + try { + const { origin } = new URL(frame.filename as string); + frame.filename = frame.filename?.replace(origin, 'app://').replace(rewriteFramesAssetPrefixPath, ''); + } catch (err) { + // Filename wasn't a properly formed URL, so there's nothing we can do + } } // We need to URI-decode the filename because Next.js has wildcard routes like "/users/[id].js" which show up as "/users/%5id%5.js" in Error stacktraces. // The corresponding sources that Next.js generates have proper brackets so we also need proper brackets in the frame so that source map resolving works. - if (frame.filename?.startsWith('app:///_next')) { - frame.filename = decodeURI(frame.filename); - } + if (experimentalThirdPartyOriginStackFrames) { + if (frame.filename?.includes('/_next')) { + frame.filename = decodeURI(frame.filename); + } + + if ( + frame.filename?.match( + /\/_next\/static\/chunks\/(main-|main-app-|polyfills-|webpack-|framework-|framework\.)[0-9a-f]+\.js$/, + ) + ) { + // We don't care about these frames. It's Next.js internal code. + frame.in_app = false; + } + } else { + if (frame.filename?.startsWith('app:///_next')) { + frame.filename = decodeURI(frame.filename); + } - if ( - frame.filename?.match( - /^app:\/\/\/_next\/static\/chunks\/(main-|main-app-|polyfills-|webpack-|framework-|framework\.)[0-9a-f]+\.js$/, - ) - ) { - // We don't care about these frames. It's Next.js internal code. - frame.in_app = false; + if ( + frame.filename?.match( + /^app:\/\/\/_next\/static\/chunks\/(main-|main-app-|polyfills-|webpack-|framework-|framework\.)[0-9a-f]+\.js$/, + ) + ) { + // We don't care about these frames. It's Next.js internal code. + frame.in_app = false; + } } return frame; diff --git a/packages/nextjs/src/client/index.ts b/packages/nextjs/src/client/index.ts index 163e29f0b9a7..c8e6d21837fd 100644 --- a/packages/nextjs/src/client/index.ts +++ b/packages/nextjs/src/client/index.ts @@ -17,6 +17,9 @@ export { browserTracingIntegration } from './browserTracingIntegration'; const globalWithInjectedValues = GLOBAL_OBJ as typeof GLOBAL_OBJ & { _sentryRewriteFramesAssetPrefixPath: string; + _sentryAssetPrefix?: string; + _sentryBasePath?: string; + _experimentalThirdPartyOriginStackFrames?: string; }; // Treeshakable guard to remove all code related to tracing @@ -67,13 +70,25 @@ function getDefaultIntegrations(options: BrowserOptions): Integration[] { customDefaultIntegrations.push(browserTracingIntegration()); } - // This value is injected at build time, based on the output directory specified in the build config. Though a default + // These values are injected at build time, based on the output directory specified in the build config. Though a default // is set there, we set it here as well, just in case something has gone wrong with the injection. - const assetPrefixPath = + const rewriteFramesAssetPrefixPath = process.env._sentryRewriteFramesAssetPrefixPath || globalWithInjectedValues._sentryRewriteFramesAssetPrefixPath || ''; - customDefaultIntegrations.push(nextjsClientStackFrameNormalizationIntegration({ assetPrefixPath })); + const assetPrefix = process.env._sentryAssetPrefix || globalWithInjectedValues._sentryAssetPrefix; + const basePath = process.env._sentryBasePath || globalWithInjectedValues._sentryBasePath; + const experimentalThirdPartyOriginStackFrames = + process.env._experimentalThirdPartyOriginStackFrames === 'true' || + globalWithInjectedValues._experimentalThirdPartyOriginStackFrames === 'true'; + customDefaultIntegrations.push( + nextjsClientStackFrameNormalizationIntegration({ + assetPrefix, + basePath, + rewriteFramesAssetPrefixPath, + experimentalThirdPartyOriginStackFrames, + }), + ); return customDefaultIntegrations; } diff --git a/packages/nextjs/src/config/types.ts b/packages/nextjs/src/config/types.ts index a747684cc753..965233d08b76 100644 --- a/packages/nextjs/src/config/types.ts +++ b/packages/nextjs/src/config/types.ts @@ -439,6 +439,15 @@ export type SentryBuildOptions = { * Defaults to `false`. */ automaticVercelMonitors?: boolean; + + /** + * Contains a set of experimental flags that might change in future releases. These flags enable + * features that are still in development and may be modified, renamed, or removed without notice. + * Use with caution in production environments. + */ + _experimental?: Partial<{ + thirdPartyOriginStackFrames: boolean; + }>; }; export type NextConfigFunction = ( diff --git a/packages/nextjs/src/config/webpack.ts b/packages/nextjs/src/config/webpack.ts index f82bb4a0476e..5ff02da355a1 100644 --- a/packages/nextjs/src/config/webpack.ts +++ b/packages/nextjs/src/config/webpack.ts @@ -624,6 +624,10 @@ function addValueInjectionLoader( _sentryRewriteFramesAssetPrefixPath: assetPrefix ? new URL(assetPrefix, 'http://dogs.are.great').pathname.replace(/\/$/, '') : '', + _sentryAssetPrefix: userNextConfig.assetPrefix, + _sentryExperimentalThirdPartyOriginStackFrames: userSentryOptions._experimental?.thirdPartyOriginStackFrames + ? 'true' + : undefined, }; if (buildContext.isServer) { diff --git a/packages/nextjs/src/config/withSentryConfig.ts b/packages/nextjs/src/config/withSentryConfig.ts index 8f57f51a8c58..ab15d1c86c0d 100644 --- a/packages/nextjs/src/config/withSentryConfig.ts +++ b/packages/nextjs/src/config/withSentryConfig.ts @@ -268,6 +268,14 @@ function setUpBuildTimeVariables(userNextConfig: NextConfigObject, userSentryOpt : '', }; + if (userNextConfig.assetPrefix) { + buildTimeVariables._assetsPrefix = userNextConfig.assetPrefix; + } + + if (userSentryOptions._experimental?.thirdPartyOriginStackFrames) { + buildTimeVariables._experimentalThirdPartyOriginStackFrames = 'true'; + } + if (rewritesTunnelPath) { buildTimeVariables._sentryRewritesTunnelPath = rewritesTunnelPath; } @@ -276,6 +284,14 @@ function setUpBuildTimeVariables(userNextConfig: NextConfigObject, userSentryOpt buildTimeVariables._sentryBasePath = basePath; } + if (userNextConfig.assetPrefix) { + buildTimeVariables._sentryAssetPrefix = userNextConfig.assetPrefix; + } + + if (userSentryOptions._experimental?.thirdPartyOriginStackFrames) { + buildTimeVariables._experimentalThirdPartyOriginStackFrames = 'true'; + } + if (typeof userNextConfig.env === 'object') { userNextConfig.env = { ...buildTimeVariables, ...userNextConfig.env }; } else if (userNextConfig.env === undefined) { From 63ea300395f3fb9926998c4c513d2cd04d25fbbd Mon Sep 17 00:00:00 2001 From: Sigrid Huemer <32902192+s1gr1d@users.noreply.github.com> Date: Mon, 24 Feb 2025 14:26:27 +0100 Subject: [PATCH 29/32] feat(core): Add `addLink(s)` to Sentry span (#15452) part of https://github.com/getsentry/sentry-javascript/issues/14991 --- .../suites/tracing/linking-addLink/init.js | 9 ++ .../suites/tracing/linking-addLink/subject.js | 28 ++++++ .../suites/tracing/linking-addLink/test.ts | 76 +++++++++++++++ .../suites/tracing/linking-addLinks/init.js | 9 ++ .../tracing/linking-addLinks/subject.js | 35 +++++++ .../suites/tracing/linking-addLinks/test.ts | 93 +++++++++++++++++++ packages/core/src/tracing/sentrySpan.ts | 18 +++- packages/core/src/utils/spanUtils.ts | 3 +- packages/core/test/lib/tracing/trace.test.ts | 68 ++++++++++++++ packages/opentelemetry/test/trace.test.ts | 4 +- 10 files changed, 338 insertions(+), 5 deletions(-) create mode 100644 dev-packages/browser-integration-tests/suites/tracing/linking-addLink/init.js create mode 100644 dev-packages/browser-integration-tests/suites/tracing/linking-addLink/subject.js create mode 100644 dev-packages/browser-integration-tests/suites/tracing/linking-addLink/test.ts create mode 100644 dev-packages/browser-integration-tests/suites/tracing/linking-addLinks/init.js create mode 100644 dev-packages/browser-integration-tests/suites/tracing/linking-addLinks/subject.js create mode 100644 dev-packages/browser-integration-tests/suites/tracing/linking-addLinks/test.ts diff --git a/dev-packages/browser-integration-tests/suites/tracing/linking-addLink/init.js b/dev-packages/browser-integration-tests/suites/tracing/linking-addLink/init.js new file mode 100644 index 000000000000..3ec6adbbdb75 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/linking-addLink/init.js @@ -0,0 +1,9 @@ +import * as Sentry from '@sentry/browser'; + +window.Sentry = Sentry; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + integrations: [], + tracesSampleRate: 1, +}); diff --git a/dev-packages/browser-integration-tests/suites/tracing/linking-addLink/subject.js b/dev-packages/browser-integration-tests/suites/tracing/linking-addLink/subject.js new file mode 100644 index 000000000000..510fb07540ad --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/linking-addLink/subject.js @@ -0,0 +1,28 @@ +// REGULAR --- +const rootSpan1 = Sentry.startInactiveSpan({ name: 'rootSpan1' }); +rootSpan1.end(); + +Sentry.startSpan({ name: 'rootSpan2' }, rootSpan2 => { + rootSpan2.addLink({ + context: rootSpan1.spanContext(), + attributes: { 'sentry.link.type': 'previous_trace' }, + }); +}); + +// NESTED --- +Sentry.startSpan({ name: 'rootSpan3' }, async rootSpan3 => { + Sentry.startSpan({ name: 'childSpan3.1' }, async childSpan1 => { + childSpan1.addLink({ + context: rootSpan1.spanContext(), + attributes: { 'sentry.link.type': 'previous_trace' }, + }); + + childSpan1.end(); + }); + + Sentry.startSpan({ name: 'childSpan3.2' }, async childSpan2 => { + childSpan2.addLink({ context: rootSpan3.spanContext() }); + + childSpan2.end(); + }); +}); diff --git a/dev-packages/browser-integration-tests/suites/tracing/linking-addLink/test.ts b/dev-packages/browser-integration-tests/suites/tracing/linking-addLink/test.ts new file mode 100644 index 000000000000..9c556335651e --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/linking-addLink/test.ts @@ -0,0 +1,76 @@ +import { expect } from '@playwright/test'; +import type { SpanJSON, TransactionEvent } from '@sentry/core'; +import { sentryTest } from '../../../utils/fixtures'; +import { envelopeRequestParser, shouldSkipTracingTest, waitForTransactionRequest } from '../../../utils/helpers'; + +sentryTest('should link spans with addLink() in trace context', async ({ getLocalTestUrl, page }) => { + if (shouldSkipTracingTest()) { + sentryTest.skip(); + } + + const rootSpan1Promise = waitForTransactionRequest(page, event => event.transaction === 'rootSpan1'); + const rootSpan2Promise = waitForTransactionRequest(page, event => event.transaction === 'rootSpan2'); + + const url = await getLocalTestUrl({ testDir: __dirname }); + await page.goto(url); + + const rootSpan1 = envelopeRequestParser(await rootSpan1Promise); + const rootSpan2 = envelopeRequestParser(await rootSpan2Promise); + + const rootSpan1_traceId = rootSpan1.contexts?.trace?.trace_id as string; + const rootSpan1_spanId = rootSpan1.contexts?.trace?.span_id as string; + + expect(rootSpan1.transaction).toBe('rootSpan1'); + expect(rootSpan1.spans).toEqual([]); + + expect(rootSpan2.transaction).toBe('rootSpan2'); + expect(rootSpan2.spans).toEqual([]); + + expect(rootSpan2.contexts?.trace?.links?.length).toBe(1); + expect(rootSpan2.contexts?.trace?.links?.[0]).toMatchObject({ + attributes: { 'sentry.link.type': 'previous_trace' }, + sampled: true, + span_id: rootSpan1_spanId, + trace_id: rootSpan1_traceId, + }); +}); + +sentryTest('should link spans with addLink() in nested startSpan() calls', async ({ getLocalTestUrl, page }) => { + if (shouldSkipTracingTest()) { + sentryTest.skip(); + } + + const rootSpan1Promise = waitForTransactionRequest(page, event => event.transaction === 'rootSpan1'); + const rootSpan3Promise = waitForTransactionRequest(page, event => event.transaction === 'rootSpan3'); + + const url = await getLocalTestUrl({ testDir: __dirname }); + await page.goto(url); + + const rootSpan1 = envelopeRequestParser(await rootSpan1Promise); + const rootSpan3 = envelopeRequestParser(await rootSpan3Promise); + + const rootSpan1_traceId = rootSpan1.contexts?.trace?.trace_id as string; + const rootSpan1_spanId = rootSpan1.contexts?.trace?.span_id as string; + + const [childSpan_3_1, childSpan_3_2] = rootSpan3.spans as [SpanJSON, SpanJSON]; + const rootSpan3_traceId = rootSpan3.contexts?.trace?.trace_id as string; + const rootSpan3_spanId = rootSpan3.contexts?.trace?.span_id as string; + + expect(rootSpan3.transaction).toBe('rootSpan3'); + + expect(childSpan_3_1.description).toBe('childSpan3.1'); + expect(childSpan_3_1.links?.length).toBe(1); + expect(childSpan_3_1.links?.[0]).toMatchObject({ + attributes: { 'sentry.link.type': 'previous_trace' }, + sampled: true, + span_id: rootSpan1_spanId, + trace_id: rootSpan1_traceId, + }); + + expect(childSpan_3_2.description).toBe('childSpan3.2'); + expect(childSpan_3_2.links?.[0]).toMatchObject({ + sampled: true, + span_id: rootSpan3_spanId, + trace_id: rootSpan3_traceId, + }); +}); diff --git a/dev-packages/browser-integration-tests/suites/tracing/linking-addLinks/init.js b/dev-packages/browser-integration-tests/suites/tracing/linking-addLinks/init.js new file mode 100644 index 000000000000..3ec6adbbdb75 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/linking-addLinks/init.js @@ -0,0 +1,9 @@ +import * as Sentry from '@sentry/browser'; + +window.Sentry = Sentry; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + integrations: [], + tracesSampleRate: 1, +}); diff --git a/dev-packages/browser-integration-tests/suites/tracing/linking-addLinks/subject.js b/dev-packages/browser-integration-tests/suites/tracing/linking-addLinks/subject.js new file mode 100644 index 000000000000..af6c89848fd3 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/linking-addLinks/subject.js @@ -0,0 +1,35 @@ +// REGULAR --- +const rootSpan1 = Sentry.startInactiveSpan({ name: 'rootSpan1' }); +rootSpan1.end(); + +const rootSpan2 = Sentry.startInactiveSpan({ name: 'rootSpan2' }); +rootSpan2.end(); + +Sentry.startSpan({ name: 'rootSpan3' }, rootSpan3 => { + rootSpan3.addLinks([ + { context: rootSpan1.spanContext() }, + { + context: rootSpan2.spanContext(), + attributes: { 'sentry.link.type': 'previous_trace' }, + }, + ]); +}); + +// NESTED --- +Sentry.startSpan({ name: 'rootSpan4' }, async rootSpan4 => { + Sentry.startSpan({ name: 'childSpan4.1' }, async childSpan1 => { + Sentry.startSpan({ name: 'childSpan4.2' }, async childSpan2 => { + childSpan2.addLinks([ + { context: rootSpan4.spanContext() }, + { + context: rootSpan2.spanContext(), + attributes: { 'sentry.link.type': 'previous_trace' }, + }, + ]); + + childSpan2.end(); + }); + + childSpan1.end(); + }); +}); diff --git a/dev-packages/browser-integration-tests/suites/tracing/linking-addLinks/test.ts b/dev-packages/browser-integration-tests/suites/tracing/linking-addLinks/test.ts new file mode 100644 index 000000000000..529eae04ae03 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/linking-addLinks/test.ts @@ -0,0 +1,93 @@ +import { expect } from '@playwright/test'; +import type { SpanJSON, TransactionEvent } from '@sentry/core'; +import { sentryTest } from '../../../utils/fixtures'; +import { envelopeRequestParser, shouldSkipTracingTest, waitForTransactionRequest } from '../../../utils/helpers'; + +sentryTest('should link spans with addLinks() in trace context', async ({ getLocalTestUrl, page }) => { + if (shouldSkipTracingTest()) { + sentryTest.skip(); + } + + const rootSpan1Promise = waitForTransactionRequest(page, event => event.transaction === 'rootSpan1'); + const rootSpan2Promise = waitForTransactionRequest(page, event => event.transaction === 'rootSpan2'); + const rootSpan3Promise = waitForTransactionRequest(page, event => event.transaction === 'rootSpan3'); + + const url = await getLocalTestUrl({ testDir: __dirname }); + await page.goto(url); + + const rootSpan1 = envelopeRequestParser(await rootSpan1Promise); + const rootSpan2 = envelopeRequestParser(await rootSpan2Promise); + const rootSpan3 = envelopeRequestParser(await rootSpan3Promise); + + const rootSpan1_traceId = rootSpan1.contexts?.trace?.trace_id as string; + const rootSpan1_spanId = rootSpan1.contexts?.trace?.span_id as string; + + expect(rootSpan1.transaction).toBe('rootSpan1'); + expect(rootSpan1.spans).toEqual([]); + + const rootSpan2_traceId = rootSpan2.contexts?.trace?.trace_id as string; + const rootSpan2_spanId = rootSpan2.contexts?.trace?.span_id as string; + + expect(rootSpan2.transaction).toBe('rootSpan2'); + expect(rootSpan2.spans).toEqual([]); + + expect(rootSpan3.transaction).toBe('rootSpan3'); + expect(rootSpan3.spans).toEqual([]); + expect(rootSpan3.contexts?.trace?.links?.length).toBe(2); + expect(rootSpan3.contexts?.trace?.links).toEqual([ + { + sampled: true, + span_id: rootSpan1_spanId, + trace_id: rootSpan1_traceId, + }, + { + attributes: { 'sentry.link.type': 'previous_trace' }, + sampled: true, + span_id: rootSpan2_spanId, + trace_id: rootSpan2_traceId, + }, + ]); +}); + +sentryTest('should link spans with addLinks() in nested startSpan() calls', async ({ getLocalTestUrl, page }) => { + if (shouldSkipTracingTest()) { + sentryTest.skip(); + } + + const rootSpan2Promise = waitForTransactionRequest(page, event => event.transaction === 'rootSpan2'); + const rootSpan4Promise = waitForTransactionRequest(page, event => event.transaction === 'rootSpan4'); + + const url = await getLocalTestUrl({ testDir: __dirname }); + await page.goto(url); + + const rootSpan2 = envelopeRequestParser(await rootSpan2Promise); + const rootSpan4 = envelopeRequestParser(await rootSpan4Promise); + + const rootSpan2_traceId = rootSpan2.contexts?.trace?.trace_id as string; + const rootSpan2_spanId = rootSpan2.contexts?.trace?.span_id as string; + + const [childSpan_4_1, childSpan_4_2] = rootSpan4.spans as [SpanJSON, SpanJSON]; + const rootSpan4_traceId = rootSpan4.contexts?.trace?.trace_id as string; + const rootSpan4_spanId = rootSpan4.contexts?.trace?.span_id as string; + + expect(rootSpan4.transaction).toBe('rootSpan4'); + + expect(childSpan_4_1.description).toBe('childSpan4.1'); + expect(childSpan_4_1.links).toBe(undefined); + + expect(childSpan_4_2.description).toBe('childSpan4.2'); + expect(childSpan_4_2.links?.length).toBe(2); + expect(childSpan_4_2.links).toEqual([ + { + sampled: true, + span_id: rootSpan4_spanId, + trace_id: rootSpan4_traceId, + }, + { + attributes: { 'sentry.link.type': 'previous_trace' }, + sampled: true, + span_id: rootSpan2_spanId, + trace_id: rootSpan2_traceId, + }, + ]); +}); diff --git a/packages/core/src/tracing/sentrySpan.ts b/packages/core/src/tracing/sentrySpan.ts index ddf036f88cdb..3194a45f707f 100644 --- a/packages/core/src/tracing/sentrySpan.ts +++ b/packages/core/src/tracing/sentrySpan.ts @@ -24,6 +24,7 @@ import type { TransactionEvent, TransactionSource, } from '../types-hoist'; +import type { SpanLink } from '../types-hoist/link'; import { logger } from '../utils-hoist/logger'; import { dropUndefinedKeys } from '../utils-hoist/object'; import { generateSpanId, generateTraceId } from '../utils-hoist/propagationContext'; @@ -31,6 +32,7 @@ import { timestampInSeconds } from '../utils-hoist/time'; import { TRACE_FLAG_NONE, TRACE_FLAG_SAMPLED, + convertSpanLinksForEnvelope, getRootSpan, getSpanDescendants, getStatusMessage, @@ -55,6 +57,7 @@ export class SentrySpan implements Span { protected _sampled: boolean | undefined; protected _name?: string | undefined; protected _attributes: SpanAttributes; + protected _links?: SpanLink[]; /** Epoch timestamp in seconds when the span started. */ protected _startTime: number; /** Epoch timestamp in seconds when the span ended. */ @@ -110,12 +113,22 @@ export class SentrySpan implements Span { } /** @inheritDoc */ - public addLink(_link: unknown): this { + public addLink(link: SpanLink): this { + if (this._links) { + this._links.push(link); + } else { + this._links = [link]; + } return this; } /** @inheritDoc */ - public addLinks(_links: unknown[]): this { + public addLinks(links: SpanLink[]): this { + if (this._links) { + this._links.push(...links); + } else { + this._links = links; + } return this; } @@ -225,6 +238,7 @@ export class SentrySpan implements Span { measurements: timedEventsToMeasurements(this._events), is_segment: (this._isStandaloneSpan && getRootSpan(this) === this) || undefined, segment_id: this._isStandaloneSpan ? getRootSpan(this).spanContext().spanId : undefined, + links: convertSpanLinksForEnvelope(this._links), }); } diff --git a/packages/core/src/utils/spanUtils.ts b/packages/core/src/utils/spanUtils.ts index d23a08a96808..7cb19fbacf3c 100644 --- a/packages/core/src/utils/spanUtils.ts +++ b/packages/core/src/utils/spanUtils.ts @@ -40,7 +40,7 @@ let hasShownSpanDropWarning = false; */ export function spanToTransactionTraceContext(span: Span): TraceContext { const { spanId: span_id, traceId: trace_id } = span.spanContext(); - const { data, op, parent_span_id, status, origin } = spanToJSON(span); + const { data, op, parent_span_id, status, origin, links } = spanToJSON(span); return dropUndefinedKeys({ parent_span_id, @@ -50,6 +50,7 @@ export function spanToTransactionTraceContext(span: Span): TraceContext { op, status, origin, + links, }); } diff --git a/packages/core/test/lib/tracing/trace.test.ts b/packages/core/test/lib/tracing/trace.test.ts index c33b50c01a85..00ee444d6f69 100644 --- a/packages/core/test/lib/tracing/trace.test.ts +++ b/packages/core/test/lib/tracing/trace.test.ts @@ -399,6 +399,40 @@ describe('startSpan', () => { }); }); + it('allows to add span links', () => { + const rawSpan1 = startInactiveSpan({ name: 'pageload_span' }); + + // @ts-expect-error _links exists on span + expect(rawSpan1?._links).toEqual(undefined); + + const span1JSON = spanToJSON(rawSpan1); + + startSpan({ name: '/users/:id' }, rawSpan2 => { + rawSpan2.addLink({ + context: rawSpan1.spanContext(), + attributes: { + 'sentry.link.type': 'previous_trace', + }, + }); + + const span2LinkJSON = spanToJSON(rawSpan2).links?.[0]; + + expect(span2LinkJSON?.attributes?.['sentry.link.type']).toBe('previous_trace'); + + // @ts-expect-error _links and _traceId exist on SentrySpan + expect(rawSpan2._links?.[0].context.traceId).toEqual(rawSpan1._traceId); + // @ts-expect-error _links and _traceId exist on SentrySpan + expect(rawSpan2?._links?.[0].context.traceId).toEqual(span1JSON.trace_id); + expect(span2LinkJSON?.trace_id).toBe(span1JSON.trace_id); + + // @ts-expect-error _links and _traceId exist on SentrySpan + expect(rawSpan2?._links?.[0].context.spanId).toEqual(rawSpan1?._spanId); + // @ts-expect-error _links and _traceId exist on SentrySpan + expect(rawSpan2?._links?.[0].context.spanId).toEqual(span1JSON.span_id); + expect(span2LinkJSON?.span_id).toBe(span1JSON.span_id); + }); + }); + it('allows to force a transaction with forceTransaction=true', async () => { const options = getDefaultTestClientOptions({ tracesSampleRate: 1.0 }); client = new TestClient(options); @@ -900,6 +934,40 @@ describe('startSpanManual', () => { }); }); + it('allows to add span links', () => { + const rawSpan1 = startInactiveSpan({ name: 'pageload_span' }); + + // @ts-expect-error _links exists on span + expect(rawSpan1?._links).toEqual(undefined); + + const span1JSON = spanToJSON(rawSpan1); + + startSpanManual({ name: '/users/:id' }, rawSpan2 => { + rawSpan2.addLink({ + context: rawSpan1.spanContext(), + attributes: { + 'sentry.link.type': 'previous_trace', + }, + }); + + const span2LinkJSON = spanToJSON(rawSpan2).links?.[0]; + + expect(span2LinkJSON?.attributes?.['sentry.link.type']).toBe('previous_trace'); + + // @ts-expect-error _links and _traceId exist on SentrySpan + expect(rawSpan2?._links?.[0].context.traceId).toEqual(rawSpan1._traceId); + // @ts-expect-error _links and _traceId exist on SentrySpan + expect(rawSpan2?._links?.[0].context.traceId).toEqual(span1JSON.trace_id); + expect(span2LinkJSON?.trace_id).toBe(span1JSON.trace_id); + + // @ts-expect-error _links and _traceId exist on SentrySpan + expect(rawSpan2?._links?.[0].context.spanId).toEqual(rawSpan1?._spanId); + // @ts-expect-error _links and _traceId exist on SentrySpan + expect(rawSpan2?._links?.[0].context.spanId).toEqual(span1JSON.span_id); + expect(span2LinkJSON?.span_id).toBe(span1JSON.span_id); + }); + }); + it('allows to force a transaction with forceTransaction=true', async () => { const options = getDefaultTestClientOptions({ tracesSampleRate: 1.0 }); client = new TestClient(options); diff --git a/packages/opentelemetry/test/trace.test.ts b/packages/opentelemetry/test/trace.test.ts index 0222264ad6de..93d5fa448dda 100644 --- a/packages/opentelemetry/test/trace.test.ts +++ b/packages/opentelemetry/test/trace.test.ts @@ -356,7 +356,7 @@ describe('trace', () => { }); }); - it('allows to pass span links', () => { + it('allows to add span links', () => { const rawSpan1 = startInactiveSpan({ name: 'pageload_span' }); // @ts-expect-error links exists on span @@ -1016,7 +1016,7 @@ describe('trace', () => { }); }); - it('allows to pass span links', () => { + it('allows to add span links', () => { const rawSpan1 = startInactiveSpan({ name: 'pageload_span' }); // @ts-expect-error links exists on span From 14667eea75b19cf711f53732a7bbb4de145c541c Mon Sep 17 00:00:00 2001 From: Sigrid Huemer <32902192+s1gr1d@users.noreply.github.com> Date: Mon, 24 Feb 2025 15:45:47 +0100 Subject: [PATCH 30/32] feat(core): Add links to span options (#15453) part of https://github.com/getsentry/sentry-javascript/issues/14991 --- .../tracing/linking-spanOptions/init.js | 9 ++++ .../tracing/linking-spanOptions/subject.js | 20 ++++++++ .../tracing/linking-spanOptions/test.ts | 48 +++++++++++++++++++ packages/core/src/tracing/sentrySpan.ts | 1 + packages/core/test/lib/tracing/trace.test.ts | 38 +++++++++++++++ 5 files changed, 116 insertions(+) create mode 100644 dev-packages/browser-integration-tests/suites/tracing/linking-spanOptions/init.js create mode 100644 dev-packages/browser-integration-tests/suites/tracing/linking-spanOptions/subject.js create mode 100644 dev-packages/browser-integration-tests/suites/tracing/linking-spanOptions/test.ts diff --git a/dev-packages/browser-integration-tests/suites/tracing/linking-spanOptions/init.js b/dev-packages/browser-integration-tests/suites/tracing/linking-spanOptions/init.js new file mode 100644 index 000000000000..3ec6adbbdb75 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/linking-spanOptions/init.js @@ -0,0 +1,9 @@ +import * as Sentry from '@sentry/browser'; + +window.Sentry = Sentry; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + integrations: [], + tracesSampleRate: 1, +}); diff --git a/dev-packages/browser-integration-tests/suites/tracing/linking-spanOptions/subject.js b/dev-packages/browser-integration-tests/suites/tracing/linking-spanOptions/subject.js new file mode 100644 index 000000000000..797ce3d98fa7 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/linking-spanOptions/subject.js @@ -0,0 +1,20 @@ +const rootSpan1 = Sentry.startInactiveSpan({ name: 'rootSpan1' }); +rootSpan1.end(); + +const rootSpan2 = Sentry.startInactiveSpan({ name: 'rootSpan2' }); +rootSpan2.end(); + +Sentry.startSpan( + { + name: 'rootSpan3', + links: [ + { context: rootSpan1.spanContext() }, + { context: rootSpan2.spanContext(), attributes: { 'sentry.link.type': 'previous_trace' } }, + ], + }, + async () => { + Sentry.startSpan({ name: 'childSpan3.1' }, async childSpan1 => { + childSpan1.end(); + }); + }, +); diff --git a/dev-packages/browser-integration-tests/suites/tracing/linking-spanOptions/test.ts b/dev-packages/browser-integration-tests/suites/tracing/linking-spanOptions/test.ts new file mode 100644 index 000000000000..c2a2ed02f0e6 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/tracing/linking-spanOptions/test.ts @@ -0,0 +1,48 @@ +import { expect } from '@playwright/test'; +import type { TransactionEvent } from '@sentry/core'; +import { sentryTest } from '../../../utils/fixtures'; +import { envelopeRequestParser, shouldSkipTracingTest, waitForTransactionRequest } from '../../../utils/helpers'; + +sentryTest('should link spans by adding "links" to span options', async ({ getLocalTestUrl, page }) => { + if (shouldSkipTracingTest()) { + sentryTest.skip(); + } + + const rootSpan1Promise = waitForTransactionRequest(page, event => event.transaction === 'rootSpan1'); + const rootSpan2Promise = waitForTransactionRequest(page, event => event.transaction === 'rootSpan2'); + const rootSpan3Promise = waitForTransactionRequest(page, event => event.transaction === 'rootSpan3'); + + const url = await getLocalTestUrl({ testDir: __dirname }); + await page.goto(url); + + const rootSpan1 = envelopeRequestParser(await rootSpan1Promise); + const rootSpan2 = envelopeRequestParser(await rootSpan2Promise); + const rootSpan3 = envelopeRequestParser(await rootSpan3Promise); + + const rootSpan1_traceId = rootSpan1.contexts?.trace?.trace_id as string; + const rootSpan1_spanId = rootSpan1.contexts?.trace?.span_id as string; + const rootSpan2_traceId = rootSpan2.contexts?.trace?.trace_id as string; + const rootSpan2_spanId = rootSpan2.contexts?.trace?.span_id as string; + + expect(rootSpan1.transaction).toBe('rootSpan1'); + expect(rootSpan1.spans).toEqual([]); + + expect(rootSpan3.transaction).toBe('rootSpan3'); + expect(rootSpan3.spans?.length).toBe(1); + expect(rootSpan3.spans?.[0].description).toBe('childSpan3.1'); + + expect(rootSpan3.contexts?.trace?.links?.length).toBe(2); + expect(rootSpan3.contexts?.trace?.links).toEqual([ + { + sampled: true, + span_id: rootSpan1_spanId, + trace_id: rootSpan1_traceId, + }, + { + attributes: { 'sentry.link.type': 'previous_trace' }, + sampled: true, + span_id: rootSpan2_spanId, + trace_id: rootSpan2_traceId, + }, + ]); +}); diff --git a/packages/core/src/tracing/sentrySpan.ts b/packages/core/src/tracing/sentrySpan.ts index 3194a45f707f..53f103c5ed52 100644 --- a/packages/core/src/tracing/sentrySpan.ts +++ b/packages/core/src/tracing/sentrySpan.ts @@ -81,6 +81,7 @@ export class SentrySpan implements Span { this._traceId = spanContext.traceId || generateTraceId(); this._spanId = spanContext.spanId || generateSpanId(); this._startTime = spanContext.startTimestamp || timestampInSeconds(); + this._links = spanContext.links; this._attributes = {}; this.setAttributes({ diff --git a/packages/core/test/lib/tracing/trace.test.ts b/packages/core/test/lib/tracing/trace.test.ts index 00ee444d6f69..e15bf146bee6 100644 --- a/packages/core/test/lib/tracing/trace.test.ts +++ b/packages/core/test/lib/tracing/trace.test.ts @@ -1305,6 +1305,44 @@ describe('startInactiveSpan', () => { }); }); + it('allows to pass span links in span options', () => { + const rawSpan1 = startInactiveSpan({ name: 'pageload_span' }); + + // @ts-expect-error _links exists on span + expect(rawSpan1?._links).toEqual(undefined); + + const rawSpan2 = startInactiveSpan({ + name: 'GET users/[id]', + links: [ + { + context: rawSpan1.spanContext(), + attributes: { 'sentry.link.type': 'previous_trace' }, + }, + ], + }); + + const span1JSON = spanToJSON(rawSpan1); + const span2JSON = spanToJSON(rawSpan2); + const span2LinkJSON = span2JSON.links?.[0]; + + expect(span2LinkJSON?.attributes?.['sentry.link.type']).toBe('previous_trace'); + + // @ts-expect-error _links and _traceId exist on SentrySpan + expect(rawSpan2?._links?.[0].context.traceId).toEqual(rawSpan1._traceId); + // @ts-expect-error _links and _traceId exist on SentrySpan + expect(rawSpan2?._links?.[0].context.traceId).toEqual(span1JSON.trace_id); + expect(span2LinkJSON?.trace_id).toBe(span1JSON.trace_id); + + // @ts-expect-error _links and _traceId exist on SentrySpan + expect(rawSpan2?._links?.[0].context.spanId).toEqual(rawSpan1?._spanId); + // @ts-expect-error _links and _traceId exist on SentrySpan + expect(rawSpan2?._links?.[0].context.spanId).toEqual(span1JSON.span_id); + expect(span2LinkJSON?.span_id).toBe(span1JSON.span_id); + + // sampling decision is inherited + expect(span2LinkJSON?.sampled).toBe(Boolean(spanToJSON(rawSpan1).data['sentry.sample_rate'])); + }); + it('allows to force a transaction with forceTransaction=true', async () => { const options = getDefaultTestClientOptions({ tracesSampleRate: 1.0 }); client = new TestClient(options); From 48065f8c38e245d15d7a910e5d454dbb4fc0dada Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Thu, 20 Feb 2025 15:36:12 +0100 Subject: [PATCH 31/32] meta: Update changelog for 9.2.0 attributions more entries correct username --- CHANGELOG.md | 40 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 39 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bddbe3762189..bfdaee843a0c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,7 +10,45 @@ - "You miss 100 percent of the chances you don't take. — Wayne Gretzky" — Michael Scott -Work in this release was contributed by @6farer. Thank you for your contribution! +## 9.2.0 + +### Important Changes + +- **feat(node): Support Express v5 ([#15380](https://github.com/getsentry/sentry-javascript/pull/15380))** + +This release adds full tracing support for Express v5, and improves tracing support for Nest.js 11 (which uses Express v5) in the Nest.js SDK. + +- **feat(sveltekit): Add Support for Cloudflare ([#14672](https://github.com/getsentry/sentry-javascript/pull/14672))** + +This release adds support for deploying SvelteKit applications to Cloudflare Pages. +A docs update with updated instructions will follow shortly. +Until then, you can give this a try by setting up the SvelteKit SDK as usual and then following the instructions outlined in the PR. + +Thank you @SG60 for contributing this feature! + +### Other Changes + +- feat(core): Add `addLink(s)` to Sentry span ([#15452](https://github.com/getsentry/sentry-javascript/pull/15452)) +- feat(core): Add links to span options ([#15453](https://github.com/getsentry/sentry-javascript/pull/15453)) +- feat(deps): Bump @sentry/webpack-plugin from 2.22.7 to 3.1.2 ([#15328](https://github.com/getsentry/sentry-javascript/pull/15328)) +- feat(feedback): Disable Feedback submit & cancel buttons while submitting ([#15408](https://github.com/getsentry/sentry-javascript/pull/15408)) +- feat(nextjs): Add experimental flag to not strip origin information from different origin stack frames ([#15418](https://github.com/getsentry/sentry-javascript/pull/15418)) +- feat(nuxt): Add `enableNitroErrorHandler` to server options ([#15444](https://github.com/getsentry/sentry-javascript/pull/15444)) +- feat(opentelemetry): Add `addLink(s)` to span ([#15387](https://github.com/getsentry/sentry-javascript/pull/15387)) +- feat(opentelemetry): Add `links` to span options ([#15403](https://github.com/getsentry/sentry-javascript/pull/15403)) +- feat(replay): Expose rrweb recordCrossOriginIframes under \_experiments ([#14916](https://github.com/getsentry/sentry-javascript/pull/14916)) +- fix(browser): Ensure that `performance.measure` spans have a positive duration ([#15415](https://github.com/getsentry/sentry-javascript/pull/15415)) +- fix(bun): Includes correct sdk metadata ([#15459](https://github.com/getsentry/sentry-javascript/pull/15459)) +- fix(core): Add Google `gmo` error to Inbound Filters ([#15432](https://github.com/getsentry/sentry-javascript/pull/15432)) +- fix(core): Ensure `http.client` span descriptions don't contain query params or fragments ([#15404](https://github.com/getsentry/sentry-javascript/pull/15404)) +- fix(core): Filter out unactionable Facebook Mobile browser error ([#15430](https://github.com/getsentry/sentry-javascript/pull/15430)) +- fix(nestjs): Pin dependency on `@opentelemetry/instrumentation` ([#15419](https://github.com/getsentry/sentry-javascript/pull/15419)) +- fix(nuxt): Only use filename with file extension from command ([#15445](https://github.com/getsentry/sentry-javascript/pull/15445)) +- fix(nuxt): Use `SentryNuxtServerOptions` type for server init ([#15441](https://github.com/getsentry/sentry-javascript/pull/15441)) +- fix(sveltekit): Avoid loading vite config to determine source maps setting ([#15440](https://github.com/getsentry/sentry-javascript/pull/15440)) +- ref(profiling-node): Bump chunk interval to 60s ([#15361](https://github.com/getsentry/sentry-javascript/pull/15361)) + +Work in this release was contributed by @6farer, @dgavranic and @SG60. Thank you for your contributions! ## 9.1.0 From 12425e233cc5633dcc1579f25571c3938d421fc8 Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Mon, 24 Feb 2025 16:35:07 +0100 Subject: [PATCH 32/32] start CI please