From da1ccb297a19e32cbda5b082e7ccc008b0d20088 Mon Sep 17 00:00:00 2001 From: Ash Anand Date: Tue, 19 Dec 2023 15:57:32 -0500 Subject: [PATCH 01/24] feat(utils): Add function to extract relevant component name --- packages/utils/src/browser.ts | 38 +++++++++++++++++++++++++++++++---- 1 file changed, 34 insertions(+), 4 deletions(-) diff --git a/packages/utils/src/browser.ts b/packages/utils/src/browser.ts index d2d8f7af9a72..5688277d0f4f 100644 --- a/packages/utils/src/browser.ts +++ b/packages/utils/src/browser.ts @@ -6,6 +6,10 @@ const WINDOW = getGlobalObject(); const DEFAULT_MAX_STRING_LENGTH = 80; +type SimpleNode = { + parentNode: SimpleNode; +} | null; + /** * Given a child DOM element, returns a query-selector statement describing that * and its ancestors @@ -16,10 +20,6 @@ export function htmlTreeAsString( elem: unknown, options: string[] | { keyAttrs?: string[]; maxStringLength?: number } = {}, ): string { - type SimpleNode = { - parentNode: SimpleNode; - } | null; - if (!elem) { return ''; } @@ -86,6 +86,11 @@ function _htmlElementAsString(el: unknown, keyAttrs?: string[]): string { return ''; } + // If using the component name annotation plugin, this value may be available on the DOM node + if (elem instanceof HTMLElement && elem.dataset && elem.dataset['sentryComponent']) { + return elem.dataset['sentryComponent']; + } + out.push(elem.tagName.toLowerCase()); // Pairs of attribute keys defined in `serializeAttribute` and their values on element. @@ -157,3 +162,28 @@ export function getDomElement(selector: string): E | null { } return null; } + +/** + * Given a DOM element, traverses up the tree until it finds the first ancestor node + * that has the `data-sentry-component` attribute. This attribute is added at build-time + * by projects that have the component name annotation plugin installed. + * + * @returns a string representation of the component for the provided DOM element, or `null` if not found + */ +export function getComponentName(elem: unknown): string | null { + let currentElem = elem as SimpleNode; + const MAX_TRAVERSE_HEIGHT = 5; + for (let i = 0; i < MAX_TRAVERSE_HEIGHT; i++) { + if (!currentElem) { + return null; + } + + if (currentElem instanceof HTMLElement && currentElem.dataset['sentryComponent']) { + return currentElem.dataset['sentryComponent']; + } + + currentElem = currentElem.parentNode; + } + + return null; +} From 5bc6f96f6bf50f556c8c7fa257559a149844fa0a Mon Sep 17 00:00:00 2001 From: Ash Anand Date: Tue, 19 Dec 2023 16:11:06 -0500 Subject: [PATCH 02/24] Add HTMLElement type to tests to prevent failures --- packages/utils/test/browser.test.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/utils/test/browser.test.ts b/packages/utils/test/browser.test.ts index 040789fe8426..5c7df188664e 100644 --- a/packages/utils/test/browser.test.ts +++ b/packages/utils/test/browser.test.ts @@ -6,6 +6,8 @@ beforeAll(() => { const dom = new JSDOM(); // @ts-expect-error need to override global document global.document = dom.window.document; + // @ts-expect-error need to add HTMLElement type or it will not be found + global.HTMLElement = new JSDOM().window.HTMLElement; }); describe('htmlTreeAsString', () => { From 810eb95ccaaaa1b33d18e264b9a259bd99895f49 Mon Sep 17 00:00:00 2001 From: Luca Forstner Date: Wed, 20 Dec 2023 15:14:47 +0100 Subject: [PATCH 03/24] build: Create internal `rollup-utils` package (#9908) --- .eslintrc.js | 2 +- .github/workflows/build.yml | 2 +- package.json | 5 ++-- packages/astro/package.json | 4 +-- ...up.npm.config.js => rollup.npm.config.mjs} | 2 +- packages/browser/package.json | 10 ++++---- ...dle.config.js => rollup.bundle.config.mjs} | 2 +- .../rollup.npm.config.mjs} | 2 +- .../browser/test/integration/debugging.md | 2 +- packages/bun/package.json | 4 +-- ...up.npm.config.js => rollup.npm.config.mjs} | 2 +- packages/core/package.json | 4 +-- packages/core/rollup.npm.config.js | 3 --- packages/core/rollup.npm.config.mjs | 3 +++ packages/deno/package.json | 4 +-- ....test.config.js => rollup.test.config.mjs} | 0 ...ypes.config.js => rollup.types.config.mjs} | 0 packages/eslint-config-sdk/src/base.js | 2 +- packages/feedback/package.json | 4 +-- ...dle.config.js => rollup.bundle.config.mjs} | 2 +- ...up.npm.config.js => rollup.npm.config.mjs} | 2 +- packages/gatsby/package.json | 4 +-- packages/gatsby/rollup.npm.config.js | 3 --- packages/gatsby/rollup.npm.config.mjs | 3 +++ packages/hub/package.json | 4 +-- packages/hub/rollup.npm.config.js | 3 --- packages/hub/rollup.npm.config.mjs | 3 +++ packages/integration-shims/package.json | 2 +- ...up.npm.config.js => rollup.npm.config.mjs} | 2 +- packages/integrations/package.json | 4 +-- ...dle.config.js => rollup.bundle.config.mjs} | 2 +- .../rollup.npm.config.mjs} | 2 +- packages/integrations/scripts/buildBundles.ts | 2 +- ...up.npm.config.js => rollup.npm.config.mjs} | 2 +- packages/nextjs/scripts/buildRollup.ts | 2 +- packages/node-experimental/package.json | 4 +-- .../node-experimental/rollup.npm.config.js | 3 --- .../node-experimental/rollup.npm.config.mjs | 3 +++ packages/node/package.json | 6 ++--- packages/node/rollup.npm.config.js | 3 --- packages/node/rollup.npm.config.mjs | 3 +++ packages/opentelemetry-node/package.json | 4 +-- .../opentelemetry-node/rollup.npm.config.js | 3 --- .../opentelemetry-node/rollup.npm.config.mjs | 3 +++ packages/opentelemetry/package.json | 4 +-- packages/opentelemetry/rollup.npm.config.js | 3 --- packages/opentelemetry/rollup.npm.config.mjs | 3 +++ packages/react/package.json | 4 +-- ...up.npm.config.js => rollup.npm.config.mjs} | 2 +- packages/remix/package.json | 4 +-- ...up.npm.config.js => rollup.npm.config.mjs} | 2 +- packages/replay-worker/package.json | 4 +-- ...ker.config.js => rollup.worker.config.mjs} | 0 packages/replay/package.json | 4 +-- ...dle.config.js => rollup.bundle.config.mjs} | 2 +- ...up.npm.config.js => rollup.npm.config.mjs} | 2 +- packages/rollup-utils/README.md | 5 ++++ .../rollup-utils/bundleHelpers.mjs | 4 +-- packages/rollup-utils/index.mjs | 7 ++++++ .../rollup-utils/npmHelpers.mjs | 7 +++--- packages/rollup-utils/package.json | 19 ++++++++++++++ .../rollup-utils/plugins/bundlePlugins.mjs | 7 +++--- .../plugins/extractPolyfillsPlugin.mjs | 4 +-- packages/rollup-utils/plugins/index.mjs | 2 ++ .../rollup-utils/plugins/npmPlugins.mjs | 3 ++- .../rollup-utils}/polyfills/es5.js | 0 .../rollup-utils/utils.mjs | 0 packages/serverless/package.json | 4 +-- ...up.aws.config.js => rollup.aws.config.mjs} | 2 +- ...up.npm.config.js => rollup.npm.config.mjs} | 2 +- .../serverless/scripts/buildLambdaLayer.ts | 2 +- packages/svelte/package.json | 4 +-- ...up.npm.config.js => rollup.npm.config.mjs} | 2 +- packages/sveltekit/package.json | 4 +-- ...up.npm.config.js => rollup.npm.config.mjs} | 2 +- packages/tracing-internal/package.json | 4 +-- .../tracing-internal/rollup.npm.config.js | 3 --- .../tracing-internal/rollup.npm.config.mjs | 3 +++ packages/tracing/package.json | 4 +-- .../rollup.npm.config.mjs} | 2 +- packages/types/package.json | 4 +-- packages/types/rollup.npm.config.js | 3 --- packages/types/rollup.npm.config.mjs | 3 +++ packages/utils/package.json | 2 +- packages/utils/rollup.npm.config.js | 3 --- packages/utils/rollup.npm.config.mjs | 3 +++ packages/utils/scripts/buildRollup.ts | 2 +- packages/vercel-edge/package.json | 4 +-- packages/vercel-edge/rollup.npm.config.js | 3 --- packages/vercel-edge/rollup.npm.config.mjs | 3 +++ packages/vue/package.json | 4 +-- packages/vue/rollup.npm.config.js | 3 --- packages/vue/rollup.npm.config.mjs | 3 +++ packages/wasm/package.json | 8 +++--- ...dle.config.js => rollup.bundle.config.mjs} | 2 +- ...up.npm.config.js => rollup.npm.config.mjs} | 2 +- rollup/index.js | 9 ------- rollup/plugins/index.js | 2 -- scripts/tarball-checksums.sh | 25 +++++++++++++++++++ 99 files changed, 199 insertions(+), 150 deletions(-) rename packages/astro/{rollup.npm.config.js => rollup.npm.config.mjs} (81%) rename packages/browser/{rollup.bundle.config.js => rollup.bundle.config.mjs} (98%) rename packages/{integrations/rollup.npm.config.js => browser/rollup.npm.config.mjs} (64%) rename packages/bun/{rollup.npm.config.js => rollup.npm.config.mjs} (62%) delete mode 100644 packages/core/rollup.npm.config.js create mode 100644 packages/core/rollup.npm.config.mjs rename packages/deno/{rollup.test.config.js => rollup.test.config.mjs} (100%) rename packages/deno/{rollup.types.config.js => rollup.types.config.mjs} (100%) rename packages/feedback/{rollup.bundle.config.js => rollup.bundle.config.mjs} (91%) rename packages/feedback/{rollup.npm.config.js => rollup.npm.config.mjs} (81%) delete mode 100644 packages/gatsby/rollup.npm.config.js create mode 100644 packages/gatsby/rollup.npm.config.mjs delete mode 100644 packages/hub/rollup.npm.config.js create mode 100644 packages/hub/rollup.npm.config.mjs rename packages/integration-shims/{rollup.npm.config.js => rollup.npm.config.mjs} (53%) rename packages/integrations/{rollup.bundle.config.js => rollup.bundle.config.mjs} (95%) rename packages/{tracing/rollup.npm.config.js => integrations/rollup.npm.config.mjs} (64%) rename packages/nextjs/{rollup.npm.config.js => rollup.npm.config.mjs} (96%) delete mode 100644 packages/node-experimental/rollup.npm.config.js create mode 100644 packages/node-experimental/rollup.npm.config.mjs delete mode 100644 packages/node/rollup.npm.config.js create mode 100644 packages/node/rollup.npm.config.mjs delete mode 100644 packages/opentelemetry-node/rollup.npm.config.js create mode 100644 packages/opentelemetry-node/rollup.npm.config.mjs delete mode 100644 packages/opentelemetry/rollup.npm.config.js create mode 100644 packages/opentelemetry/rollup.npm.config.mjs rename packages/react/{rollup.npm.config.js => rollup.npm.config.mjs} (51%) rename packages/remix/{rollup.npm.config.js => rollup.npm.config.mjs} (80%) rename packages/replay-worker/{rollup.worker.config.js => rollup.worker.config.mjs} (100%) rename packages/replay/{rollup.bundle.config.js => rollup.bundle.config.mjs} (91%) rename packages/replay/{rollup.npm.config.js => rollup.npm.config.mjs} (81%) create mode 100644 packages/rollup-utils/README.md rename rollup/bundleHelpers.js => packages/rollup-utils/bundleHelpers.mjs (98%) create mode 100644 packages/rollup-utils/index.mjs rename rollup/npmHelpers.js => packages/rollup-utils/npmHelpers.mjs (94%) create mode 100644 packages/rollup-utils/package.json rename rollup/plugins/bundlePlugins.js => packages/rollup-utils/plugins/bundlePlugins.mjs (95%) rename rollup/plugins/extractPolyfillsPlugin.js => packages/rollup-utils/plugins/extractPolyfillsPlugin.mjs (98%) create mode 100644 packages/rollup-utils/plugins/index.mjs rename rollup/plugins/npmPlugins.js => packages/rollup-utils/plugins/npmPlugins.mjs (98%) rename {rollup => packages/rollup-utils}/polyfills/es5.js (100%) rename rollup/utils.js => packages/rollup-utils/utils.mjs (100%) rename packages/serverless/{rollup.aws.config.js => rollup.aws.config.mjs} (97%) rename packages/serverless/{rollup.npm.config.js => rollup.npm.config.mjs} (86%) rename packages/svelte/{rollup.npm.config.js => rollup.npm.config.mjs} (69%) rename packages/sveltekit/{rollup.npm.config.js => rollup.npm.config.mjs} (77%) delete mode 100644 packages/tracing-internal/rollup.npm.config.js create mode 100644 packages/tracing-internal/rollup.npm.config.mjs rename packages/{browser/rollup.npm.config.js => tracing/rollup.npm.config.mjs} (64%) delete mode 100644 packages/types/rollup.npm.config.js create mode 100644 packages/types/rollup.npm.config.mjs delete mode 100644 packages/utils/rollup.npm.config.js create mode 100644 packages/utils/rollup.npm.config.mjs delete mode 100644 packages/vercel-edge/rollup.npm.config.js create mode 100644 packages/vercel-edge/rollup.npm.config.mjs delete mode 100644 packages/vue/rollup.npm.config.js create mode 100644 packages/vue/rollup.npm.config.mjs rename packages/wasm/{rollup.bundle.config.js => rollup.bundle.config.mjs} (91%) rename packages/wasm/{rollup.npm.config.js => rollup.npm.config.mjs} (64%) delete mode 100644 rollup/index.js delete mode 100644 rollup/plugins/index.js create mode 100644 scripts/tarball-checksums.sh diff --git a/.eslintrc.js b/.eslintrc.js index 5a217a09fd75..882db9c4b069 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -62,7 +62,7 @@ module.exports = { }, }, { - files: ['scenarios/**', 'rollup/**'], + files: ['scenarios/**', 'packages/rollup-utils/**'], parserOptions: { sourceType: 'module', }, diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 1c7d639db7ed..726e2398d488 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -86,9 +86,9 @@ jobs: - 'CHANGELOG.md' - '.github/**' - 'jest/**' - - 'rollup/**' - 'scripts/**' - 'packages/core/**' + - 'packages/rollup-utils/**' - 'packages/tracing/**' - 'packages/tracing-internal/**' - 'packages/utils/**' diff --git a/package.json b/package.json index db8a133e494b..080235bde9a9 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "private": true, "scripts": { - "build": "node ./scripts/verify-packages-versions.js && run-s build:types build:transpile build:bundle", + "build": "node ./scripts/verify-packages-versions.js && run-s build:transpile build:types build:bundle", "build:bundle": "lerna run build:bundle", "build:dev": "lerna run build:types,build:transpile", "build:dev:filter": "lerna run build:dev --include-filtered-dependencies --include-filtered-dependents --scope", @@ -69,6 +69,7 @@ "packages/remix", "packages/replay", "packages/replay-worker", + "packages/rollup-utils", "packages/serverless", "packages/svelte", "packages/sveltekit", @@ -99,7 +100,6 @@ "@types/rimraf": "^3.0.2", "@types/sinon": "^7.0.11", "@vitest/coverage-c8": "^0.29.2", - "acorn": "^8.7.0", "chai": "^4.1.2", "codecov": "^3.6.5", "deepmerge": "^4.2.2", @@ -116,7 +116,6 @@ "mocha": "^6.1.4", "nodemon": "^2.0.16", "npm-run-all": "^4.1.5", - "recast": "^0.20.5", "replace-in-file": "^4.0.0", "rimraf": "^3.0.2", "rollup": "^2.67.1", diff --git a/packages/astro/package.json b/packages/astro/package.json index 6546a3243030..af17f35903a4 100644 --- a/packages/astro/package.json +++ b/packages/astro/package.json @@ -58,11 +58,11 @@ "scripts": { "build": "run-p build:transpile build:types", "build:dev": "yarn build", - "build:transpile": "rollup -c rollup.npm.config.js --bundleConfigAsCjs", + "build:transpile": "rollup -c rollup.npm.config.mjs", "build:types": "tsc -p tsconfig.types.json", "build:watch": "run-p build:transpile:watch build:types:watch", "build:dev:watch": "yarn build:watch", - "build:transpile:watch": "rollup -c rollup.npm.config.js --bundleConfigAsCjs --watch", + "build:transpile:watch": "rollup -c rollup.npm.config.mjs --watch", "build:types:watch": "tsc -p tsconfig.types.json --watch", "build:tarball": "ts-node ../../scripts/prepack.ts && npm pack ./build", "circularDepCheck": "madge --circular src/index.client.ts && madge --circular src/index.server.ts && madge --circular src/index.types.ts", diff --git a/packages/astro/rollup.npm.config.js b/packages/astro/rollup.npm.config.mjs similarity index 81% rename from packages/astro/rollup.npm.config.js rename to packages/astro/rollup.npm.config.mjs index eaab8bdb45bf..c485392d0ec7 100644 --- a/packages/astro/rollup.npm.config.js +++ b/packages/astro/rollup.npm.config.mjs @@ -1,4 +1,4 @@ -import { makeBaseNPMConfig, makeNPMConfigVariants } from '../../rollup/index.js'; +import { makeBaseNPMConfig, makeNPMConfigVariants } from '@sentry-internal/rollup-utils'; const variants = makeNPMConfigVariants( makeBaseNPMConfig({ diff --git a/packages/browser/package.json b/packages/browser/package.json index f47e4c89b373..4f5b3a0876dc 100644 --- a/packages/browser/package.json +++ b/packages/browser/package.json @@ -57,16 +57,16 @@ "build": "run-p build:transpile build:bundle build:types", "build:dev": "yarn build", "build:bundle": "run-p build:bundle:es5 build:bundle:es6", - "build:bundle:es5": "JS_VERSION=es5 rollup -c rollup.bundle.config.js", - "build:bundle:es6": "JS_VERSION=es6 rollup -c rollup.bundle.config.js", - "build:transpile": "rollup -c rollup.npm.config.js", + "build:bundle:es5": "JS_VERSION=es5 rollup -c rollup.bundle.config.mjs", + "build:bundle:es6": "JS_VERSION=es6 rollup -c rollup.bundle.config.mjs", + "build:transpile": "rollup -c rollup.npm.config.mjs", "build:types": "run-s build:types:core build:types:downlevel", "build:types:core": "tsc -p tsconfig.types.json", "build:types:downlevel": "yarn downlevel-dts build/npm/types build/npm/types-ts3.8 --to ts3.8", "build:watch": "run-p build:transpile:watch build:bundle:watch build:types:watch", "build:dev:watch": "run-p build:transpile:watch build:types:watch", - "build:bundle:watch": "rollup -c rollup.bundle.config.js --watch", - "build:transpile:watch": "rollup -c rollup.npm.config.js --watch", + "build:bundle:watch": "rollup -c rollup.bundle.config.mjs --watch", + "build:transpile:watch": "rollup -c rollup.npm.config.mjs --watch", "build:types:watch": "tsc -p tsconfig.types.json --watch", "build:tarball": "ts-node ../../scripts/prepack.ts --bundles && npm pack ./build/npm", "circularDepCheck": "madge --circular src/index.ts", diff --git a/packages/browser/rollup.bundle.config.js b/packages/browser/rollup.bundle.config.mjs similarity index 98% rename from packages/browser/rollup.bundle.config.js rename to packages/browser/rollup.bundle.config.mjs index 9ebe2bd8b5aa..c64a88931a33 100644 --- a/packages/browser/rollup.bundle.config.js +++ b/packages/browser/rollup.bundle.config.mjs @@ -1,4 +1,4 @@ -import { makeBaseBundleConfig, makeBundleConfigVariants } from '../../rollup/index.js'; +import { makeBaseBundleConfig, makeBundleConfigVariants } from '@sentry-internal/rollup-utils'; const builds = []; diff --git a/packages/integrations/rollup.npm.config.js b/packages/browser/rollup.npm.config.mjs similarity index 64% rename from packages/integrations/rollup.npm.config.js rename to packages/browser/rollup.npm.config.mjs index 4ffa8b9396d8..6d09adefc859 100644 --- a/packages/integrations/rollup.npm.config.js +++ b/packages/browser/rollup.npm.config.mjs @@ -1,4 +1,4 @@ -import { makeBaseNPMConfig, makeNPMConfigVariants } from '../../rollup/index.js'; +import { makeBaseNPMConfig, makeNPMConfigVariants } from '@sentry-internal/rollup-utils'; export default makeNPMConfigVariants( makeBaseNPMConfig({ diff --git a/packages/browser/test/integration/debugging.md b/packages/browser/test/integration/debugging.md index f97abe998aed..f25d70d788ae 100644 --- a/packages/browser/test/integration/debugging.md +++ b/packages/browser/test/integration/debugging.md @@ -15,7 +15,7 @@ These tests are hard to debug, because the testing system is somewhat complex, s - Repo-level `rollup/bundleHelpers.js`: - Comment out all bundle variants except whichever one `run.js` is turning into `artifacts/sdk.js`. -- Browser-package-level `rollup.bundle.config.js`: +- Browser-package-level `rollup.bundle.config.mjs`: - Build only one of `es5` and `es6`. - Run `build:bundle:watch` in a separate terminal tab, so that when you add `console.log`s to the SDK, they get picked up. diff --git a/packages/bun/package.json b/packages/bun/package.json index 0d7bb7b19edb..2c227464c197 100644 --- a/packages/bun/package.json +++ b/packages/bun/package.json @@ -34,13 +34,13 @@ "scripts": { "build": "run-p build:transpile build:types", "build:dev": "yarn build", - "build:transpile": "rollup -c rollup.npm.config.js", + "build:transpile": "rollup -c rollup.npm.config.mjs", "build:types": "run-s build:types:core build:types:downlevel", "build:types:core": "tsc -p tsconfig.types.json", "build:types:downlevel": "yarn downlevel-dts build/types build/types-ts3.8 --to ts3.8", "build:watch": "run-p build:transpile:watch build:types:watch", "build:dev:watch": "yarn build:watch", - "build:transpile:watch": "rollup -c rollup.npm.config.js --watch", + "build:transpile:watch": "rollup -c rollup.npm.config.mjs --watch", "build:types:watch": "tsc -p tsconfig.types.json --watch", "build:tarball": "ts-node ../../scripts/prepack.ts && npm pack ./build", "circularDepCheck": "madge --circular src/index.ts", diff --git a/packages/bun/rollup.npm.config.js b/packages/bun/rollup.npm.config.mjs similarity index 62% rename from packages/bun/rollup.npm.config.js rename to packages/bun/rollup.npm.config.mjs index ebbeb7063089..19f01d8cb3f8 100644 --- a/packages/bun/rollup.npm.config.js +++ b/packages/bun/rollup.npm.config.mjs @@ -1,4 +1,4 @@ -import { makeBaseNPMConfig, makeNPMConfigVariants } from '../../rollup/index.js'; +import { makeBaseNPMConfig, makeNPMConfigVariants } from '@sentry-internal/rollup-utils'; const config = makeNPMConfigVariants(makeBaseNPMConfig()); diff --git a/packages/core/package.json b/packages/core/package.json index 45d214a9933a..9b479f86857e 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -29,13 +29,13 @@ "scripts": { "build": "run-p build:transpile build:types", "build:dev": "yarn build", - "build:transpile": "rollup -c rollup.npm.config.js", + "build:transpile": "rollup -c rollup.npm.config.mjs", "build:types": "run-s build:types:core build:types:downlevel", "build:types:core": "tsc -p tsconfig.types.json", "build:types:downlevel": "yarn downlevel-dts build/types build/types-ts3.8 --to ts3.8", "build:watch": "run-p build:transpile:watch build:types:watch", "build:dev:watch": "yarn build:watch", - "build:transpile:watch": "rollup -c rollup.npm.config.js --watch", + "build:transpile:watch": "rollup -c rollup.npm.config.mjs --watch", "build:types:watch": "tsc -p tsconfig.types.json --watch", "build:tarball": "ts-node ../../scripts/prepack.ts && npm pack ./build", "circularDepCheck": "madge --circular src/index.ts", diff --git a/packages/core/rollup.npm.config.js b/packages/core/rollup.npm.config.js deleted file mode 100644 index 5a62b528ef44..000000000000 --- a/packages/core/rollup.npm.config.js +++ /dev/null @@ -1,3 +0,0 @@ -import { makeBaseNPMConfig, makeNPMConfigVariants } from '../../rollup/index.js'; - -export default makeNPMConfigVariants(makeBaseNPMConfig()); diff --git a/packages/core/rollup.npm.config.mjs b/packages/core/rollup.npm.config.mjs new file mode 100644 index 000000000000..84a06f2fb64a --- /dev/null +++ b/packages/core/rollup.npm.config.mjs @@ -0,0 +1,3 @@ +import { makeBaseNPMConfig, makeNPMConfigVariants } from '@sentry-internal/rollup-utils'; + +export default makeNPMConfigVariants(makeBaseNPMConfig()); diff --git a/packages/deno/package.json b/packages/deno/package.json index e879bd7e96e5..a9a4be67d57e 100644 --- a/packages/deno/package.json +++ b/packages/deno/package.json @@ -34,7 +34,7 @@ "build:transpile": "yarn deno-types && rollup -c rollup.config.js", "build:types": "run-s deno-types build:types:tsc build:types:bundle", "build:types:tsc": "tsc -p tsconfig.types.json", - "build:types:bundle": "rollup -c rollup.types.config.js", + "build:types:bundle": "rollup -c rollup.types.config.mjs", "build:tarball": "ts-node ../../scripts/prepack.ts && npm pack ./build", "circularDepCheck": "madge --circular src/index.ts", "clean": "rimraf build build-types build-test coverage", @@ -45,7 +45,7 @@ "install:deno": "node ./scripts/install-deno.mjs", "pretest": "run-s deno-types test:build", "test": "run-s install:deno test:types test:unit", - "test:build": "tsc -p tsconfig.test.types.json && rollup -c rollup.test.config.js", + "test:build": "tsc -p tsconfig.test.types.json && rollup -c rollup.test.config.mjs", "test:types": "deno check ./build/index.mjs", "test:unit": "deno test --allow-read --allow-run", "test:unit:update": "deno test --allow-read --allow-write --allow-run -- --update", diff --git a/packages/deno/rollup.test.config.js b/packages/deno/rollup.test.config.mjs similarity index 100% rename from packages/deno/rollup.test.config.js rename to packages/deno/rollup.test.config.mjs diff --git a/packages/deno/rollup.types.config.js b/packages/deno/rollup.types.config.mjs similarity index 100% rename from packages/deno/rollup.types.config.js rename to packages/deno/rollup.types.config.mjs diff --git a/packages/eslint-config-sdk/src/base.js b/packages/eslint-config-sdk/src/base.js index a76af21e7a51..70f54626fbc7 100644 --- a/packages/eslint-config-sdk/src/base.js +++ b/packages/eslint-config-sdk/src/base.js @@ -204,7 +204,7 @@ module.exports = { }, { // Configuration for config files like webpack/rollup - files: ['*.config.js'], + files: ['*.config.js', '*.config.mjs'], parserOptions: { sourceType: 'module', ecmaVersion: 2018, diff --git a/packages/feedback/package.json b/packages/feedback/package.json index 6613d5af34ec..e15b1875ec1a 100644 --- a/packages/feedback/package.json +++ b/packages/feedback/package.json @@ -29,8 +29,8 @@ }, "scripts": { "build": "run-p build:transpile build:types build:bundle", - "build:transpile": "rollup -c rollup.npm.config.js", - "build:bundle": "rollup -c rollup.bundle.config.js", + "build:transpile": "rollup -c rollup.npm.config.mjs", + "build:bundle": "rollup -c rollup.bundle.config.mjs", "build:dev": "run-p build:transpile build:types", "build:types": "run-s build:types:core build:types:downlevel", "build:types:core": "tsc -p tsconfig.types.json", diff --git a/packages/feedback/rollup.bundle.config.js b/packages/feedback/rollup.bundle.config.mjs similarity index 91% rename from packages/feedback/rollup.bundle.config.js rename to packages/feedback/rollup.bundle.config.mjs index 185b38249dc0..3a9404947667 100644 --- a/packages/feedback/rollup.bundle.config.js +++ b/packages/feedback/rollup.bundle.config.mjs @@ -1,4 +1,4 @@ -import { makeBaseBundleConfig, makeBundleConfigVariants } from '../../rollup/index.js'; +import { makeBaseBundleConfig, makeBundleConfigVariants } from '@sentry-internal/rollup-utils'; const baseBundleConfig = makeBaseBundleConfig({ bundleType: 'addon', diff --git a/packages/feedback/rollup.npm.config.js b/packages/feedback/rollup.npm.config.mjs similarity index 81% rename from packages/feedback/rollup.npm.config.js rename to packages/feedback/rollup.npm.config.mjs index e823e7b18863..5a1800f23b08 100644 --- a/packages/feedback/rollup.npm.config.js +++ b/packages/feedback/rollup.npm.config.mjs @@ -1,4 +1,4 @@ -import { makeBaseNPMConfig, makeNPMConfigVariants } from '../../rollup/index'; +import { makeBaseNPMConfig, makeNPMConfigVariants } from '@sentry-internal/rollup-utils'; export default makeNPMConfigVariants( makeBaseNPMConfig({ diff --git a/packages/gatsby/package.json b/packages/gatsby/package.json index 47e54ae5b6d2..c2c99396c6e1 100644 --- a/packages/gatsby/package.json +++ b/packages/gatsby/package.json @@ -46,13 +46,13 @@ "build:dev": "yarn build", "build:plugin": "tsc -p tsconfig.plugin.json", "build:transpile": "run-p build:rollup build:plugin", - "build:rollup": "rollup -c rollup.npm.config.js", + "build:rollup": "rollup -c rollup.npm.config.mjs", "build:types": "run-s build:types:core build:types:downlevel", "build:types:core": "tsc -p tsconfig.types.json", "build:types:downlevel": "yarn downlevel-dts build/types build/types-ts3.8 --to ts3.8", "build:watch": "run-p build:transpile:watch build:types:watch", "build:dev:watch": "yarn build:watch", - "build:transpile:watch": "rollup -c rollup.npm.config.js --watch", + "build:transpile:watch": "rollup -c rollup.npm.config.mjs --watch", "build:types:watch": "tsc -p tsconfig.types.json --watch", "build:tarball": "ts-node ../../scripts/prepack.ts && npm pack ./build", "circularDepCheck": "madge --circular src/index.ts", diff --git a/packages/gatsby/rollup.npm.config.js b/packages/gatsby/rollup.npm.config.js deleted file mode 100644 index 5a62b528ef44..000000000000 --- a/packages/gatsby/rollup.npm.config.js +++ /dev/null @@ -1,3 +0,0 @@ -import { makeBaseNPMConfig, makeNPMConfigVariants } from '../../rollup/index.js'; - -export default makeNPMConfigVariants(makeBaseNPMConfig()); diff --git a/packages/gatsby/rollup.npm.config.mjs b/packages/gatsby/rollup.npm.config.mjs new file mode 100644 index 000000000000..84a06f2fb64a --- /dev/null +++ b/packages/gatsby/rollup.npm.config.mjs @@ -0,0 +1,3 @@ +import { makeBaseNPMConfig, makeNPMConfigVariants } from '@sentry-internal/rollup-utils'; + +export default makeNPMConfigVariants(makeBaseNPMConfig()); diff --git a/packages/hub/package.json b/packages/hub/package.json index 75c66ff8d3d8..2fa2b52951f0 100644 --- a/packages/hub/package.json +++ b/packages/hub/package.json @@ -30,13 +30,13 @@ "scripts": { "build": "run-p build:transpile build:types", "build:dev": "yarn build", - "build:transpile": "rollup -c rollup.npm.config.js", + "build:transpile": "rollup -c rollup.npm.config.mjs", "build:types": "run-s build:types:core build:types:downlevel", "build:types:core": "tsc -p tsconfig.types.json", "build:types:downlevel": "yarn downlevel-dts build/types build/types-ts3.8 --to ts3.8", "build:watch": "run-p build:transpile:watch build:types:watch", "build:dev:watch": "yarn build:watch", - "build:transpile:watch": "rollup -c rollup.npm.config.js --watch", + "build:transpile:watch": "rollup -c rollup.npm.config.mjs --watch", "build:types:watch": "tsc -p tsconfig.types.json --watch", "build:tarball": "ts-node ../../scripts/prepack.ts && npm pack ./build", "circularDepCheck": "madge --circular src/index.ts", diff --git a/packages/hub/rollup.npm.config.js b/packages/hub/rollup.npm.config.js deleted file mode 100644 index 5a62b528ef44..000000000000 --- a/packages/hub/rollup.npm.config.js +++ /dev/null @@ -1,3 +0,0 @@ -import { makeBaseNPMConfig, makeNPMConfigVariants } from '../../rollup/index.js'; - -export default makeNPMConfigVariants(makeBaseNPMConfig()); diff --git a/packages/hub/rollup.npm.config.mjs b/packages/hub/rollup.npm.config.mjs new file mode 100644 index 000000000000..84a06f2fb64a --- /dev/null +++ b/packages/hub/rollup.npm.config.mjs @@ -0,0 +1,3 @@ +import { makeBaseNPMConfig, makeNPMConfigVariants } from '@sentry-internal/rollup-utils'; + +export default makeNPMConfigVariants(makeBaseNPMConfig()); diff --git a/packages/integration-shims/package.json b/packages/integration-shims/package.json index 57d2bf660e2d..18008fcc0a31 100644 --- a/packages/integration-shims/package.json +++ b/packages/integration-shims/package.json @@ -16,7 +16,7 @@ "private": true, "scripts": { "build": "run-p build:transpile build:types", - "build:transpile": "rollup -c rollup.npm.config.js", + "build:transpile": "rollup -c rollup.npm.config.mjs", "build:types": "run-s build:types:core build:types:downlevel", "build:types:core": "tsc -p tsconfig.types.json", "build:types:downlevel": "yarn downlevel-dts build/types build/types-ts3.8 --to ts3.8", diff --git a/packages/integration-shims/rollup.npm.config.js b/packages/integration-shims/rollup.npm.config.mjs similarity index 53% rename from packages/integration-shims/rollup.npm.config.js rename to packages/integration-shims/rollup.npm.config.mjs index 2928d05abeed..601cc6837ce0 100644 --- a/packages/integration-shims/rollup.npm.config.js +++ b/packages/integration-shims/rollup.npm.config.mjs @@ -1,4 +1,4 @@ -import { makeBaseNPMConfig, makeNPMConfigVariants } from '../../rollup/index.js'; +import { makeBaseNPMConfig, makeNPMConfigVariants } from '@sentry-internal/rollup-utils'; export default makeNPMConfigVariants( makeBaseNPMConfig({ diff --git a/packages/integrations/package.json b/packages/integrations/package.json index 00df25bdb690..0989974a4b8a 100644 --- a/packages/integrations/package.json +++ b/packages/integrations/package.json @@ -36,13 +36,13 @@ "build": "run-p build:transpile build:types build:bundle", "build:bundle": "ts-node scripts/buildBundles.ts --parallel", "build:dev": "run-p build:transpile build:types", - "build:transpile": "rollup -c rollup.npm.config.js", + "build:transpile": "rollup -c rollup.npm.config.mjs", "build:types": "run-s build:types:core build:types:downlevel", "build:types:core": "tsc -p tsconfig.types.json", "build:types:downlevel": "yarn downlevel-dts build/npm/types build/npm/types-ts3.8 --to ts3.8", "build:watch": "run-p build:transpile:watch build:types:watch", "build:dev:watch": "yarn build:watch", - "build:transpile:watch": "rollup -c rollup.npm.config.js --watch", + "build:transpile:watch": "rollup -c rollup.npm.config.mjs --watch", "build:types:watch": "tsc -p tsconfig.types.json --watch", "build:tarball": "ts-node ../../scripts/prepack.ts --bundles && npm pack ./build/npm", "circularDepCheck": "madge --circular src/index.ts", diff --git a/packages/integrations/rollup.bundle.config.js b/packages/integrations/rollup.bundle.config.mjs similarity index 95% rename from packages/integrations/rollup.bundle.config.js rename to packages/integrations/rollup.bundle.config.mjs index 7c4d2d15e2a6..366585b8abe3 100644 --- a/packages/integrations/rollup.bundle.config.js +++ b/packages/integrations/rollup.bundle.config.mjs @@ -1,6 +1,6 @@ import commonjs from '@rollup/plugin-commonjs'; -import { insertAt, makeBaseBundleConfig, makeBundleConfigVariants } from '../../rollup/index.js'; +import { insertAt, makeBaseBundleConfig, makeBundleConfigVariants } from '@sentry-internal/rollup-utils'; const builds = []; diff --git a/packages/tracing/rollup.npm.config.js b/packages/integrations/rollup.npm.config.mjs similarity index 64% rename from packages/tracing/rollup.npm.config.js rename to packages/integrations/rollup.npm.config.mjs index 4ffa8b9396d8..6d09adefc859 100644 --- a/packages/tracing/rollup.npm.config.js +++ b/packages/integrations/rollup.npm.config.mjs @@ -1,4 +1,4 @@ -import { makeBaseNPMConfig, makeNPMConfigVariants } from '../../rollup/index.js'; +import { makeBaseNPMConfig, makeNPMConfigVariants } from '@sentry-internal/rollup-utils'; export default makeNPMConfigVariants( makeBaseNPMConfig({ diff --git a/packages/integrations/scripts/buildBundles.ts b/packages/integrations/scripts/buildBundles.ts index c3c61ed66ef3..d056d5a523e2 100644 --- a/packages/integrations/scripts/buildBundles.ts +++ b/packages/integrations/scripts/buildBundles.ts @@ -16,7 +16,7 @@ function getIntegrations(): string[] { /** Builds a bundle for a specific integration and JavaScript ES version */ async function buildBundle(integration: string, jsVersion: string): Promise { return new Promise((resolve, reject) => { - const child = spawn('yarn', ['--silent', 'rollup', '--config', 'rollup.bundle.config.js'], { + const child = spawn('yarn', ['--silent', 'rollup', '--config', 'rollup.bundle.config.mjs'], { env: { ...process.env, INTEGRATION_FILE: integration, JS_VERSION: jsVersion }, }); diff --git a/packages/nextjs/rollup.npm.config.js b/packages/nextjs/rollup.npm.config.mjs similarity index 96% rename from packages/nextjs/rollup.npm.config.js rename to packages/nextjs/rollup.npm.config.mjs index e033fd6f90c1..39b79c9593b2 100644 --- a/packages/nextjs/rollup.npm.config.js +++ b/packages/nextjs/rollup.npm.config.mjs @@ -1,4 +1,4 @@ -import { makeBaseNPMConfig, makeNPMConfigVariants } from '../../rollup/index.js'; +import { makeBaseNPMConfig, makeNPMConfigVariants } from '@sentry-internal/rollup-utils'; export default [ ...makeNPMConfigVariants( diff --git a/packages/nextjs/scripts/buildRollup.ts b/packages/nextjs/scripts/buildRollup.ts index 0bf01ba51e4c..d273146b872d 100644 --- a/packages/nextjs/scripts/buildRollup.ts +++ b/packages/nextjs/scripts/buildRollup.ts @@ -10,7 +10,7 @@ function run(cmd: string, options?: childProcess.ExecSyncOptions): string | Buff return childProcess.execSync(cmd, { stdio: 'inherit', ...options }); } -run('yarn rollup -c rollup.npm.config.js'); +run('yarn rollup -c rollup.npm.config.mjs'); // Regardless of whether nextjs is using the CJS or ESM version of our SDK, we want the code from our templates to be in // ESM (since we'll be adding it onto page files which are themselves written in ESM), so copy the ESM versions of the diff --git a/packages/node-experimental/package.json b/packages/node-experimental/package.json index b68e8e730287..39f4c5955ec8 100644 --- a/packages/node-experimental/package.json +++ b/packages/node-experimental/package.json @@ -54,13 +54,13 @@ "scripts": { "build": "run-p build:transpile build:types", "build:dev": "yarn build", - "build:transpile": "rollup -c rollup.npm.config.js", + "build:transpile": "rollup -c rollup.npm.config.mjs", "build:types": "run-s build:types:core build:types:downlevel", "build:types:core": "tsc -p tsconfig.types.json", "build:types:downlevel": "yarn downlevel-dts build/types build/types-ts3.8 --to ts3.8", "build:watch": "run-p build:transpile:watch build:types:watch", "build:dev:watch": "yarn build:watch", - "build:transpile:watch": "rollup -c rollup.npm.config.js --watch", + "build:transpile:watch": "rollup -c rollup.npm.config.mjs --watch", "build:types:watch": "tsc -p tsconfig.types.json --watch", "build:tarball": "ts-node ../../scripts/prepack.ts && npm pack ./build", "circularDepCheck": "madge --circular src/index.ts", diff --git a/packages/node-experimental/rollup.npm.config.js b/packages/node-experimental/rollup.npm.config.js deleted file mode 100644 index 5a62b528ef44..000000000000 --- a/packages/node-experimental/rollup.npm.config.js +++ /dev/null @@ -1,3 +0,0 @@ -import { makeBaseNPMConfig, makeNPMConfigVariants } from '../../rollup/index.js'; - -export default makeNPMConfigVariants(makeBaseNPMConfig()); diff --git a/packages/node-experimental/rollup.npm.config.mjs b/packages/node-experimental/rollup.npm.config.mjs new file mode 100644 index 000000000000..84a06f2fb64a --- /dev/null +++ b/packages/node-experimental/rollup.npm.config.mjs @@ -0,0 +1,3 @@ +import { makeBaseNPMConfig, makeNPMConfigVariants } from '@sentry-internal/rollup-utils'; + +export default makeNPMConfigVariants(makeBaseNPMConfig()); diff --git a/packages/node/package.json b/packages/node/package.json index 08cc8f08aafa..a2725d727a5e 100644 --- a/packages/node/package.json +++ b/packages/node/package.json @@ -39,15 +39,15 @@ "undici": "^5.21.0" }, "scripts": { - "build": "run-p build:transpile build:types", + "build": "run-s build:transpile build:types", "build:dev": "yarn build", - "build:transpile": "rollup -c rollup.npm.config.js", + "build:transpile": "rollup -c rollup.npm.config.mjs", "build:types": "run-s build:types:core build:types:downlevel", "build:types:core": "tsc -p tsconfig.types.json", "build:types:downlevel": "yarn downlevel-dts build/types build/types-ts3.8 --to ts3.8", "build:watch": "run-p build:transpile:watch build:types:watch", "build:dev:watch": "yarn build:watch", - "build:transpile:watch": "rollup -c rollup.npm.config.js --watch", + "build:transpile:watch": "rollup -c rollup.npm.config.mjs --watch", "build:types:watch": "tsc -p tsconfig.types.json --watch", "build:tarball": "ts-node ../../scripts/prepack.ts && npm pack ./build", "circularDepCheck": "madge --circular src/index.ts", diff --git a/packages/node/rollup.npm.config.js b/packages/node/rollup.npm.config.js deleted file mode 100644 index 5a62b528ef44..000000000000 --- a/packages/node/rollup.npm.config.js +++ /dev/null @@ -1,3 +0,0 @@ -import { makeBaseNPMConfig, makeNPMConfigVariants } from '../../rollup/index.js'; - -export default makeNPMConfigVariants(makeBaseNPMConfig()); diff --git a/packages/node/rollup.npm.config.mjs b/packages/node/rollup.npm.config.mjs new file mode 100644 index 000000000000..84a06f2fb64a --- /dev/null +++ b/packages/node/rollup.npm.config.mjs @@ -0,0 +1,3 @@ +import { makeBaseNPMConfig, makeNPMConfigVariants } from '@sentry-internal/rollup-utils'; + +export default makeNPMConfigVariants(makeBaseNPMConfig()); diff --git a/packages/opentelemetry-node/package.json b/packages/opentelemetry-node/package.json index 2f941536e63e..95056c2db144 100644 --- a/packages/opentelemetry-node/package.json +++ b/packages/opentelemetry-node/package.json @@ -44,13 +44,13 @@ "scripts": { "build": "run-p build:transpile build:types", "build:dev": "yarn build", - "build:transpile": "rollup -c rollup.npm.config.js", + "build:transpile": "rollup -c rollup.npm.config.mjs", "build:types": "run-s build:types:core build:types:downlevel", "build:types:core": "tsc -p tsconfig.types.json", "build:types:downlevel": "yarn downlevel-dts build/types build/types-ts3.8 --to ts3.8", "build:watch": "run-p build:transpile:watch build:types:watch", "build:dev:watch": "yarn build:watch", - "build:transpile:watch": "rollup -c rollup.npm.config.js --watch", + "build:transpile:watch": "rollup -c rollup.npm.config.mjs --watch", "build:types:watch": "tsc -p tsconfig.types.json --watch", "build:tarball": "ts-node ../../scripts/prepack.ts && npm pack ./build", "circularDepCheck": "madge --circular src/index.ts", diff --git a/packages/opentelemetry-node/rollup.npm.config.js b/packages/opentelemetry-node/rollup.npm.config.js deleted file mode 100644 index 5a62b528ef44..000000000000 --- a/packages/opentelemetry-node/rollup.npm.config.js +++ /dev/null @@ -1,3 +0,0 @@ -import { makeBaseNPMConfig, makeNPMConfigVariants } from '../../rollup/index.js'; - -export default makeNPMConfigVariants(makeBaseNPMConfig()); diff --git a/packages/opentelemetry-node/rollup.npm.config.mjs b/packages/opentelemetry-node/rollup.npm.config.mjs new file mode 100644 index 000000000000..84a06f2fb64a --- /dev/null +++ b/packages/opentelemetry-node/rollup.npm.config.mjs @@ -0,0 +1,3 @@ +import { makeBaseNPMConfig, makeNPMConfigVariants } from '@sentry-internal/rollup-utils'; + +export default makeNPMConfigVariants(makeBaseNPMConfig()); diff --git a/packages/opentelemetry/package.json b/packages/opentelemetry/package.json index 4ca5eab7a721..29dbec1784cb 100644 --- a/packages/opentelemetry/package.json +++ b/packages/opentelemetry/package.json @@ -44,13 +44,13 @@ "scripts": { "build": "run-p build:transpile build:types", "build:dev": "yarn build", - "build:transpile": "rollup -c rollup.npm.config.js", + "build:transpile": "rollup -c rollup.npm.config.mjs", "build:types": "run-s build:types:core build:types:downlevel", "build:types:core": "tsc -p tsconfig.types.json", "build:types:downlevel": "yarn downlevel-dts build/types build/types-ts3.8 --to ts3.8", "build:watch": "run-p build:transpile:watch build:types:watch", "build:dev:watch": "yarn build:watch", - "build:transpile:watch": "rollup -c rollup.npm.config.js --watch", + "build:transpile:watch": "rollup -c rollup.npm.config.mjs --watch", "build:types:watch": "tsc -p tsconfig.types.json --watch", "build:tarball": "ts-node ../../scripts/prepack.ts && npm pack ./build", "circularDepCheck": "madge --circular src/index.ts", diff --git a/packages/opentelemetry/rollup.npm.config.js b/packages/opentelemetry/rollup.npm.config.js deleted file mode 100644 index 5a62b528ef44..000000000000 --- a/packages/opentelemetry/rollup.npm.config.js +++ /dev/null @@ -1,3 +0,0 @@ -import { makeBaseNPMConfig, makeNPMConfigVariants } from '../../rollup/index.js'; - -export default makeNPMConfigVariants(makeBaseNPMConfig()); diff --git a/packages/opentelemetry/rollup.npm.config.mjs b/packages/opentelemetry/rollup.npm.config.mjs new file mode 100644 index 000000000000..84a06f2fb64a --- /dev/null +++ b/packages/opentelemetry/rollup.npm.config.mjs @@ -0,0 +1,3 @@ +import { makeBaseNPMConfig, makeNPMConfigVariants } from '@sentry-internal/rollup-utils'; + +export default makeNPMConfigVariants(makeBaseNPMConfig()); diff --git a/packages/react/package.json b/packages/react/package.json index 0611bc0c5de0..586352e98697 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -59,13 +59,13 @@ "scripts": { "build": "run-p build:transpile build:types", "build:dev": "yarn build", - "build:transpile": "rollup -c rollup.npm.config.js", + "build:transpile": "rollup -c rollup.npm.config.mjs", "build:types": "run-s build:types:core build:types:downlevel", "build:types:core": "tsc -p tsconfig.types.json", "build:types:downlevel": "yarn downlevel-dts build/types build/types-ts3.8 --to ts3.8", "build:watch": "run-p build:transpile:watch build:types:watch", "build:dev:watch": "yarn build:watch", - "build:transpile:watch": "rollup -c rollup.npm.config.js --watch", + "build:transpile:watch": "rollup -c rollup.npm.config.mjs --watch", "build:types:watch": "tsc -p tsconfig.types.json --watch", "build:tarball": "ts-node ../../scripts/prepack.ts && npm pack ./build", "circularDepCheck": "madge --circular src/index.ts", diff --git a/packages/react/rollup.npm.config.js b/packages/react/rollup.npm.config.mjs similarity index 51% rename from packages/react/rollup.npm.config.js rename to packages/react/rollup.npm.config.mjs index ebe81bb263c6..d87739380bd6 100644 --- a/packages/react/rollup.npm.config.js +++ b/packages/react/rollup.npm.config.mjs @@ -1,4 +1,4 @@ -import { makeBaseNPMConfig, makeNPMConfigVariants } from '../../rollup/index.js'; +import { makeBaseNPMConfig, makeNPMConfigVariants } from '@sentry-internal/rollup-utils'; export default makeNPMConfigVariants( makeBaseNPMConfig({ diff --git a/packages/remix/package.json b/packages/remix/package.json index 030bc03e0053..8ab0d273d0b0 100644 --- a/packages/remix/package.json +++ b/packages/remix/package.json @@ -49,13 +49,13 @@ "scripts": { "build": "run-p build:transpile build:types", "build:dev": "yarn build", - "build:transpile": "rollup -c rollup.npm.config.js", + "build:transpile": "rollup -c rollup.npm.config.mjs", "build:types": "run-s build:types:core build:types:downlevel", "build:types:core": "tsc -p tsconfig.types.json", "build:types:downlevel": "yarn downlevel-dts build/types build/types-ts3.8 --to ts3.8", "build:watch": "run-p build:transpile:watch build:types:watch", "build:dev:watch": "yarn build:watch", - "build:transpile:watch": "rollup -c rollup.npm.config.js --watch", + "build:transpile:watch": "rollup -c rollup.npm.config.mjs --watch", "build:types:watch": "tsc -p tsconfig.types.json --watch", "build:tarball": "ts-node ../../scripts/prepack.ts && npm pack ./build", "circularDepCheck": "madge --circular src/index.server.ts", diff --git a/packages/remix/rollup.npm.config.js b/packages/remix/rollup.npm.config.mjs similarity index 80% rename from packages/remix/rollup.npm.config.js rename to packages/remix/rollup.npm.config.mjs index dc51f24c7bb2..c588c260c703 100644 --- a/packages/remix/rollup.npm.config.js +++ b/packages/remix/rollup.npm.config.mjs @@ -1,4 +1,4 @@ -import { makeBaseNPMConfig, makeNPMConfigVariants } from '../../rollup/index.js'; +import { makeBaseNPMConfig, makeNPMConfigVariants } from '@sentry-internal/rollup-utils'; export default makeNPMConfigVariants( makeBaseNPMConfig({ diff --git a/packages/replay-worker/package.json b/packages/replay-worker/package.json index 06e806f070e9..ea42514386f5 100644 --- a/packages/replay-worker/package.json +++ b/packages/replay-worker/package.json @@ -16,8 +16,8 @@ "private": true, "scripts": { "build": "run-p build:transpile build:types", - "build:transpile": "rollup -c rollup.worker.config.js", - "build:examples": "rollup -c rollup.examples.config.js", + "build:transpile": "rollup -c rollup.worker.config.mjs", + "build:examples": "rollup -c rollup.examples.config.mjs", "build:types": "run-s build:types:core build:types:downlevel", "build:types:core": "tsc -p tsconfig.types.json", "build:types:downlevel": "yarn downlevel-dts build/npm/types build/npm/types-ts3.8 --to ts3.8", diff --git a/packages/replay-worker/rollup.worker.config.js b/packages/replay-worker/rollup.worker.config.mjs similarity index 100% rename from packages/replay-worker/rollup.worker.config.js rename to packages/replay-worker/rollup.worker.config.mjs diff --git a/packages/replay/package.json b/packages/replay/package.json index 177064e08084..36dd377bbd14 100644 --- a/packages/replay/package.json +++ b/packages/replay/package.json @@ -15,8 +15,8 @@ "sideEffects": false, "scripts": { "build": "run-p build:transpile build:types build:bundle", - "build:transpile": "rollup -c rollup.npm.config.js", - "build:bundle": "rollup -c rollup.bundle.config.js", + "build:transpile": "rollup -c rollup.npm.config.mjs", + "build:bundle": "rollup -c rollup.bundle.config.mjs", "build:dev": "run-p build:transpile build:types", "build:types": "run-s build:types:core build:types:downlevel", "build:types:core": "tsc -p tsconfig.types.json", diff --git a/packages/replay/rollup.bundle.config.js b/packages/replay/rollup.bundle.config.mjs similarity index 91% rename from packages/replay/rollup.bundle.config.js rename to packages/replay/rollup.bundle.config.mjs index 75f240f85822..a209b8d41af4 100644 --- a/packages/replay/rollup.bundle.config.js +++ b/packages/replay/rollup.bundle.config.mjs @@ -1,4 +1,4 @@ -import { makeBaseBundleConfig, makeBundleConfigVariants } from '../../rollup/index.js'; +import { makeBaseBundleConfig, makeBundleConfigVariants } from '@sentry-internal/rollup-utils'; const baseBundleConfig = makeBaseBundleConfig({ bundleType: 'addon', diff --git a/packages/replay/rollup.npm.config.js b/packages/replay/rollup.npm.config.mjs similarity index 81% rename from packages/replay/rollup.npm.config.js rename to packages/replay/rollup.npm.config.mjs index c3c2db72bebf..8c50a33f0afb 100644 --- a/packages/replay/rollup.npm.config.js +++ b/packages/replay/rollup.npm.config.mjs @@ -1,4 +1,4 @@ -import { makeBaseNPMConfig, makeNPMConfigVariants } from '../../rollup/index'; +import { makeBaseNPMConfig, makeNPMConfigVariants } from '@sentry-internal/rollup-utils'; export default makeNPMConfigVariants( makeBaseNPMConfig({ diff --git a/packages/rollup-utils/README.md b/packages/rollup-utils/README.md new file mode 100644 index 000000000000..2d79f3eabeec --- /dev/null +++ b/packages/rollup-utils/README.md @@ -0,0 +1,5 @@ +# The `rollup-utils` Package + +This is a small utility packages for all the Rollup configurations we have in this project. It contains helpers to create standardized configs, custom rollup plugins, and other things that might have to do with the build process like polyfill snippets. + +This package will not be published and is only intended to be used inside this repository. diff --git a/rollup/bundleHelpers.js b/packages/rollup-utils/bundleHelpers.mjs similarity index 98% rename from rollup/bundleHelpers.js rename to packages/rollup-utils/bundleHelpers.mjs index cd329dfb31d2..93a84670a6ff 100644 --- a/rollup/bundleHelpers.js +++ b/packages/rollup-utils/bundleHelpers.mjs @@ -19,8 +19,8 @@ import { makeSucrasePlugin, makeTSPlugin, makeTerserPlugin, -} from './plugins/index.js'; -import { mergePlugins } from './utils'; +} from './plugins/index.mjs'; +import { mergePlugins } from './utils.mjs'; const BUNDLE_VARIANTS = ['.js', '.min.js', '.debug.min.js']; diff --git a/packages/rollup-utils/index.mjs b/packages/rollup-utils/index.mjs new file mode 100644 index 000000000000..2d8c9a2150bc --- /dev/null +++ b/packages/rollup-utils/index.mjs @@ -0,0 +1,7 @@ +// TODO Is this necessary? +import * as plugins from './plugins/index.mjs'; +export { plugins }; + +export * from './bundleHelpers.mjs'; +export * from './npmHelpers.mjs'; +export { insertAt } from './utils.mjs'; diff --git a/rollup/npmHelpers.js b/packages/rollup-utils/npmHelpers.mjs similarity index 94% rename from rollup/npmHelpers.js rename to packages/rollup-utils/npmHelpers.mjs index 3d374a4e05f0..6085a502200f 100644 --- a/rollup/npmHelpers.js +++ b/packages/rollup-utils/npmHelpers.mjs @@ -2,6 +2,7 @@ * Rollup config docs: https://rollupjs.org/guide/en/#big-list-of-options */ +import * as fs from 'fs'; import { builtinModules } from 'module'; import * as path from 'path'; @@ -15,10 +16,10 @@ import { makeRrwebBuildPlugin, makeSetSDKSourcePlugin, makeSucrasePlugin, -} from './plugins/index.js'; -import { mergePlugins } from './utils'; +} from './plugins/index.mjs'; +import { mergePlugins } from './utils.mjs'; -const packageDotJSON = require(path.resolve(process.cwd(), './package.json')); +const packageDotJSON = JSON.parse(fs.readFileSync(path.resolve(process.cwd(), './package.json'), { encoding: 'utf8' })); export function makeBaseNPMConfig(options = {}) { const { diff --git a/packages/rollup-utils/package.json b/packages/rollup-utils/package.json new file mode 100644 index 000000000000..b44f51559d01 --- /dev/null +++ b/packages/rollup-utils/package.json @@ -0,0 +1,19 @@ +{ + "name": "@sentry-internal/rollup-utils", + "version": "7.88.0", + "description": "Rollup utilities used at Sentry for the Sentry JavaScript SDK", + "repository": "git://github.com/getsentry/sentry-javascript.git", + "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/rollup-utils", + "author": "Sentry", + "license": "MIT", + "private": true, + "main": "./index.mjs", + "dependencies": { + "acorn": "^8.7.0", + "recast": "^0.20.5" + }, + "volta": { + "extends": "../../package.json" + }, + "type": "module" +} diff --git a/rollup/plugins/bundlePlugins.js b/packages/rollup-utils/plugins/bundlePlugins.mjs similarity index 95% rename from rollup/plugins/bundlePlugins.js rename to packages/rollup-utils/plugins/bundlePlugins.mjs index 87dfffc1f0f0..66f8e8c78228 100644 --- a/rollup/plugins/bundlePlugins.js +++ b/packages/rollup-utils/plugins/bundlePlugins.mjs @@ -8,8 +8,10 @@ * Typescript plugin docs: https://github.com/rollup/plugins/tree/master/packages/typescript/#readme */ +import * as childProcess from 'child_process'; import * as fs from 'fs'; import * as path from 'path'; +import { fileURLToPath } from 'url'; import commonjs from '@rollup/plugin-commonjs'; import { nodeResolve } from '@rollup/plugin-node-resolve'; @@ -26,7 +28,7 @@ import { terser } from 'rollup-plugin-terser'; * @returns An instance of the `rollup-plugin-license` plugin */ export function makeLicensePlugin(title) { - const commitHash = require('child_process').execSync('git rev-parse --short HEAD', { encoding: 'utf-8' }).trim(); + const commitHash = childProcess.execSync('git rev-parse --short HEAD', { encoding: 'utf-8' }).trim(); const plugin = license({ banner: { @@ -42,8 +44,7 @@ export function makeLicensePlugin(title) { } export function getEs5Polyfills() { - // Note: __dirname resolves to e.g. packages/browser or packages/tracing - return fs.readFileSync(path.join(__dirname, '../../rollup/polyfills/es5.js'), 'utf-8'); + return fs.readFileSync(path.join(path.dirname(fileURLToPath(import.meta.url)), '../polyfills/es5.js'), 'utf-8'); } /** diff --git a/rollup/plugins/extractPolyfillsPlugin.js b/packages/rollup-utils/plugins/extractPolyfillsPlugin.mjs similarity index 98% rename from rollup/plugins/extractPolyfillsPlugin.js rename to packages/rollup-utils/plugins/extractPolyfillsPlugin.mjs index e036e70e9593..ad7c99094843 100644 --- a/rollup/plugins/extractPolyfillsPlugin.js +++ b/packages/rollup-utils/plugins/extractPolyfillsPlugin.mjs @@ -1,7 +1,7 @@ import * as path from 'path'; +import * as acorn from 'acorn'; import * as recast from 'recast'; -import * as acornParser from 'recast/parsers/acorn'; const POLYFILL_NAMES = new Set([ '_asyncNullishCoalesce', @@ -53,7 +53,7 @@ export function makeExtractPolyfillsPlugin() { // See https://github.com/benjamn/recast/issues/578. parser: { parse(source, options) { - return acornParser.parse(source, { + return acorn.parse(source, { ...options, // By this point in the build, everything should already have been down-compiled to whatever JS version // we're targeting. Setting this parser to `latest` just means that whatever that version is (or changes diff --git a/packages/rollup-utils/plugins/index.mjs b/packages/rollup-utils/plugins/index.mjs new file mode 100644 index 000000000000..5e40aa3508e4 --- /dev/null +++ b/packages/rollup-utils/plugins/index.mjs @@ -0,0 +1,2 @@ +export * from './bundlePlugins.mjs'; +export * from './npmPlugins.mjs'; diff --git a/rollup/plugins/npmPlugins.js b/packages/rollup-utils/plugins/npmPlugins.mjs similarity index 98% rename from rollup/plugins/npmPlugins.js rename to packages/rollup-utils/plugins/npmPlugins.mjs index b7683a1306e4..507480c1dd43 100644 --- a/rollup/plugins/npmPlugins.js +++ b/packages/rollup-utils/plugins/npmPlugins.mjs @@ -64,6 +64,7 @@ export function makeSucrasePlugin(options = {}) { export function makeDebuggerPlugin(hookName) { return { name: 'debugger-plugin', + // eslint-disable-next-line no-unused-vars [hookName]: (..._args) => { // eslint-disable-next-line no-debugger debugger; @@ -130,4 +131,4 @@ export function makeRrwebBuildPlugin({ excludeShadowDom, excludeIframe } = {}) { }); } -export { makeExtractPolyfillsPlugin } from './extractPolyfillsPlugin.js'; +export { makeExtractPolyfillsPlugin } from './extractPolyfillsPlugin.mjs'; diff --git a/rollup/polyfills/es5.js b/packages/rollup-utils/polyfills/es5.js similarity index 100% rename from rollup/polyfills/es5.js rename to packages/rollup-utils/polyfills/es5.js diff --git a/rollup/utils.js b/packages/rollup-utils/utils.mjs similarity index 100% rename from rollup/utils.js rename to packages/rollup-utils/utils.mjs diff --git a/packages/serverless/package.json b/packages/serverless/package.json index 3db9a1290d59..5ae633418dd0 100644 --- a/packages/serverless/package.json +++ b/packages/serverless/package.json @@ -46,13 +46,13 @@ "build": "run-p build:transpile build:types build:bundle", "build:bundle": "yarn ts-node scripts/buildLambdaLayer.ts", "build:dev": "run-p build:transpile build:types", - "build:transpile": "rollup -c rollup.npm.config.js", + "build:transpile": "rollup -c rollup.npm.config.mjs", "build:types": "run-s build:types:core build:types:downlevel", "build:types:core": "tsc -p tsconfig.types.json", "build:types:downlevel": "yarn downlevel-dts build/npm/types build/npm/types-ts3.8 --to ts3.8", "build:watch": "run-p build:transpile:watch build:types:watch", "build:dev:watch": "yarn build:watch", - "build:transpile:watch": "rollup -c rollup.npm.config.js --watch", + "build:transpile:watch": "rollup -c rollup.npm.config.mjs --watch", "build:types:watch": "tsc -p tsconfig.types.json --watch", "build:tarball": "ts-node ../../scripts/prepack.ts --bundles && npm pack ./build/npm", "circularDepCheck": "madge --circular src/index.ts", diff --git a/packages/serverless/rollup.aws.config.js b/packages/serverless/rollup.aws.config.mjs similarity index 97% rename from packages/serverless/rollup.aws.config.js rename to packages/serverless/rollup.aws.config.mjs index 5a32f2632f3e..5d9883a3f9f7 100644 --- a/packages/serverless/rollup.aws.config.js +++ b/packages/serverless/rollup.aws.config.mjs @@ -1,4 +1,4 @@ -import { makeBaseBundleConfig, makeBaseNPMConfig, makeBundleConfigVariants } from '../../rollup/index.js'; +import { makeBaseBundleConfig, makeBaseNPMConfig, makeBundleConfigVariants } from '@sentry-internal/rollup-utils'; export default [ // The SDK diff --git a/packages/serverless/rollup.npm.config.js b/packages/serverless/rollup.npm.config.mjs similarity index 86% rename from packages/serverless/rollup.npm.config.js rename to packages/serverless/rollup.npm.config.mjs index 4e9641d5879e..b51a3bdafdb5 100644 --- a/packages/serverless/rollup.npm.config.js +++ b/packages/serverless/rollup.npm.config.mjs @@ -1,4 +1,4 @@ -import { makeBaseNPMConfig, makeNPMConfigVariants } from '../../rollup/index.js'; +import { makeBaseNPMConfig, makeNPMConfigVariants } from '@sentry-internal/rollup-utils'; export default makeNPMConfigVariants( makeBaseNPMConfig({ diff --git a/packages/serverless/scripts/buildLambdaLayer.ts b/packages/serverless/scripts/buildLambdaLayer.ts index 540a1cab7451..9b6912898386 100644 --- a/packages/serverless/scripts/buildLambdaLayer.ts +++ b/packages/serverless/scripts/buildLambdaLayer.ts @@ -20,7 +20,7 @@ async function buildLambdaLayer(): Promise { await ensureBundleBuildPrereqs({ dependencies: ['@sentry/utils', '@sentry/hub', '@sentry/core', '@sentry/node'], }); - run('yarn rollup --config rollup.aws.config.js'); + run('yarn rollup --config rollup.aws.config.mjs'); // We build a minified bundle, but it's standing in for the regular `index.js` file listed in `package.json`'s `main` // property, so we have to rename it so it's findable. diff --git a/packages/svelte/package.json b/packages/svelte/package.json index be4bd6383e0e..5688faf899af 100644 --- a/packages/svelte/package.json +++ b/packages/svelte/package.json @@ -39,13 +39,13 @@ "scripts": { "build": "run-p build:transpile build:types", "build:dev": "yarn build", - "build:transpile": "rollup -c rollup.npm.config.js", + "build:transpile": "rollup -c rollup.npm.config.mjs", "build:types": "run-s build:types:core build:types:downlevel", "build:types:core": "tsc -p tsconfig.types.json", "build:types:downlevel": "yarn downlevel-dts build/types build/types-ts3.8 --to ts3.8", "build:watch": "run-p build:transpile:watch build:types:watch", "build:dev:watch": "yarn build:watch", - "build:transpile:watch": "rollup -c rollup.npm.config.js --watch", + "build:transpile:watch": "rollup -c rollup.npm.config.mjs --watch", "build:types:watch": "tsc -p tsconfig.types.json --watch", "build:tarball": "ts-node ../../scripts/prepack.ts && npm pack ./build", "circularDepCheck": "madge --circular src/index.ts", diff --git a/packages/svelte/rollup.npm.config.js b/packages/svelte/rollup.npm.config.mjs similarity index 69% rename from packages/svelte/rollup.npm.config.js rename to packages/svelte/rollup.npm.config.mjs index ae3c7a9a5b8a..e756611ad33a 100644 --- a/packages/svelte/rollup.npm.config.js +++ b/packages/svelte/rollup.npm.config.mjs @@ -1,4 +1,4 @@ -import { makeBaseNPMConfig, makeNPMConfigVariants } from '../../rollup/index.js'; +import { makeBaseNPMConfig, makeNPMConfigVariants } from '@sentry-internal/rollup-utils'; export default makeNPMConfigVariants( makeBaseNPMConfig({ diff --git a/packages/sveltekit/package.json b/packages/sveltekit/package.json index f3ac37df21b8..d3ac78962355 100644 --- a/packages/sveltekit/package.json +++ b/packages/sveltekit/package.json @@ -51,11 +51,11 @@ "scripts": { "build": "run-p build:transpile build:types", "build:dev": "yarn build", - "build:transpile": "rollup -c rollup.npm.config.js --bundleConfigAsCjs", + "build:transpile": "rollup -c rollup.npm.config.mjs", "build:types": "tsc -p tsconfig.types.json", "build:watch": "run-p build:transpile:watch build:types:watch", "build:dev:watch": "yarn build:watch", - "build:transpile:watch": "rollup -c rollup.npm.config.js --bundleConfigAsCjs --watch", + "build:transpile:watch": "rollup -c rollup.npm.config.mjs --watch", "build:types:watch": "tsc -p tsconfig.types.json --watch", "build:tarball": "ts-node ../../scripts/prepack.ts && npm pack ./build", "circularDepCheck": "madge --circular src/index.client.ts && madge --circular src/index.server.ts && madge --circular src/index.types.ts", diff --git a/packages/sveltekit/rollup.npm.config.js b/packages/sveltekit/rollup.npm.config.mjs similarity index 77% rename from packages/sveltekit/rollup.npm.config.js rename to packages/sveltekit/rollup.npm.config.mjs index 8e562f3d5168..b0a19e091ad8 100644 --- a/packages/sveltekit/rollup.npm.config.js +++ b/packages/sveltekit/rollup.npm.config.mjs @@ -1,4 +1,4 @@ -import { makeBaseNPMConfig, makeNPMConfigVariants } from '../../rollup/index.js'; +import { makeBaseNPMConfig, makeNPMConfigVariants } from '@sentry-internal/rollup-utils'; export default makeNPMConfigVariants( makeBaseNPMConfig({ diff --git a/packages/tracing-internal/package.json b/packages/tracing-internal/package.json index 30ca1c3f22fe..f0ed285430ba 100644 --- a/packages/tracing-internal/package.json +++ b/packages/tracing-internal/package.json @@ -33,13 +33,13 @@ "scripts": { "build": "run-p build:transpile build:types", "build:dev": "yarn build", - "build:transpile": "rollup -c rollup.npm.config.js", + "build:transpile": "rollup -c rollup.npm.config.mjs", "build:types": "run-s build:types:core build:types:downlevel", "build:types:core": "tsc -p tsconfig.types.json", "build:types:downlevel": "yarn downlevel-dts build/types build/types-ts3.8 --to ts3.8", "build:watch": "run-p build:transpile:watch build:types:watch", "build:dev:watch": "run-p build:transpile:watch build:types:watch", - "build:transpile:watch": "rollup -c rollup.npm.config.js --watch", + "build:transpile:watch": "rollup -c rollup.npm.config.mjs --watch", "build:types:watch": "tsc -p tsconfig.types.json --watch", "build:tarball": "ts-node ../../scripts/prepack.ts && npm pack ./build", "clean": "rimraf build coverage sentry-internal-tracing-*.tgz", diff --git a/packages/tracing-internal/rollup.npm.config.js b/packages/tracing-internal/rollup.npm.config.js deleted file mode 100644 index 5a62b528ef44..000000000000 --- a/packages/tracing-internal/rollup.npm.config.js +++ /dev/null @@ -1,3 +0,0 @@ -import { makeBaseNPMConfig, makeNPMConfigVariants } from '../../rollup/index.js'; - -export default makeNPMConfigVariants(makeBaseNPMConfig()); diff --git a/packages/tracing-internal/rollup.npm.config.mjs b/packages/tracing-internal/rollup.npm.config.mjs new file mode 100644 index 000000000000..84a06f2fb64a --- /dev/null +++ b/packages/tracing-internal/rollup.npm.config.mjs @@ -0,0 +1,3 @@ +import { makeBaseNPMConfig, makeNPMConfigVariants } from '@sentry-internal/rollup-utils'; + +export default makeNPMConfigVariants(makeBaseNPMConfig()); diff --git a/packages/tracing/package.json b/packages/tracing/package.json index 6f7ba1b3a778..90fa1a9cc987 100644 --- a/packages/tracing/package.json +++ b/packages/tracing/package.json @@ -36,13 +36,13 @@ "scripts": { "build": "run-p build:transpile build:types", "build:dev": "run-p build:transpile build:types", - "build:transpile": "rollup -c rollup.npm.config.js", + "build:transpile": "rollup -c rollup.npm.config.mjs", "build:types": "run-s build:types:core build:types:downlevel", "build:types:core": "tsc -p tsconfig.types.json", "build:types:downlevel": "yarn downlevel-dts build/npm/types build/npm/types-ts3.8 --to ts3.8", "build:watch": "run-p build:transpile:watch build:types:watch", "build:dev:watch": "run-p build:transpile:watch build:types:watch", - "build:transpile:watch": "rollup -c rollup.npm.config.js --watch", + "build:transpile:watch": "rollup -c rollup.npm.config.mjs --watch", "build:types:watch": "tsc -p tsconfig.types.json --watch", "build:tarball": "ts-node ../../scripts/prepack.ts --bundles && npm pack ./build/npm", "clean": "rimraf build coverage sentry-tracing-*.tgz", diff --git a/packages/browser/rollup.npm.config.js b/packages/tracing/rollup.npm.config.mjs similarity index 64% rename from packages/browser/rollup.npm.config.js rename to packages/tracing/rollup.npm.config.mjs index 4ffa8b9396d8..6d09adefc859 100644 --- a/packages/browser/rollup.npm.config.js +++ b/packages/tracing/rollup.npm.config.mjs @@ -1,4 +1,4 @@ -import { makeBaseNPMConfig, makeNPMConfigVariants } from '../../rollup/index.js'; +import { makeBaseNPMConfig, makeNPMConfigVariants } from '@sentry-internal/rollup-utils'; export default makeNPMConfigVariants( makeBaseNPMConfig({ diff --git a/packages/types/package.json b/packages/types/package.json index eaa65fdb2a79..d5ac8ba2164b 100644 --- a/packages/types/package.json +++ b/packages/types/package.json @@ -25,13 +25,13 @@ "scripts": { "build": "run-p build:transpile build:types", "build:dev": "yarn build", - "build:transpile": "rollup -c rollup.npm.config.js", + "build:transpile": "rollup -c rollup.npm.config.mjs", "build:types": "run-s build:types:core build:types:downlevel", "build:types:core": "tsc -p tsconfig.types.json", "build:types:downlevel": "yarn downlevel-dts build/types build/types-ts3.8 --to ts3.8", "build:watch": "run-p build:transpile:watch build:types:watch", "build:dev:watch": "yarn build:watch", - "build:transpile:watch": "rollup -c rollup.npm.config.js --watch", + "build:transpile:watch": "rollup -c rollup.npm.config.mjs --watch", "build:types:watch": "tsc -p tsconfig.types.json --watch", "build:tarball": "ts-node ../../scripts/prepack.ts && npm pack ./build", "clean": "rimraf build sentry-types-*.tgz", diff --git a/packages/types/rollup.npm.config.js b/packages/types/rollup.npm.config.js deleted file mode 100644 index 5a62b528ef44..000000000000 --- a/packages/types/rollup.npm.config.js +++ /dev/null @@ -1,3 +0,0 @@ -import { makeBaseNPMConfig, makeNPMConfigVariants } from '../../rollup/index.js'; - -export default makeNPMConfigVariants(makeBaseNPMConfig()); diff --git a/packages/types/rollup.npm.config.mjs b/packages/types/rollup.npm.config.mjs new file mode 100644 index 000000000000..84a06f2fb64a --- /dev/null +++ b/packages/types/rollup.npm.config.mjs @@ -0,0 +1,3 @@ +import { makeBaseNPMConfig, makeNPMConfigVariants } from '@sentry-internal/rollup-utils'; + +export default makeNPMConfigVariants(makeBaseNPMConfig()); diff --git a/packages/utils/package.json b/packages/utils/package.json index eeb9ab784ae9..940a62d01846 100644 --- a/packages/utils/package.json +++ b/packages/utils/package.json @@ -39,7 +39,7 @@ "build:types:downlevel": "yarn downlevel-dts build/types build/types-ts3.8 --to ts3.8", "build:watch": "run-p build:transpile:watch build:types:watch", "build:dev:watch": "yarn build:watch", - "build:transpile:watch": "rollup -c rollup.npm.config.js --watch", + "build:transpile:watch": "rollup -c rollup.npm.config.mjs --watch", "build:types:watch": "tsc -p tsconfig.types.json --watch", "build:tarball": "ts-node ../../scripts/prepack.ts && npm pack ./build", "circularDepCheck": "madge --circular src/index.ts", diff --git a/packages/utils/rollup.npm.config.js b/packages/utils/rollup.npm.config.js deleted file mode 100644 index 5a62b528ef44..000000000000 --- a/packages/utils/rollup.npm.config.js +++ /dev/null @@ -1,3 +0,0 @@ -import { makeBaseNPMConfig, makeNPMConfigVariants } from '../../rollup/index.js'; - -export default makeNPMConfigVariants(makeBaseNPMConfig()); diff --git a/packages/utils/rollup.npm.config.mjs b/packages/utils/rollup.npm.config.mjs new file mode 100644 index 000000000000..84a06f2fb64a --- /dev/null +++ b/packages/utils/rollup.npm.config.mjs @@ -0,0 +1,3 @@ +import { makeBaseNPMConfig, makeNPMConfigVariants } from '@sentry-internal/rollup-utils'; + +export default makeNPMConfigVariants(makeBaseNPMConfig()); diff --git a/packages/utils/scripts/buildRollup.ts b/packages/utils/scripts/buildRollup.ts index 6ce18d747787..80064a14aed7 100644 --- a/packages/utils/scripts/buildRollup.ts +++ b/packages/utils/scripts/buildRollup.ts @@ -9,7 +9,7 @@ function run(cmd: string, options?: childProcess.ExecSyncOptions): string | Buff return childProcess.execSync(cmd, { stdio: 'inherit', ...options }); } -run('yarn rollup -c rollup.npm.config.js'); +run('yarn rollup -c rollup.npm.config.mjs'); // We want to distribute the README because it contains the MIT license blurb from Sucrase and Rollup fs.copyFileSync('src/buildPolyfills/README.md', 'build/cjs/buildPolyfills/README.md'); diff --git a/packages/vercel-edge/package.json b/packages/vercel-edge/package.json index 380e28a08d1d..7ad9965f1a71 100644 --- a/packages/vercel-edge/package.json +++ b/packages/vercel-edge/package.json @@ -35,13 +35,13 @@ "scripts": { "build": "run-p build:transpile build:types", "build:dev": "yarn build", - "build:transpile": "rollup -c rollup.npm.config.js", + "build:transpile": "rollup -c rollup.npm.config.mjs", "build:types": "run-s build:types:core build:types:downlevel", "build:types:core": "tsc -p tsconfig.types.json", "build:types:downlevel": "yarn downlevel-dts build/types build/types-ts3.8 --to ts3.8", "build:watch": "run-p build:transpile:watch build:types:watch", "build:dev:watch": "yarn build:watch", - "build:transpile:watch": "rollup -c rollup.npm.config.js --watch", + "build:transpile:watch": "rollup -c rollup.npm.config.mjs --watch", "build:types:watch": "tsc -p tsconfig.types.json --watch", "build:tarball": "ts-node ../../scripts/prepack.ts && npm pack ./build", "circularDepCheck": "madge --circular src/index.ts", diff --git a/packages/vercel-edge/rollup.npm.config.js b/packages/vercel-edge/rollup.npm.config.js deleted file mode 100644 index 5a62b528ef44..000000000000 --- a/packages/vercel-edge/rollup.npm.config.js +++ /dev/null @@ -1,3 +0,0 @@ -import { makeBaseNPMConfig, makeNPMConfigVariants } from '../../rollup/index.js'; - -export default makeNPMConfigVariants(makeBaseNPMConfig()); diff --git a/packages/vercel-edge/rollup.npm.config.mjs b/packages/vercel-edge/rollup.npm.config.mjs new file mode 100644 index 000000000000..84a06f2fb64a --- /dev/null +++ b/packages/vercel-edge/rollup.npm.config.mjs @@ -0,0 +1,3 @@ +import { makeBaseNPMConfig, makeNPMConfigVariants } from '@sentry-internal/rollup-utils'; + +export default makeNPMConfigVariants(makeBaseNPMConfig()); diff --git a/packages/vue/package.json b/packages/vue/package.json index ffd7ee5e1bb2..096ba312ff9d 100644 --- a/packages/vue/package.json +++ b/packages/vue/package.json @@ -37,13 +37,13 @@ "scripts": { "build": "run-p build:transpile build:types", "build:dev": "run-p build:transpile build:types", - "build:transpile": "rollup -c rollup.npm.config.js", + "build:transpile": "rollup -c rollup.npm.config.mjs", "build:types": "run-s build:types:core build:types:downlevel", "build:types:core": "tsc -p tsconfig.types.json", "build:types:downlevel": "yarn downlevel-dts build/types build/types-ts3.8 --to ts3.8", "build:watch": "run-p build:transpile:watch build:types:watch", "build:dev:watch": "run-p build:transpile:watch build:types:watch", - "build:transpile:watch": "rollup -c rollup.npm.config.js --watch", + "build:transpile:watch": "rollup -c rollup.npm.config.mjs --watch", "build:types:watch": "tsc -p tsconfig.types.json --watch", "build:tarball": "ts-node ../../scripts/prepack.ts && npm pack ./build", "circularDepCheck": "madge --circular src/index.ts", diff --git a/packages/vue/rollup.npm.config.js b/packages/vue/rollup.npm.config.js deleted file mode 100644 index 5a62b528ef44..000000000000 --- a/packages/vue/rollup.npm.config.js +++ /dev/null @@ -1,3 +0,0 @@ -import { makeBaseNPMConfig, makeNPMConfigVariants } from '../../rollup/index.js'; - -export default makeNPMConfigVariants(makeBaseNPMConfig()); diff --git a/packages/vue/rollup.npm.config.mjs b/packages/vue/rollup.npm.config.mjs new file mode 100644 index 000000000000..84a06f2fb64a --- /dev/null +++ b/packages/vue/rollup.npm.config.mjs @@ -0,0 +1,3 @@ +import { makeBaseNPMConfig, makeNPMConfigVariants } from '@sentry-internal/rollup-utils'; + +export default makeNPMConfigVariants(makeBaseNPMConfig()); diff --git a/packages/wasm/package.json b/packages/wasm/package.json index 1ec627dc86b7..8ed638485478 100644 --- a/packages/wasm/package.json +++ b/packages/wasm/package.json @@ -29,16 +29,16 @@ }, "scripts": { "build": "run-p build:transpile build:bundle build:types", - "build:bundle": "rollup --config rollup.bundle.config.js", + "build:bundle": "rollup --config rollup.bundle.config.mjs", "build:dev": "run-p build:transpile build:types", - "build:transpile": "rollup -c rollup.npm.config.js", + "build:transpile": "rollup -c rollup.npm.config.mjs", "build:types": "run-s build:types:core build:types:downlevel", "build:types:core": "tsc -p tsconfig.types.json", "build:types:downlevel": "yarn downlevel-dts build/npm/types build/npm/types-ts3.8 --to ts3.8", "build:watch": "run-p build:transpile:watch build:bundle:watch build:types:watch", - "build:bundle:watch": "rollup --config rollup.bundle.config.js --watch", + "build:bundle:watch": "rollup --config rollup.bundle.config.mjs --watch", "build:dev:watch": "run-p build:transpile:watch build:types:watch", - "build:transpile:watch": "rollup -c rollup.npm.config.js --watch", + "build:transpile:watch": "rollup -c rollup.npm.config.mjs --watch", "build:types:watch": "tsc -p tsconfig.types.json --watch", "build:tarball": "ts-node ../../scripts/prepack.ts --bundles && npm pack ./build/npm", "circularDepCheck": "madge --circular src/index.ts", diff --git a/packages/wasm/rollup.bundle.config.js b/packages/wasm/rollup.bundle.config.mjs similarity index 91% rename from packages/wasm/rollup.bundle.config.js rename to packages/wasm/rollup.bundle.config.mjs index 2c97176f0dee..b149d75d6244 100644 --- a/packages/wasm/rollup.bundle.config.js +++ b/packages/wasm/rollup.bundle.config.mjs @@ -1,4 +1,4 @@ -import { makeBaseBundleConfig, makeBundleConfigVariants } from '../../rollup/index.js'; +import { makeBaseBundleConfig, makeBundleConfigVariants } from '@sentry-internal/rollup-utils'; const baseBundleConfig = makeBaseBundleConfig({ bundleType: 'addon', diff --git a/packages/wasm/rollup.npm.config.js b/packages/wasm/rollup.npm.config.mjs similarity index 64% rename from packages/wasm/rollup.npm.config.js rename to packages/wasm/rollup.npm.config.mjs index 4ffa8b9396d8..6d09adefc859 100644 --- a/packages/wasm/rollup.npm.config.js +++ b/packages/wasm/rollup.npm.config.mjs @@ -1,4 +1,4 @@ -import { makeBaseNPMConfig, makeNPMConfigVariants } from '../../rollup/index.js'; +import { makeBaseNPMConfig, makeNPMConfigVariants } from '@sentry-internal/rollup-utils'; export default makeNPMConfigVariants( makeBaseNPMConfig({ diff --git a/rollup/index.js b/rollup/index.js deleted file mode 100644 index 2ae4712165ad..000000000000 --- a/rollup/index.js +++ /dev/null @@ -1,9 +0,0 @@ -Error.stackTraceLimit = Infinity; - -// TODO Is this necessary? -import * as plugins from './plugins/index.js'; -export { plugins }; - -export * from './bundleHelpers.js'; -export * from './npmHelpers.js'; -export { insertAt } from './utils.js'; diff --git a/rollup/plugins/index.js b/rollup/plugins/index.js deleted file mode 100644 index 014b3b383b4d..000000000000 --- a/rollup/plugins/index.js +++ /dev/null @@ -1,2 +0,0 @@ -export * from './bundlePlugins'; -export * from './npmPlugins'; diff --git a/scripts/tarball-checksums.sh b/scripts/tarball-checksums.sh new file mode 100644 index 000000000000..60487f9037d3 --- /dev/null +++ b/scripts/tarball-checksums.sh @@ -0,0 +1,25 @@ +#!/bin/sh + +# Get the directory of the script +script_dir=$(cd "$(dirname "$0")" && pwd) + +# Function to calculate SHA checksums for files +calculate_sha_checksum() { + file="$1" + # Strip the directory name from the file path + file_name=$(basename "$file") + sha_checksum=$(sha256sum "$file" | awk '{print $1}') + # Align the output + printf "%-48s: %s\n" "$file_name" "$sha_checksum" +} + +# Main function to process files recursively +process_files() { + # Find all ".tgz" files recursively + find "$script_dir/.." -type f -name "*.tgz" | while IFS= read -r file; do + calculate_sha_checksum "$file" + done +} + +# Call the main function to process files +process_files From e1bc0e82a0ca952e1c3c663832f2555d3ebdaa95 Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Wed, 20 Dec 2023 17:20:57 +0100 Subject: [PATCH 04/24] build: bump sucrase to 3.34.0 (#9942) To ensure we have up-to-date TS support, e.g. `satisfies` cannot be transpiled previously. See https://github.com/alangpierce/sucrase/blob/main/CHANGELOG.md --- yarn.lock | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index 64244109473e..42e9c147cc69 100644 --- a/yarn.lock +++ b/yarn.lock @@ -29365,10 +29365,11 @@ stylus@0.54.8, stylus@^0.54.7, stylus@^0.54.8: source-map "^0.7.3" sucrase@^3.20.0: - version "3.21.0" - resolved "https://registry.yarnpkg.com/sucrase/-/sucrase-3.21.0.tgz#6a5affdbe716b22e4dc99c57d366ad0d216444b9" - integrity sha512-FjAhMJjDcifARI7bZej0Bi1yekjWQHoEvWIXhLPwDhC6O4iZ5PtGb86WV56riW87hzpgB13wwBKO9vKAiWu5VQ== + version "3.34.0" + resolved "https://registry.yarnpkg.com/sucrase/-/sucrase-3.34.0.tgz#1e0e2d8fcf07f8b9c3569067d92fbd8690fb576f" + integrity sha512-70/LQEZ07TEcxiU2dz51FKaE6hCTWC6vr7FOk3Gr0U60C3shtAN+H+BFr9XlYe5xqf3RA8nrc+VIwzCfnxuXJw== dependencies: + "@jridgewell/gen-mapping" "^0.3.2" commander "^4.0.0" glob "7.1.6" lines-and-columns "^1.1.6" From 1621214a125710734d0934c7100c71bec63908c0 Mon Sep 17 00:00:00 2001 From: Luca Forstner Date: Wed, 20 Dec 2023 18:08:21 +0100 Subject: [PATCH 05/24] ci: Use `package.json`s as additional dependency cache key (#9937) --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 726e2398d488..fb2ace6e6e65 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -176,7 +176,7 @@ jobs: # so no need to reinstall them - name: Compute dependency cache key id: compute_lockfile_hash - run: echo "hash=${{ hashFiles('yarn.lock') }}" >> "$GITHUB_OUTPUT" + run: echo "hash=${{ hashFiles('yarn.lock', '**/package.json') }}" >> "$GITHUB_OUTPUT" - name: Check dependency cache uses: actions/cache@v3 From 61094d4d03a8d9ddda36a8b95b7b74ffbed017f2 Mon Sep 17 00:00:00 2001 From: Ash Anand Date: Wed, 20 Dec 2023 13:53:20 -0500 Subject: [PATCH 06/24] Check if HTMLElement exists first --- packages/utils/src/browser.ts | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/packages/utils/src/browser.ts b/packages/utils/src/browser.ts index 5688277d0f4f..53549a878adf 100644 --- a/packages/utils/src/browser.ts +++ b/packages/utils/src/browser.ts @@ -86,9 +86,12 @@ function _htmlElementAsString(el: unknown, keyAttrs?: string[]): string { return ''; } - // If using the component name annotation plugin, this value may be available on the DOM node - if (elem instanceof HTMLElement && elem.dataset && elem.dataset['sentryComponent']) { - return elem.dataset['sentryComponent']; + // @ts-expect-error WINDOW has HTMLElement + if (WINDOW.HTMLElement) { + // If using the component name annotation plugin, this value may be available on the DOM node + if (elem instanceof HTMLElement && elem.dataset && elem.dataset['sentryComponent']) { + return elem.dataset['sentryComponent']; + } } out.push(elem.tagName.toLowerCase()); @@ -171,6 +174,11 @@ export function getDomElement(selector: string): E | null { * @returns a string representation of the component for the provided DOM element, or `null` if not found */ export function getComponentName(elem: unknown): string | null { + // @ts-expect-error WINDOW has HTMLElement + if (!WINDOW.HTMLElement) { + return null; + } + let currentElem = elem as SimpleNode; const MAX_TRAVERSE_HEIGHT = 5; for (let i = 0; i < MAX_TRAVERSE_HEIGHT; i++) { From 811eb29d7983781c8a5a64dee6f387e94051f561 Mon Sep 17 00:00:00 2001 From: Ash Anand Date: Wed, 20 Dec 2023 17:28:02 -0500 Subject: [PATCH 07/24] feat(react): Send component name on spans --- packages/react/src/profiler.tsx | 5 +++++ packages/react/test/profiler.test.tsx | 9 ++++++--- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/packages/react/src/profiler.tsx b/packages/react/src/profiler.tsx index 9647d34f0fb4..b8ffe2f1708a 100644 --- a/packages/react/src/profiler.tsx +++ b/packages/react/src/profiler.tsx @@ -63,6 +63,7 @@ class Profiler extends React.Component { description: `<${name}>`, op: REACT_MOUNT_OP, origin: 'auto.ui.react.profiler', + data: { 'ui.component_name': name }, }); } } @@ -87,6 +88,7 @@ class Profiler extends React.Component { this._updateSpan = this._mountSpan.startChild({ data: { changedProps, + 'ui.component_name': this.props.name, }, description: `<${this.props.name}>`, op: REACT_UPDATE_OP, @@ -120,6 +122,7 @@ class Profiler extends React.Component { op: REACT_RENDER_OP, origin: 'auto.ui.react.profiler', startTimestamp: this._mountSpan.endTimestamp, + data: { 'ui.component_name': name }, }); } } @@ -184,6 +187,7 @@ function useProfiler( description: `<${name}>`, op: REACT_MOUNT_OP, origin: 'auto.ui.react.profiler', + data: { 'ui.component_name': name }, }); } @@ -203,6 +207,7 @@ function useProfiler( op: REACT_RENDER_OP, origin: 'auto.ui.react.profiler', startTimestamp: mountSpan.endTimestamp, + data: { 'ui.component_name': name }, }); } }; diff --git a/packages/react/test/profiler.test.tsx b/packages/react/test/profiler.test.tsx index 70eaff2d2c8b..fa629434cdf3 100644 --- a/packages/react/test/profiler.test.tsx +++ b/packages/react/test/profiler.test.tsx @@ -80,6 +80,7 @@ describe('withProfiler', () => { description: `<${UNKNOWN_COMPONENT}>`, op: REACT_MOUNT_OP, origin: 'auto.ui.react.profiler', + data: { 'ui.component_name': 'unknown' }, }); }); }); @@ -99,6 +100,7 @@ describe('withProfiler', () => { op: REACT_RENDER_OP, origin: 'auto.ui.react.profiler', startTimestamp: undefined, + data: { 'ui.component_name': 'unknown' }, }); }); @@ -114,7 +116,6 @@ describe('withProfiler', () => { expect(mockStartChild).toHaveBeenCalledTimes(1); }); }); - describe('update span', () => { it('is created when component is updated', () => { const ProfiledComponent = withProfiler((props: { num: number }) =>
{props.num}
); @@ -126,7 +127,7 @@ describe('withProfiler', () => { rerender(); expect(mockStartChild).toHaveBeenCalledTimes(2); expect(mockStartChild).toHaveBeenLastCalledWith({ - data: { changedProps: ['num'] }, + data: { changedProps: ['num'], 'ui.component_name': 'unknown' }, description: `<${UNKNOWN_COMPONENT}>`, op: REACT_UPDATE_OP, origin: 'auto.ui.react.profiler', @@ -137,7 +138,7 @@ describe('withProfiler', () => { rerender(); expect(mockStartChild).toHaveBeenCalledTimes(3); expect(mockStartChild).toHaveBeenLastCalledWith({ - data: { changedProps: ['num'] }, + data: { changedProps: ['num'], 'ui.component_name': 'unknown' }, description: `<${UNKNOWN_COMPONENT}>`, op: REACT_UPDATE_OP, origin: 'auto.ui.react.profiler', @@ -180,6 +181,7 @@ describe('useProfiler()', () => { description: '', op: REACT_MOUNT_OP, origin: 'auto.ui.react.profiler', + data: { 'ui.component_name': 'Example' }, }); }); }); @@ -203,6 +205,7 @@ describe('useProfiler()', () => { description: '', op: REACT_RENDER_OP, origin: 'auto.ui.react.profiler', + data: { 'ui.component_name': 'Example' }, }), ); }); From 52810b3352500d61d263936a301f6447984b9e56 Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Thu, 21 Dec 2023 09:25:27 +0100 Subject: [PATCH 08/24] ref: Small integration refactors (#9928) Some small fixes to integrations & rewriting some "forgotten" ones to functional syntax. --- packages/browser/src/profiling/integration.ts | 169 ++++++++---------- packages/bun/src/integrations/bunserver.ts | 42 ++--- packages/core/src/metrics/integration.ts | 45 ++--- packages/integration-shims/src/Feedback.ts | 7 +- 4 files changed, 121 insertions(+), 142 deletions(-) diff --git a/packages/browser/src/profiling/integration.ts b/packages/browser/src/profiling/integration.ts index 5173705feaa6..3f823429c122 100644 --- a/packages/browser/src/profiling/integration.ts +++ b/packages/browser/src/profiling/integration.ts @@ -1,5 +1,5 @@ -import { getCurrentScope } from '@sentry/core'; -import type { Client, EventEnvelope, EventProcessor, Hub, Integration, Transaction } from '@sentry/types'; +import { convertIntegrationFnToClass, getCurrentScope } from '@sentry/core'; +import type { EventEnvelope, IntegrationFn, Transaction } from '@sentry/types'; import type { Profile } from '@sentry/types/src/profiling'; import { logger } from '@sentry/utils'; @@ -16,108 +16,97 @@ import { takeProfileFromGlobalCache, } from './utils'; -/** - * Browser profiling integration. Stores any event that has contexts["profile"]["profile_id"] - * This exists because we do not want to await async profiler.stop calls as transaction.finish is called - * in a synchronous context. Instead, we handle sending the profile async from the promise callback and - * rely on being able to pull the event from the cache when we need to construct the envelope. This makes the - * integration less reliable as we might be dropping profiles when the cache is full. - * - * @experimental - */ -export class BrowserProfilingIntegration implements Integration { - public static id: string = 'BrowserProfilingIntegration'; +const INTEGRATION_NAME = 'BrowserProfiling'; - public readonly name: string; +const browserProfilingIntegration: IntegrationFn = () => { + return { + name: INTEGRATION_NAME, + setup(client) { + const scope = getCurrentScope(); - /** @deprecated This is never set. */ - public getCurrentHub?: () => Hub; + const transaction = scope.getTransaction(); - public constructor() { - this.name = BrowserProfilingIntegration.id; - } - - /** - * @inheritDoc - */ - public setupOnce(_addGlobalEventProcessor: (callback: EventProcessor) => void, _getCurrentHub: () => Hub): void { - // noop - } - - /** @inheritdoc */ - public setup(client: Client): void { - const scope = getCurrentScope(); - - const transaction = scope.getTransaction(); - - if (transaction && isAutomatedPageLoadTransaction(transaction)) { - if (shouldProfileTransaction(transaction)) { - startProfileForTransaction(transaction); - } - } - - if (typeof client.on !== 'function') { - logger.warn('[Profiling] Client does not support hooks, profiling will be disabled'); - return; - } - - client.on('startTransaction', (transaction: Transaction) => { - if (shouldProfileTransaction(transaction)) { - startProfileForTransaction(transaction); - } - }); - - client.on('beforeEnvelope', (envelope): void => { - // if not profiles are in queue, there is nothing to add to the envelope. - if (!getActiveProfilesCount()) { - return; + if (transaction && isAutomatedPageLoadTransaction(transaction)) { + if (shouldProfileTransaction(transaction)) { + startProfileForTransaction(transaction); + } } - const profiledTransactionEvents = findProfiledTransactionsFromEnvelope(envelope); - if (!profiledTransactionEvents.length) { + if (typeof client.on !== 'function') { + logger.warn('[Profiling] Client does not support hooks, profiling will be disabled'); return; } - const profilesToAddToEnvelope: Profile[] = []; - - for (const profiledTransaction of profiledTransactionEvents) { - const context = profiledTransaction && profiledTransaction.contexts; - const profile_id = context && context['profile'] && context['profile']['profile_id']; - const start_timestamp = context && context['profile'] && context['profile']['start_timestamp']; - - if (typeof profile_id !== 'string') { - DEBUG_BUILD && logger.log('[Profiling] cannot find profile for a transaction without a profile context'); - continue; + client.on('startTransaction', (transaction: Transaction) => { + if (shouldProfileTransaction(transaction)) { + startProfileForTransaction(transaction); } + }); - if (!profile_id) { - DEBUG_BUILD && logger.log('[Profiling] cannot find profile for a transaction without a profile context'); - continue; + client.on('beforeEnvelope', (envelope): void => { + // if not profiles are in queue, there is nothing to add to the envelope. + if (!getActiveProfilesCount()) { + return; } - // Remove the profile from the transaction context before sending, relay will take care of the rest. - if (context && context['profile']) { - delete context.profile; + const profiledTransactionEvents = findProfiledTransactionsFromEnvelope(envelope); + if (!profiledTransactionEvents.length) { + return; } - const profile = takeProfileFromGlobalCache(profile_id); - if (!profile) { - DEBUG_BUILD && logger.log(`[Profiling] Could not retrieve profile for transaction: ${profile_id}`); - continue; + const profilesToAddToEnvelope: Profile[] = []; + + for (const profiledTransaction of profiledTransactionEvents) { + const context = profiledTransaction && profiledTransaction.contexts; + const profile_id = context && context['profile'] && context['profile']['profile_id']; + const start_timestamp = context && context['profile'] && context['profile']['start_timestamp']; + + if (typeof profile_id !== 'string') { + DEBUG_BUILD && logger.log('[Profiling] cannot find profile for a transaction without a profile context'); + continue; + } + + if (!profile_id) { + DEBUG_BUILD && logger.log('[Profiling] cannot find profile for a transaction without a profile context'); + continue; + } + + // Remove the profile from the transaction context before sending, relay will take care of the rest. + if (context && context['profile']) { + delete context.profile; + } + + const profile = takeProfileFromGlobalCache(profile_id); + if (!profile) { + DEBUG_BUILD && logger.log(`[Profiling] Could not retrieve profile for transaction: ${profile_id}`); + continue; + } + + const profileEvent = createProfilingEvent( + profile_id, + start_timestamp as number | undefined, + profile, + profiledTransaction as ProfiledEvent, + ); + if (profileEvent) { + profilesToAddToEnvelope.push(profileEvent); + } } - const profileEvent = createProfilingEvent( - profile_id, - start_timestamp as number | undefined, - profile, - profiledTransaction as ProfiledEvent, - ); - if (profileEvent) { - profilesToAddToEnvelope.push(profileEvent); - } - } + addProfilesToEnvelope(envelope as EventEnvelope, profilesToAddToEnvelope); + }); + }, + }; +}; - addProfilesToEnvelope(envelope as EventEnvelope, profilesToAddToEnvelope); - }); - } -} +/** + * Browser profiling integration. Stores any event that has contexts["profile"]["profile_id"] + * This exists because we do not want to await async profiler.stop calls as transaction.finish is called + * in a synchronous context. Instead, we handle sending the profile async from the promise callback and + * rely on being able to pull the event from the cache when we need to construct the envelope. This makes the + * integration less reliable as we might be dropping profiles when the cache is full. + * + * @experimental + */ +// eslint-disable-next-line deprecation/deprecation +export const BrowserProfilingIntegration = convertIntegrationFnToClass(INTEGRATION_NAME, browserProfilingIntegration); diff --git a/packages/bun/src/integrations/bunserver.ts b/packages/bun/src/integrations/bunserver.ts index 6644728c19b3..89d245908400 100644 --- a/packages/bun/src/integrations/bunserver.ts +++ b/packages/bun/src/integrations/bunserver.ts @@ -1,28 +1,30 @@ -import { Transaction, captureException, continueTrace, runWithAsyncContext, startSpan } from '@sentry/core'; -import type { Integration } from '@sentry/types'; +import { + Transaction, + captureException, + continueTrace, + convertIntegrationFnToClass, + runWithAsyncContext, + startSpan, +} from '@sentry/core'; +import type { IntegrationFn } from '@sentry/types'; import { getSanitizedUrlString, parseUrl } from '@sentry/utils'; +const INTEGRATION_NAME = 'BunServer'; + +const bunServerIntegration: IntegrationFn = () => { + return { + name: INTEGRATION_NAME, + setupOnce() { + instrumentBunServe(); + }, + }; +}; + /** * Instruments `Bun.serve` to automatically create transactions and capture errors. */ -export class BunServer implements Integration { - /** - * @inheritDoc - */ - public static id: string = 'BunServer'; - - /** - * @inheritDoc - */ - public name: string = BunServer.id; - - /** - * @inheritDoc - */ - public setupOnce(): void { - instrumentBunServe(); - } -} +// eslint-disable-next-line deprecation/deprecation +export const BunServer = convertIntegrationFnToClass(INTEGRATION_NAME, bunServerIntegration); /** * Instruments Bun.serve by patching it's options. diff --git a/packages/core/src/metrics/integration.ts b/packages/core/src/metrics/integration.ts index 9ce7f836ca8c..0c3fa626a9e5 100644 --- a/packages/core/src/metrics/integration.ts +++ b/packages/core/src/metrics/integration.ts @@ -1,38 +1,23 @@ -import type { ClientOptions, Integration } from '@sentry/types'; +import type { ClientOptions, IntegrationFn } from '@sentry/types'; import type { BaseClient } from '../baseclient'; +import { convertIntegrationFnToClass } from '../integration'; import { SimpleMetricsAggregator } from './simpleaggregator'; +const INTEGRATION_NAME = 'MetricsAggregator'; + +const metricsAggregatorIntegration: IntegrationFn = () => { + return { + name: INTEGRATION_NAME, + setup(client: BaseClient) { + client.metricsAggregator = new SimpleMetricsAggregator(client); + }, + }; +}; + /** * Enables Sentry metrics monitoring. * * @experimental This API is experimental and might having breaking changes in the future. */ -export class MetricsAggregator implements Integration { - /** - * @inheritDoc - */ - public static id: string = 'MetricsAggregator'; - - /** - * @inheritDoc - */ - public name: string; - - public constructor() { - this.name = MetricsAggregator.id; - } - - /** - * @inheritDoc - */ - public setupOnce(): void { - // Do nothing - } - - /** - * @inheritDoc - */ - public setup(client: BaseClient): void { - client.metricsAggregator = new SimpleMetricsAggregator(client); - } -} +// eslint-disable-next-line deprecation/deprecation +export const MetricsAggregator = convertIntegrationFnToClass(INTEGRATION_NAME, metricsAggregatorIntegration); diff --git a/packages/integration-shims/src/Feedback.ts b/packages/integration-shims/src/Feedback.ts index 3f41a15a8231..78db6083d3c2 100644 --- a/packages/integration-shims/src/Feedback.ts +++ b/packages/integration-shims/src/Feedback.ts @@ -1,4 +1,5 @@ import type { Integration } from '@sentry/types'; +import { consoleSandbox } from '@sentry/utils'; /** * This is a shim for the Feedback integration. @@ -20,8 +21,10 @@ class FeedbackShim implements Integration { public constructor(_options: any) { this.name = FeedbackShim.id; - // eslint-disable-next-line no-console - console.error('You are using new Feedback() even though this bundle does not include Feedback.'); + consoleSandbox(() => { + // eslint-disable-next-line no-console + console.error('You are using new Feedback() even though this bundle does not include Feedback.'); + }); } /** jsdoc */ From 617384639104635d9421464d7205d837d27eee34 Mon Sep 17 00:00:00 2001 From: Tim Fish Date: Thu, 21 Dec 2023 09:46:26 +0100 Subject: [PATCH 09/24] feat(node): Rework ANR to use worker script via an integration (#9945) --- packages/core/src/index.ts | 2 +- packages/node-experimental/src/sdk/init.ts | 8 +- .../suites/anr/basic-session.js | 29 +- .../suites/anr/basic.js | 29 +- .../suites/anr/basic.mjs | 11 +- .../suites/anr/forked.js | 29 +- .../suites/anr/legacy.js | 31 ++ .../suites/anr/test-transport.js | 17 - .../node-integration-tests/suites/anr/test.ts | 79 ++-- packages/node/rollup.anr-worker.config.mjs | 35 ++ packages/node/rollup.npm.config.mjs | 7 +- packages/node/src/anr/debugger.ts | 55 --- packages/node/src/anr/index.ts | 308 --------------- packages/node/src/anr/websocket.ts | 366 ------------------ packages/node/src/index.ts | 3 +- packages/node/src/integrations/anr/common.ts | 34 ++ packages/node/src/integrations/anr/index.ts | 155 ++++++++ packages/node/src/integrations/anr/legacy.ts | 32 ++ .../src/integrations/anr/worker-script.ts | 2 + packages/node/src/integrations/anr/worker.ts | 215 ++++++++++ packages/node/src/integrations/index.ts | 1 + packages/node/src/sdk.ts | 6 - packages/rollup-utils/bundleHelpers.mjs | 10 + packages/utils/src/anr.ts | 51 +-- 24 files changed, 617 insertions(+), 898 deletions(-) create mode 100644 packages/node-integration-tests/suites/anr/legacy.js delete mode 100644 packages/node-integration-tests/suites/anr/test-transport.js create mode 100644 packages/node/rollup.anr-worker.config.mjs delete mode 100644 packages/node/src/anr/debugger.ts delete mode 100644 packages/node/src/anr/index.ts delete mode 100644 packages/node/src/anr/websocket.ts create mode 100644 packages/node/src/integrations/anr/common.ts create mode 100644 packages/node/src/integrations/anr/index.ts create mode 100644 packages/node/src/integrations/anr/legacy.ts create mode 100644 packages/node/src/integrations/anr/worker-script.ts create mode 100644 packages/node/src/integrations/anr/worker.ts diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 114ac4a670fe..08f18f38ade6 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -5,7 +5,7 @@ export type { ServerRuntimeClientOptions } from './server-runtime-client'; export type { RequestDataIntegrationOptions } from './integrations/requestdata'; export * from './tracing'; -export { createEventEnvelope } from './envelope'; +export { createEventEnvelope, createSessionEnvelope } from './envelope'; export { addBreadcrumb, captureCheckIn, diff --git a/packages/node-experimental/src/sdk/init.ts b/packages/node-experimental/src/sdk/init.ts index e7c6ebf72381..821757a9a246 100644 --- a/packages/node-experimental/src/sdk/init.ts +++ b/packages/node-experimental/src/sdk/init.ts @@ -4,7 +4,6 @@ import { defaultIntegrations as defaultNodeIntegrations, defaultStackParser, getSentryRelease, - isAnrChildProcess, makeNodeTransport, } from '@sentry/node'; import type { Integration } from '@sentry/types'; @@ -113,15 +112,14 @@ function getClientOptions(options: NodeExperimentalOptions): NodeExperimentalCli const release = getRelease(options.release); - // If there is no release, or we are in an ANR child process, we disable autoSessionTracking by default const autoSessionTracking = - typeof release !== 'string' || isAnrChildProcess() + typeof release !== 'string' ? false : options.autoSessionTracking === undefined ? true : options.autoSessionTracking; - // We enforce tracesSampleRate = 0 in ANR child processes - const tracesSampleRate = isAnrChildProcess() ? 0 : getTracesSampleRate(options.tracesSampleRate); + + const tracesSampleRate = getTracesSampleRate(options.tracesSampleRate); const baseOptions = dropUndefinedKeys({ transport: makeNodeTransport, diff --git a/packages/node-integration-tests/suites/anr/basic-session.js b/packages/node-integration-tests/suites/anr/basic-session.js index 29cdc17e76c9..03c8c94fdadf 100644 --- a/packages/node-integration-tests/suites/anr/basic-session.js +++ b/packages/node-integration-tests/suites/anr/basic-session.js @@ -1,31 +1,28 @@ const crypto = require('crypto'); +const assert = require('assert'); const Sentry = require('@sentry/node'); -const { transport } = require('./test-transport.js'); - -// close both processes after 5 seconds setTimeout(() => { process.exit(); -}, 5000); +}, 10000); Sentry.init({ dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0', debug: true, - transport, + integrations: [new Sentry.Integrations.Anr({ captureStackTrace: true, anrThreshold: 200 })], }); -Sentry.enableAnrDetection({ captureStackTrace: true, anrThreshold: 200 }).then(() => { - function longWork() { - for (let i = 0; i < 100; i++) { - const salt = crypto.randomBytes(128).toString('base64'); - // eslint-disable-next-line no-unused-vars - const hash = crypto.pbkdf2Sync('myPassword', salt, 10000, 512, 'sha512'); - } +function longWork() { + for (let i = 0; i < 100; i++) { + const salt = crypto.randomBytes(128).toString('base64'); + // eslint-disable-next-line no-unused-vars + const hash = crypto.pbkdf2Sync('myPassword', salt, 10000, 512, 'sha512'); + assert.ok(hash); } +} - setTimeout(() => { - longWork(); - }, 1000); -}); +setTimeout(() => { + longWork(); +}, 1000); diff --git a/packages/node-integration-tests/suites/anr/basic.js b/packages/node-integration-tests/suites/anr/basic.js index 33c4151a19f1..5e0323e2c6c5 100644 --- a/packages/node-integration-tests/suites/anr/basic.js +++ b/packages/node-integration-tests/suites/anr/basic.js @@ -1,32 +1,29 @@ const crypto = require('crypto'); +const assert = require('assert'); const Sentry = require('@sentry/node'); -const { transport } = require('./test-transport.js'); - -// close both processes after 5 seconds setTimeout(() => { process.exit(); -}, 5000); +}, 10000); Sentry.init({ dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0', debug: true, autoSessionTracking: false, - transport, + integrations: [new Sentry.Integrations.Anr({ captureStackTrace: true, anrThreshold: 200 })], }); -Sentry.enableAnrDetection({ captureStackTrace: true, anrThreshold: 200 }).then(() => { - function longWork() { - for (let i = 0; i < 100; i++) { - const salt = crypto.randomBytes(128).toString('base64'); - // eslint-disable-next-line no-unused-vars - const hash = crypto.pbkdf2Sync('myPassword', salt, 10000, 512, 'sha512'); - } +function longWork() { + for (let i = 0; i < 100; i++) { + const salt = crypto.randomBytes(128).toString('base64'); + // eslint-disable-next-line no-unused-vars + const hash = crypto.pbkdf2Sync('myPassword', salt, 10000, 512, 'sha512'); + assert.ok(hash); } +} - setTimeout(() => { - longWork(); - }, 1000); -}); +setTimeout(() => { + longWork(); +}, 1000); diff --git a/packages/node-integration-tests/suites/anr/basic.mjs b/packages/node-integration-tests/suites/anr/basic.mjs index 3d10dc556076..17c8a2d460df 100644 --- a/packages/node-integration-tests/suites/anr/basic.mjs +++ b/packages/node-integration-tests/suites/anr/basic.mjs @@ -1,29 +1,26 @@ +import * as assert from 'assert'; import * as crypto from 'crypto'; import * as Sentry from '@sentry/node'; -const { transport } = await import('./test-transport.js'); - -// close both processes after 5 seconds setTimeout(() => { process.exit(); -}, 5000); +}, 10000); Sentry.init({ dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0', debug: true, autoSessionTracking: false, - transport, + integrations: [new Sentry.Integrations.Anr({ captureStackTrace: true, anrThreshold: 200 })], }); -await Sentry.enableAnrDetection({ captureStackTrace: true, anrThreshold: 200 }); - function longWork() { for (let i = 0; i < 100; i++) { const salt = crypto.randomBytes(128).toString('base64'); // eslint-disable-next-line no-unused-vars const hash = crypto.pbkdf2Sync('myPassword', salt, 10000, 512, 'sha512'); + assert.ok(hash); } } diff --git a/packages/node-integration-tests/suites/anr/forked.js b/packages/node-integration-tests/suites/anr/forked.js index 33c4151a19f1..5e0323e2c6c5 100644 --- a/packages/node-integration-tests/suites/anr/forked.js +++ b/packages/node-integration-tests/suites/anr/forked.js @@ -1,32 +1,29 @@ const crypto = require('crypto'); +const assert = require('assert'); const Sentry = require('@sentry/node'); -const { transport } = require('./test-transport.js'); - -// close both processes after 5 seconds setTimeout(() => { process.exit(); -}, 5000); +}, 10000); Sentry.init({ dsn: 'https://public@dsn.ingest.sentry.io/1337', release: '1.0', debug: true, autoSessionTracking: false, - transport, + integrations: [new Sentry.Integrations.Anr({ captureStackTrace: true, anrThreshold: 200 })], }); -Sentry.enableAnrDetection({ captureStackTrace: true, anrThreshold: 200 }).then(() => { - function longWork() { - for (let i = 0; i < 100; i++) { - const salt = crypto.randomBytes(128).toString('base64'); - // eslint-disable-next-line no-unused-vars - const hash = crypto.pbkdf2Sync('myPassword', salt, 10000, 512, 'sha512'); - } +function longWork() { + for (let i = 0; i < 100; i++) { + const salt = crypto.randomBytes(128).toString('base64'); + // eslint-disable-next-line no-unused-vars + const hash = crypto.pbkdf2Sync('myPassword', salt, 10000, 512, 'sha512'); + assert.ok(hash); } +} - setTimeout(() => { - longWork(); - }, 1000); -}); +setTimeout(() => { + longWork(); +}, 1000); diff --git a/packages/node-integration-tests/suites/anr/legacy.js b/packages/node-integration-tests/suites/anr/legacy.js new file mode 100644 index 000000000000..46b6e1437b10 --- /dev/null +++ b/packages/node-integration-tests/suites/anr/legacy.js @@ -0,0 +1,31 @@ +const crypto = require('crypto'); +const assert = require('assert'); + +const Sentry = require('@sentry/node'); + +setTimeout(() => { + process.exit(); +}, 10000); + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', + debug: true, + autoSessionTracking: false, +}); + +// eslint-disable-next-line deprecation/deprecation +Sentry.enableAnrDetection({ captureStackTrace: true, anrThreshold: 200 }).then(() => { + function longWork() { + for (let i = 0; i < 100; i++) { + const salt = crypto.randomBytes(128).toString('base64'); + // eslint-disable-next-line no-unused-vars + const hash = crypto.pbkdf2Sync('myPassword', salt, 10000, 512, 'sha512'); + assert.ok(hash); + } + } + + setTimeout(() => { + longWork(); + }, 1000); +}); diff --git a/packages/node-integration-tests/suites/anr/test-transport.js b/packages/node-integration-tests/suites/anr/test-transport.js deleted file mode 100644 index 86836cd6ab35..000000000000 --- a/packages/node-integration-tests/suites/anr/test-transport.js +++ /dev/null @@ -1,17 +0,0 @@ -const { TextEncoder, TextDecoder } = require('util'); - -const { createTransport } = require('@sentry/core'); -const { parseEnvelope } = require('@sentry/utils'); - -const textEncoder = new TextEncoder(); -const textDecoder = new TextDecoder(); - -// A transport that just logs the envelope payloads to console for checking in tests -exports.transport = () => { - return createTransport({ recordDroppedEvent: () => {}, textEncoder }, async request => { - const env = parseEnvelope(request.body, textEncoder, textDecoder); - // eslint-disable-next-line no-console - console.log(JSON.stringify(env[1][0][1])); - return { statusCode: 200 }; - }); -}; diff --git a/packages/node-integration-tests/suites/anr/test.ts b/packages/node-integration-tests/suites/anr/test.ts index 0c815c280f00..a070f611a0ab 100644 --- a/packages/node-integration-tests/suites/anr/test.ts +++ b/packages/node-integration-tests/suites/anr/test.ts @@ -2,17 +2,16 @@ import * as childProcess from 'child_process'; import * as path from 'path'; import type { Event } from '@sentry/node'; import type { SerializedSession } from '@sentry/types'; -import { parseSemver } from '@sentry/utils'; - -const NODE_VERSION = parseSemver(process.versions.node).major || 0; +import { conditionalTest } from '../../utils'; /** The output will contain logging so we need to find the line that parses as JSON */ function parseJsonLines(input: string, expected: number): T { const results = input .split('\n') .map(line => { + const trimmed = line.startsWith('[ANR Worker] ') ? line.slice(13) : line; try { - return JSON.parse(line) as T; + return JSON.parse(trimmed) as T; } catch { return undefined; } @@ -24,12 +23,9 @@ function parseJsonLines(input: string, expected: number): T return results; } -describe('should report ANR when event loop blocked', () => { +conditionalTest({ min: 16 })('should report ANR when event loop blocked', () => { test('CJS', done => { - // The stack trace is different when node < 12 - const testFramesDetails = NODE_VERSION >= 12; - - expect.assertions(testFramesDetails ? 7 : 5); + expect.assertions(13); const testScriptPath = path.resolve(__dirname, 'basic.js'); @@ -41,21 +37,46 @@ describe('should report ANR when event loop blocked', () => { expect(event.exception?.values?.[0].value).toEqual('Application Not Responding for at least 200 ms'); expect(event.exception?.values?.[0].stacktrace?.frames?.length).toBeGreaterThan(4); - if (testFramesDetails) { - expect(event.exception?.values?.[0].stacktrace?.frames?.[2].function).toEqual('?'); - expect(event.exception?.values?.[0].stacktrace?.frames?.[3].function).toEqual('longWork'); - } + expect(event.exception?.values?.[0].stacktrace?.frames?.[2].function).toEqual('?'); + expect(event.exception?.values?.[0].stacktrace?.frames?.[3].function).toEqual('longWork'); + + expect(event.contexts?.trace?.trace_id).toBeDefined(); + expect(event.contexts?.trace?.span_id).toBeDefined(); + + expect(event.contexts?.device?.arch).toBeDefined(); + expect(event.contexts?.app?.app_start_time).toBeDefined(); + expect(event.contexts?.os?.name).toBeDefined(); + expect(event.contexts?.culture?.timezone).toBeDefined(); done(); }); }); - test('ESM', done => { - if (NODE_VERSION < 14) { + test('Legacy API', done => { + // TODO (v8): Remove this old API and this test + expect.assertions(9); + + const testScriptPath = path.resolve(__dirname, 'legacy.js'); + + childProcess.exec(`node ${testScriptPath}`, { encoding: 'utf8' }, (_, stdout) => { + const [event] = parseJsonLines<[Event]>(stdout, 1); + + expect(event.exception?.values?.[0].mechanism).toEqual({ type: 'ANR' }); + expect(event.exception?.values?.[0].type).toEqual('ApplicationNotResponding'); + expect(event.exception?.values?.[0].value).toEqual('Application Not Responding for at least 200 ms'); + expect(event.exception?.values?.[0].stacktrace?.frames?.length).toBeGreaterThan(4); + + expect(event.exception?.values?.[0].stacktrace?.frames?.[2].function).toEqual('?'); + expect(event.exception?.values?.[0].stacktrace?.frames?.[3].function).toEqual('longWork'); + + expect(event.contexts?.trace?.trace_id).toBeDefined(); + expect(event.contexts?.trace?.span_id).toBeDefined(); + done(); - return; - } + }); + }); + test('ESM', done => { expect.assertions(7); const testScriptPath = path.resolve(__dirname, 'basic.mjs'); @@ -66,7 +87,7 @@ describe('should report ANR when event loop blocked', () => { expect(event.exception?.values?.[0].mechanism).toEqual({ type: 'ANR' }); expect(event.exception?.values?.[0].type).toEqual('ApplicationNotResponding'); expect(event.exception?.values?.[0].value).toEqual('Application Not Responding for at least 200 ms'); - expect(event.exception?.values?.[0].stacktrace?.frames?.length).toBeGreaterThan(4); + expect(event.exception?.values?.[0].stacktrace?.frames?.length).toBeGreaterThanOrEqual(4); expect(event.exception?.values?.[0].stacktrace?.frames?.[2].function).toEqual('?'); expect(event.exception?.values?.[0].stacktrace?.frames?.[3].function).toEqual('longWork'); @@ -75,10 +96,7 @@ describe('should report ANR when event loop blocked', () => { }); test('With session', done => { - // The stack trace is different when node < 12 - const testFramesDetails = NODE_VERSION >= 12; - - expect.assertions(testFramesDetails ? 9 : 7); + expect.assertions(9); const testScriptPath = path.resolve(__dirname, 'basic-session.js'); @@ -90,10 +108,8 @@ describe('should report ANR when event loop blocked', () => { expect(event.exception?.values?.[0].value).toEqual('Application Not Responding for at least 200 ms'); expect(event.exception?.values?.[0].stacktrace?.frames?.length).toBeGreaterThan(4); - if (testFramesDetails) { - expect(event.exception?.values?.[0].stacktrace?.frames?.[2].function).toEqual('?'); - expect(event.exception?.values?.[0].stacktrace?.frames?.[3].function).toEqual('longWork'); - } + expect(event.exception?.values?.[0].stacktrace?.frames?.[2].function).toEqual('?'); + expect(event.exception?.values?.[0].stacktrace?.frames?.[3].function).toEqual('longWork'); expect(session.status).toEqual('abnormal'); expect(session.abnormal_mechanism).toEqual('anr_foreground'); @@ -103,10 +119,7 @@ describe('should report ANR when event loop blocked', () => { }); test('from forked process', done => { - // The stack trace is different when node < 12 - const testFramesDetails = NODE_VERSION >= 12; - - expect.assertions(testFramesDetails ? 7 : 5); + expect.assertions(7); const testScriptPath = path.resolve(__dirname, 'forker.js'); @@ -118,10 +131,8 @@ describe('should report ANR when event loop blocked', () => { expect(event.exception?.values?.[0].value).toEqual('Application Not Responding for at least 200 ms'); expect(event.exception?.values?.[0].stacktrace?.frames?.length).toBeGreaterThan(4); - if (testFramesDetails) { - expect(event.exception?.values?.[0].stacktrace?.frames?.[2].function).toEqual('?'); - expect(event.exception?.values?.[0].stacktrace?.frames?.[3].function).toEqual('longWork'); - } + expect(event.exception?.values?.[0].stacktrace?.frames?.[2].function).toEqual('?'); + expect(event.exception?.values?.[0].stacktrace?.frames?.[3].function).toEqual('longWork'); done(); }); diff --git a/packages/node/rollup.anr-worker.config.mjs b/packages/node/rollup.anr-worker.config.mjs new file mode 100644 index 000000000000..9887342c63fd --- /dev/null +++ b/packages/node/rollup.anr-worker.config.mjs @@ -0,0 +1,35 @@ +import { makeBaseBundleConfig } from '@sentry-internal/rollup-utils'; + +function createAnrWorkerConfig(destDir, esm) { + return makeBaseBundleConfig({ + bundleType: 'node-worker', + entrypoints: ['src/integrations/anr/worker.ts'], + jsVersion: 'es6', + licenseTitle: '@sentry/node', + outputFileBase: () => 'worker-script.js', + packageSpecificConfig: { + output: { + dir: destDir, + sourcemap: false, + }, + plugins: [ + { + name: 'output-base64-worker-script', + renderChunk(code) { + const base64Code = Buffer.from(code).toString('base64'); + if (esm) { + return `export const base64WorkerScript = '${base64Code}';`; + } else { + return `exports.base64WorkerScript = '${base64Code}';`; + } + }, + }, + ], + }, + }); +} + +export const anrWorkerConfigs = [ + createAnrWorkerConfig('build/esm/integrations/anr', true), + createAnrWorkerConfig('build/cjs/integrations/anr', false), +]; diff --git a/packages/node/rollup.npm.config.mjs b/packages/node/rollup.npm.config.mjs index 84a06f2fb64a..88c90de4825f 100644 --- a/packages/node/rollup.npm.config.mjs +++ b/packages/node/rollup.npm.config.mjs @@ -1,3 +1,8 @@ import { makeBaseNPMConfig, makeNPMConfigVariants } from '@sentry-internal/rollup-utils'; +import { anrWorkerConfigs } from './rollup.anr-worker.config.mjs'; -export default makeNPMConfigVariants(makeBaseNPMConfig()); +export default [ + ...makeNPMConfigVariants(makeBaseNPMConfig()), + // The ANR worker builds must come after the main build because they overwrite the worker-script.js file + ...anrWorkerConfigs, +]; diff --git a/packages/node/src/anr/debugger.ts b/packages/node/src/anr/debugger.ts deleted file mode 100644 index 01b2a90fe0f9..000000000000 --- a/packages/node/src/anr/debugger.ts +++ /dev/null @@ -1,55 +0,0 @@ -import type { StackFrame } from '@sentry/types'; -import { createDebugPauseMessageHandler } from '@sentry/utils'; -import type { Debugger } from 'inspector'; - -import { getModuleFromFilename } from '../module'; -import { createWebSocketClient } from './websocket'; - -// The only messages we care about -type DebugMessage = - | { - method: 'Debugger.scriptParsed'; - params: Debugger.ScriptParsedEventDataType; - } - | { method: 'Debugger.paused'; params: Debugger.PausedEventDataType }; - -/** - * Wraps a websocket connection with the basic logic of the Node debugger protocol. - * @param url The URL to connect to - * @param onMessage A callback that will be called with each return message from the debugger - * @returns A function that can be used to send commands to the debugger - */ -async function webSocketDebugger( - url: string, - onMessage: (message: DebugMessage) => void, -): Promise<(method: string) => void> { - let id = 0; - const webSocket = await createWebSocketClient(url); - - webSocket.on('message', (data: Buffer) => { - const message = JSON.parse(data.toString()) as DebugMessage; - onMessage(message); - }); - - return (method: string) => { - webSocket.send(JSON.stringify({ id: id++, method })); - }; -} - -/** - * Captures stack traces from the Node debugger. - * @param url The URL to connect to - * @param callback A callback that will be called with the stack frames - * @returns A function that triggers the debugger to pause and capture a stack trace - */ -export async function captureStackTrace(url: string, callback: (frames: StackFrame[]) => void): Promise<() => void> { - const sendCommand: (method: string) => void = await webSocketDebugger( - url, - createDebugPauseMessageHandler(cmd => sendCommand(cmd), getModuleFromFilename, callback), - ); - - return () => { - sendCommand('Debugger.enable'); - sendCommand('Debugger.pause'); - }; -} diff --git a/packages/node/src/anr/index.ts b/packages/node/src/anr/index.ts deleted file mode 100644 index 13ac5c52c6ef..000000000000 --- a/packages/node/src/anr/index.ts +++ /dev/null @@ -1,308 +0,0 @@ -import { spawn } from 'child_process'; -import { getClient, getCurrentScope, makeSession, updateSession } from '@sentry/core'; -import type { Event, Session, StackFrame } from '@sentry/types'; -import { logger, watchdogTimer } from '@sentry/utils'; - -import { addEventProcessor, captureEvent, flush } from '..'; -import { captureStackTrace } from './debugger'; - -const DEFAULT_INTERVAL = 50; -const DEFAULT_HANG_THRESHOLD = 5000; - -interface Options { - /** - * The app entry script. This is used to run the same script as the child process. - * - * Defaults to `process.argv[1]`. - */ - entryScript: string; - /** - * Interval to send heartbeat messages to the child process. - * - * Defaults to 50ms. - */ - pollInterval: number; - /** - * Threshold in milliseconds to trigger an ANR event. - * - * Defaults to 5000ms. - */ - anrThreshold: number; - /** - * Whether to capture a stack trace when the ANR event is triggered. - * - * Defaults to `false`. - * - * This uses the node debugger which enables the inspector API and opens the required ports. - */ - captureStackTrace: boolean; - /** - * @deprecated Use 'init' debug option instead - */ - debug: boolean; -} - -function createAnrEvent(blockedMs: number, frames?: StackFrame[]): Event { - return { - level: 'error', - exception: { - values: [ - { - type: 'ApplicationNotResponding', - value: `Application Not Responding for at least ${blockedMs} ms`, - stacktrace: { frames }, - mechanism: { - // This ensures the UI doesn't say 'Crashed in' for the stack trace - type: 'ANR', - }, - }, - ], - }, - }; -} - -interface InspectorApi { - open: (port: number) => void; - url: () => string | undefined; -} - -/** - * Starts the node debugger and returns the inspector url. - * - * When inspector.url() returns undefined, it means the port is already in use so we try the next port. - */ -function startInspector(startPort: number = 9229): string | undefined { - // eslint-disable-next-line @typescript-eslint/no-var-requires - const inspector: InspectorApi = require('inspector'); - let inspectorUrl: string | undefined = undefined; - let port = startPort; - - while (inspectorUrl === undefined && port < startPort + 100) { - inspector.open(port); - inspectorUrl = inspector.url(); - port++; - } - - return inspectorUrl; -} - -function startChildProcess(options: Options): void { - function log(message: string, ...args: unknown[]): void { - logger.log(`[ANR] ${message}`, ...args); - } - - try { - const env = { ...process.env }; - env.SENTRY_ANR_CHILD_PROCESS = 'true'; - - if (options.captureStackTrace) { - env.SENTRY_INSPECT_URL = startInspector(); - } - - log(`Spawning child process with execPath:'${process.execPath}' and entryScript:'${options.entryScript}'`); - - const child = spawn(process.execPath, [options.entryScript], { - env, - stdio: logger.isEnabled() ? ['inherit', 'inherit', 'inherit', 'ipc'] : ['ignore', 'ignore', 'ignore', 'ipc'], - }); - // The child process should not keep the main process alive - child.unref(); - - const timer = setInterval(() => { - try { - const currentSession = getCurrentScope()?.getSession(); - // We need to copy the session object and remove the toJSON method so it can be sent to the child process - // serialized without making it a SerializedSession - const session = currentSession ? { ...currentSession, toJSON: undefined } : undefined; - // message the child process to tell it the main event loop is still running - child.send({ session }); - } catch (_) { - // - } - }, options.pollInterval); - - child.on('message', (msg: string) => { - if (msg === 'session-ended') { - log('ANR event sent from child process. Clearing session in this process.'); - getCurrentScope()?.setSession(undefined); - } - }); - - const end = (type: string): ((...args: unknown[]) => void) => { - return (...args): void => { - clearInterval(timer); - log(`Child process ${type}`, ...args); - }; - }; - - child.on('error', end('error')); - child.on('disconnect', end('disconnect')); - child.on('exit', end('exit')); - } catch (e) { - log('Failed to start child process', e); - } -} - -function createHrTimer(): { getTimeMs: () => number; reset: () => void } { - let lastPoll = process.hrtime(); - - return { - getTimeMs: (): number => { - const [seconds, nanoSeconds] = process.hrtime(lastPoll); - return Math.floor(seconds * 1e3 + nanoSeconds / 1e6); - }, - reset: (): void => { - lastPoll = process.hrtime(); - }, - }; -} - -function handleChildProcess(options: Options): void { - process.title = 'sentry-anr'; - - function log(message: string): void { - logger.log(`[ANR child process] ${message}`); - } - - log('Started'); - let session: Session | undefined; - - function sendAnrEvent(frames?: StackFrame[]): void { - if (session) { - log('Sending abnormal session'); - updateSession(session, { status: 'abnormal', abnormal_mechanism: 'anr_foreground' }); - getClient()?.sendSession(session); - - try { - // Notify the main process that the session has ended so the session can be cleared from the scope - process.send?.('session-ended'); - } catch (_) { - // ignore - } - } - - captureEvent(createAnrEvent(options.anrThreshold, frames)); - - flush(3000).then( - () => { - // We only capture one event to avoid spamming users with errors - process.exit(); - }, - () => { - // We only capture one event to avoid spamming users with errors - process.exit(); - }, - ); - } - - addEventProcessor(event => { - // Strip sdkProcessingMetadata from all child process events to remove trace info - delete event.sdkProcessingMetadata; - event.tags = { - ...event.tags, - 'process.name': 'ANR', - }; - return event; - }); - - let debuggerPause: Promise<() => void> | undefined; - - // if attachStackTrace is enabled, we'll have a debugger url to connect to - if (process.env.SENTRY_INSPECT_URL) { - log('Connecting to debugger'); - - debuggerPause = captureStackTrace(process.env.SENTRY_INSPECT_URL, frames => { - log('Capturing event with stack frames'); - sendAnrEvent(frames); - }); - } - - async function watchdogTimeout(): Promise { - log('Watchdog timeout'); - - try { - const pauseAndCapture = await debuggerPause; - - if (pauseAndCapture) { - log('Pausing debugger to capture stack trace'); - pauseAndCapture(); - return; - } - } catch (_) { - // ignore - } - - log('Capturing event'); - sendAnrEvent(); - } - - const { poll } = watchdogTimer(createHrTimer, options.pollInterval, options.anrThreshold, watchdogTimeout); - - process.on('message', (msg: { session: Session | undefined }) => { - if (msg.session) { - session = makeSession(msg.session); - } - poll(); - }); - process.on('disconnect', () => { - // Parent process has exited. - process.exit(); - }); -} - -/** - * Returns true if the current process is an ANR child process. - */ -export function isAnrChildProcess(): boolean { - return !!process.send && !!process.env.SENTRY_ANR_CHILD_PROCESS; -} - -/** - * **Note** This feature is still in beta so there may be breaking changes in future releases. - * - * Starts a child process that detects Application Not Responding (ANR) errors. - * - * It's important to await on the returned promise before your app code to ensure this code does not run in the ANR - * child process. - * - * ```js - * import { init, enableAnrDetection } from '@sentry/node'; - * - * init({ dsn: "__DSN__" }); - * - * // with ESM + Node 14+ - * await enableAnrDetection({ captureStackTrace: true }); - * runApp(); - * - * // with CJS or Node 10+ - * enableAnrDetection({ captureStackTrace: true }).then(() => { - * runApp(); - * }); - * ``` - */ -export function enableAnrDetection(options: Partial): Promise { - // When pm2 runs the script in cluster mode, process.argv[1] is the pm2 script and process.env.pm_exec_path is the - // path to the entry script - const entryScript = options.entryScript || process.env.pm_exec_path || process.argv[1]; - - const anrOptions: Options = { - entryScript, - pollInterval: options.pollInterval || DEFAULT_INTERVAL, - anrThreshold: options.anrThreshold || DEFAULT_HANG_THRESHOLD, - captureStackTrace: !!options.captureStackTrace, - // eslint-disable-next-line deprecation/deprecation - debug: !!options.debug, - }; - - if (isAnrChildProcess()) { - handleChildProcess(anrOptions); - // In the child process, the promise never resolves which stops the app code from running - return new Promise(() => { - // Never resolve - }); - } else { - startChildProcess(anrOptions); - // In the main process, the promise resolves immediately - return Promise.resolve(); - } -} diff --git a/packages/node/src/anr/websocket.ts b/packages/node/src/anr/websocket.ts deleted file mode 100644 index 7229f0fc07e7..000000000000 --- a/packages/node/src/anr/websocket.ts +++ /dev/null @@ -1,366 +0,0 @@ -/* eslint-disable no-bitwise */ -/** - * A simple WebSocket client implementation copied from Rome before being modified for our use: - * https://github.com/jeremyBanks/rome/tree/b034dd22d5f024f87c50eef2872e22b3ad48973a/packages/%40romejs/codec-websocket - * - * Original license: - * - * MIT License - * - * Copyright (c) Facebook, Inc. and its affiliates. - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ - -import * as crypto from 'crypto'; -import { EventEmitter } from 'events'; -import * as http from 'http'; -import type { Socket } from 'net'; -import * as url from 'url'; - -type BuildFrameOpts = { - opcode: number; - fin: boolean; - data: Buffer; -}; - -type Frame = { - fin: boolean; - opcode: number; - mask: undefined | Buffer; - payload: Buffer; - payloadLength: number; -}; - -const OPCODES = { - CONTINUATION: 0, - TEXT: 1, - BINARY: 2, - TERMINATE: 8, - PING: 9, - PONG: 10, -}; - -const GUID = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11'; - -function isCompleteFrame(frame: Frame): boolean { - return Buffer.byteLength(frame.payload) >= frame.payloadLength; -} - -function unmaskPayload(payload: Buffer, mask: undefined | Buffer, offset: number): Buffer { - if (mask === undefined) { - return payload; - } - - for (let i = 0; i < payload.length; i++) { - payload[i] ^= mask[(offset + i) & 3]; - } - - return payload; -} - -function buildFrame(opts: BuildFrameOpts): Buffer { - const { opcode, fin, data } = opts; - - let offset = 6; - let dataLength = data.length; - - if (dataLength >= 65_536) { - offset += 8; - dataLength = 127; - } else if (dataLength > 125) { - offset += 2; - dataLength = 126; - } - - const head = Buffer.allocUnsafe(offset); - - head[0] = fin ? opcode | 128 : opcode; - head[1] = dataLength; - - if (dataLength === 126) { - head.writeUInt16BE(data.length, 2); - } else if (dataLength === 127) { - head.writeUInt32BE(0, 2); - head.writeUInt32BE(data.length, 6); - } - - const mask = crypto.randomBytes(4); - head[1] |= 128; - head[offset - 4] = mask[0]; - head[offset - 3] = mask[1]; - head[offset - 2] = mask[2]; - head[offset - 1] = mask[3]; - - const masked = Buffer.alloc(dataLength); - for (let i = 0; i < dataLength; ++i) { - masked[i] = data[i] ^ mask[i & 3]; - } - - return Buffer.concat([head, masked]); -} - -function parseFrame(buffer: Buffer): Frame { - const firstByte = buffer.readUInt8(0); - const isFinalFrame: boolean = Boolean((firstByte >>> 7) & 1); - const opcode: number = firstByte & 15; - - const secondByte: number = buffer.readUInt8(1); - const isMasked: boolean = Boolean((secondByte >>> 7) & 1); - - // Keep track of our current position as we advance through the buffer - let currentOffset = 2; - let payloadLength = secondByte & 127; - if (payloadLength > 125) { - if (payloadLength === 126) { - payloadLength = buffer.readUInt16BE(currentOffset); - currentOffset += 2; - } else if (payloadLength === 127) { - const leftPart = buffer.readUInt32BE(currentOffset); - currentOffset += 4; - - // The maximum safe integer in JavaScript is 2^53 - 1. An error is returned - - // if payload length is greater than this number. - if (leftPart >= Number.MAX_SAFE_INTEGER) { - throw new Error('Unsupported WebSocket frame: payload length > 2^53 - 1'); - } - - const rightPart = buffer.readUInt32BE(currentOffset); - currentOffset += 4; - - payloadLength = leftPart * Math.pow(2, 32) + rightPart; - } else { - throw new Error('Unknown payload length'); - } - } - - // Get the masking key if one exists - let mask; - if (isMasked) { - mask = buffer.slice(currentOffset, currentOffset + 4); - currentOffset += 4; - } - - const payload = unmaskPayload(buffer.slice(currentOffset), mask, 0); - - return { - fin: isFinalFrame, - opcode, - mask, - payload, - payloadLength, - }; -} - -function createKey(key: string): string { - return crypto.createHash('sha1').update(`${key}${GUID}`).digest('base64'); -} - -class WebSocketInterface extends EventEmitter { - private _alive: boolean; - private _incompleteFrame: undefined | Frame; - private _unfinishedFrame: undefined | Frame; - private _socket: Socket; - - public constructor(socket: Socket) { - super(); - // When a frame is set here then any additional continuation frames payloads will be appended - this._unfinishedFrame = undefined; - - // When a frame is set here, all additional chunks will be appended until we reach the correct payloadLength - this._incompleteFrame = undefined; - - this._socket = socket; - this._alive = true; - - socket.on('data', buff => { - this._addBuffer(buff); - }); - - socket.on('error', (err: NodeJS.ErrnoException) => { - if (err.code === 'ECONNRESET') { - this.emit('close'); - } else { - this.emit('error'); - } - }); - - socket.on('close', () => { - this.end(); - }); - } - - public end(): void { - if (!this._alive) { - return; - } - - this._alive = false; - this.emit('close'); - this._socket.end(); - } - - public send(buff: string): void { - this._sendFrame({ - opcode: OPCODES.TEXT, - fin: true, - data: Buffer.from(buff), - }); - } - - private _sendFrame(frameOpts: BuildFrameOpts): void { - this._socket.write(buildFrame(frameOpts)); - } - - private _completeFrame(frame: Frame): void { - // If we have an unfinished frame then only allow continuations - const { _unfinishedFrame: unfinishedFrame } = this; - if (unfinishedFrame !== undefined) { - if (frame.opcode === OPCODES.CONTINUATION) { - unfinishedFrame.payload = Buffer.concat([ - unfinishedFrame.payload, - unmaskPayload(frame.payload, unfinishedFrame.mask, unfinishedFrame.payload.length), - ]); - - if (frame.fin) { - this._unfinishedFrame = undefined; - this._completeFrame(unfinishedFrame); - } - return; - } else { - // Silently ignore the previous frame... - this._unfinishedFrame = undefined; - } - } - - if (frame.fin) { - if (frame.opcode === OPCODES.PING) { - this._sendFrame({ - opcode: OPCODES.PONG, - fin: true, - data: frame.payload, - }); - } else { - // Trim off any excess payload - let excess; - if (frame.payload.length > frame.payloadLength) { - excess = frame.payload.slice(frame.payloadLength); - frame.payload = frame.payload.slice(0, frame.payloadLength); - } - - this.emit('message', frame.payload); - - if (excess !== undefined) { - this._addBuffer(excess); - } - } - } else { - this._unfinishedFrame = frame; - } - } - - private _addBufferToIncompleteFrame(incompleteFrame: Frame, buff: Buffer): void { - incompleteFrame.payload = Buffer.concat([ - incompleteFrame.payload, - unmaskPayload(buff, incompleteFrame.mask, incompleteFrame.payload.length), - ]); - - if (isCompleteFrame(incompleteFrame)) { - this._incompleteFrame = undefined; - this._completeFrame(incompleteFrame); - } - } - - private _addBuffer(buff: Buffer): void { - // Check if we're still waiting for the rest of a payload - const { _incompleteFrame: incompleteFrame } = this; - if (incompleteFrame !== undefined) { - this._addBufferToIncompleteFrame(incompleteFrame, buff); - return; - } - - // There needs to be atleast two values in the buffer for us to parse - // a frame from it. - // See: https://github.com/getsentry/sentry-javascript/issues/9307 - if (buff.length <= 1) { - return; - } - - const frame = parseFrame(buff); - - if (isCompleteFrame(frame)) { - // Frame has been completed! - this._completeFrame(frame); - } else { - this._incompleteFrame = frame; - } - } -} - -/** - * Creates a WebSocket client - */ -export async function createWebSocketClient(rawUrl: string): Promise { - const parts = url.parse(rawUrl); - - return new Promise((resolve, reject) => { - const key = crypto.randomBytes(16).toString('base64'); - const digest = createKey(key); - - const req = http.request({ - hostname: parts.hostname, - port: parts.port, - path: parts.path, - method: 'GET', - headers: { - Connection: 'Upgrade', - Upgrade: 'websocket', - 'Sec-WebSocket-Key': key, - 'Sec-WebSocket-Version': '13', - }, - }); - - req.on('response', (res: http.IncomingMessage) => { - if (res.statusCode && res.statusCode >= 400) { - process.stderr.write(`Unexpected HTTP code: ${res.statusCode}\n`); - res.pipe(process.stderr); - } else { - res.pipe(process.stderr); - } - }); - - req.on('upgrade', (res: http.IncomingMessage, socket: Socket) => { - if (res.headers['sec-websocket-accept'] !== digest) { - socket.end(); - reject(new Error(`Digest mismatch ${digest} !== ${res.headers['sec-websocket-accept']}`)); - return; - } - - const client = new WebSocketInterface(socket); - resolve(client); - }); - - req.on('error', err => { - reject(err); - }); - - req.end(); - }); -} diff --git a/packages/node/src/index.ts b/packages/node/src/index.ts index 06524bcd0c0a..af73f34df7bc 100644 --- a/packages/node/src/index.ts +++ b/packages/node/src/index.ts @@ -79,7 +79,8 @@ export { defaultIntegrations, init, defaultStackParser, getSentryRelease } from export { addRequestDataToEvent, DEFAULT_USER_INCLUDES, extractRequestData } from '@sentry/utils'; export { deepReadDirSync } from './utils'; export { getModuleFromFilename } from './module'; -export { enableAnrDetection, isAnrChildProcess } from './anr'; +// eslint-disable-next-line deprecation/deprecation +export { enableAnrDetection } from './integrations/anr/legacy'; import { Integrations as CoreIntegrations } from '@sentry/core'; diff --git a/packages/node/src/integrations/anr/common.ts b/packages/node/src/integrations/anr/common.ts new file mode 100644 index 000000000000..38583dfacaaf --- /dev/null +++ b/packages/node/src/integrations/anr/common.ts @@ -0,0 +1,34 @@ +import type { Contexts, DsnComponents, SdkMetadata } from '@sentry/types'; + +export interface Options { + /** + * Interval to send heartbeat messages to the ANR worker. + * + * Defaults to 50ms. + */ + pollInterval: number; + /** + * Threshold in milliseconds to trigger an ANR event. + * + * Defaults to 5000ms. + */ + anrThreshold: number; + /** + * Whether to capture a stack trace when the ANR event is triggered. + * + * Defaults to `false`. + * + * This uses the node debugger which enables the inspector API and opens the required ports. + */ + captureStackTrace: boolean; +} + +export interface WorkerStartData extends Options { + debug: boolean; + sdkMetadata: SdkMetadata; + dsn: DsnComponents; + release: string | undefined; + environment: string; + dist: string | undefined; + contexts: Contexts; +} diff --git a/packages/node/src/integrations/anr/index.ts b/packages/node/src/integrations/anr/index.ts new file mode 100644 index 000000000000..4a623fb3ff0b --- /dev/null +++ b/packages/node/src/integrations/anr/index.ts @@ -0,0 +1,155 @@ +// TODO (v8): This import can be removed once we only support Node with global URL +import { URL } from 'url'; +import { getCurrentScope } from '@sentry/core'; +import type { Contexts, Event, EventHint, Integration } from '@sentry/types'; +import { dynamicRequire, logger } from '@sentry/utils'; +import type { Worker, WorkerOptions } from 'worker_threads'; +import type { NodeClient } from '../../client'; +import { NODE_VERSION } from '../../nodeVersion'; +import type { Options, WorkerStartData } from './common'; +import { base64WorkerScript } from './worker-script'; + +const DEFAULT_INTERVAL = 50; +const DEFAULT_HANG_THRESHOLD = 5000; + +type WorkerNodeV14 = Worker & { new (filename: string | URL, options?: WorkerOptions): Worker }; + +type WorkerThreads = { + Worker: WorkerNodeV14; +}; + +function log(message: string, ...args: unknown[]): void { + logger.log(`[ANR] ${message}`, ...args); +} + +/** + * We need to use dynamicRequire because worker_threads is not available in node < v12 and webpack error will when + * targeting those versions + */ +function getWorkerThreads(): WorkerThreads { + return dynamicRequire(module, 'worker_threads'); +} + +/** + * Gets contexts by calling all event processors. This relies on being called after all integrations are setup + */ +async function getContexts(client: NodeClient): Promise { + let event: Event | null = { message: 'ANR' }; + const eventHint: EventHint = {}; + + for (const processor of client.getEventProcessors()) { + if (event === null) break; + event = await processor(event, eventHint); + } + + return event?.contexts || {}; +} + +interface InspectorApi { + open: (port: number) => void; + url: () => string | undefined; +} + +/** + * Starts a thread to detect App Not Responding (ANR) events + */ +export class Anr implements Integration { + public name: string = 'Anr'; + + public constructor(private readonly _options: Partial = {}) {} + + /** @inheritdoc */ + public setupOnce(): void { + // Do nothing + } + + /** @inheritdoc */ + public setup(client: NodeClient): void { + if ((NODE_VERSION.major || 0) < 16) { + throw new Error('ANR detection requires Node 16 or later'); + } + + // setImmediate is used to ensure that all other integrations have been setup + setImmediate(() => this._startWorker(client)); + } + + /** + * Starts the ANR worker thread + */ + private async _startWorker(client: NodeClient): Promise { + const contexts = await getContexts(client); + const dsn = client.getDsn(); + + if (!dsn) { + return; + } + + // These will not be accurate if sent later from the worker thread + delete contexts.app?.app_memory; + delete contexts.device?.free_memory; + + const initOptions = client.getOptions(); + + const sdkMetadata = client.getSdkMetadata() || {}; + if (sdkMetadata.sdk) { + sdkMetadata.sdk.integrations = initOptions.integrations.map(i => i.name); + } + + const options: WorkerStartData = { + debug: logger.isEnabled(), + dsn, + environment: initOptions.environment || 'production', + release: initOptions.release, + dist: initOptions.dist, + sdkMetadata, + pollInterval: this._options.pollInterval || DEFAULT_INTERVAL, + anrThreshold: this._options.anrThreshold || DEFAULT_HANG_THRESHOLD, + captureStackTrace: !!this._options.captureStackTrace, + contexts, + }; + + if (options.captureStackTrace) { + // eslint-disable-next-line @typescript-eslint/no-var-requires + const inspector: InspectorApi = require('inspector'); + inspector.open(0); + } + + const { Worker } = getWorkerThreads(); + + const worker = new Worker(new URL(`data:application/javascript;base64,${base64WorkerScript}`), { + workerData: options, + }); + // Ensure this thread can't block app exit + worker.unref(); + + const timer = setInterval(() => { + try { + const currentSession = getCurrentScope().getSession(); + // We need to copy the session object and remove the toJSON method so it can be sent to the worker + // serialized without making it a SerializedSession + const session = currentSession ? { ...currentSession, toJSON: undefined } : undefined; + // message the worker to tell it the main event loop is still running + worker.postMessage({ session }); + } catch (_) { + // + } + }, options.pollInterval); + + worker.on('message', (msg: string) => { + if (msg === 'session-ended') { + log('ANR event sent from ANR worker. Clearing session in this thread.'); + getCurrentScope().setSession(undefined); + } + }); + + worker.once('error', (err: Error) => { + clearInterval(timer); + log('ANR worker error', err); + }); + + worker.once('exit', (code: number) => { + clearInterval(timer); + log('ANR worker exit', code); + }); + } +} diff --git a/packages/node/src/integrations/anr/legacy.ts b/packages/node/src/integrations/anr/legacy.ts new file mode 100644 index 000000000000..1d1ebc3024e3 --- /dev/null +++ b/packages/node/src/integrations/anr/legacy.ts @@ -0,0 +1,32 @@ +import { getClient } from '@sentry/core'; +import { Anr } from '.'; +import type { NodeClient } from '../../client'; + +// TODO (v8): Remove this entire file and the `enableAnrDetection` export + +interface LegacyOptions { + entryScript: string; + pollInterval: number; + anrThreshold: number; + captureStackTrace: boolean; + debug: boolean; +} + +/** + * @deprecated Use the `Anr` integration instead. + * + * ```ts + * import * as Sentry from '@sentry/node'; + * + * Sentry.init({ + * dsn: '__DSN__', + * integrations: [new Sentry.Integrations.Anr({ captureStackTrace: true })], + * }); + * ``` + */ +export function enableAnrDetection(options: Partial): Promise { + const client = getClient() as NodeClient; + const integration = new Anr(options); + integration.setup(client); + return Promise.resolve(); +} diff --git a/packages/node/src/integrations/anr/worker-script.ts b/packages/node/src/integrations/anr/worker-script.ts new file mode 100644 index 000000000000..16394eaacfe1 --- /dev/null +++ b/packages/node/src/integrations/anr/worker-script.ts @@ -0,0 +1,2 @@ +// This file is a placeholder that gets overwritten in the build directory. +export const base64WorkerScript = ''; diff --git a/packages/node/src/integrations/anr/worker.ts b/packages/node/src/integrations/anr/worker.ts new file mode 100644 index 000000000000..142fe0d608e7 --- /dev/null +++ b/packages/node/src/integrations/anr/worker.ts @@ -0,0 +1,215 @@ +import { + createEventEnvelope, + createSessionEnvelope, + getEnvelopeEndpointWithUrlEncodedAuth, + makeSession, + updateSession, +} from '@sentry/core'; +import type { Event, Session, StackFrame, TraceContext } from '@sentry/types'; +import { callFrameToStackFrame, stripSentryFramesAndReverse, watchdogTimer } from '@sentry/utils'; +import { Session as InspectorSession } from 'inspector'; +import { parentPort, workerData } from 'worker_threads'; +import { makeNodeTransport } from '../../transports'; +import type { WorkerStartData } from './common'; + +type VoidFunction = () => void; +type InspectorSessionNodeV12 = InspectorSession & { connectToMainThread: VoidFunction }; + +const options: WorkerStartData = workerData; +let session: Session | undefined; +let hasSentAnrEvent = false; + +function log(msg: string): void { + if (options.debug) { + // eslint-disable-next-line no-console + console.log(`[ANR Worker] ${msg}`); + } +} + +const url = getEnvelopeEndpointWithUrlEncodedAuth(options.dsn); +const transport = makeNodeTransport({ + url, + recordDroppedEvent: () => { + // + }, +}); + +async function sendAbnormalSession(): Promise { + // of we have an existing session passed from the main thread, send it as abnormal + if (session) { + log('Sending abnormal session'); + updateSession(session, { status: 'abnormal', abnormal_mechanism: 'anr_foreground' }); + + log(JSON.stringify(session)); + + const envelope = createSessionEnvelope(session, options.dsn, options.sdkMetadata); + await transport.send(envelope); + + try { + // Notify the main process that the session has ended so the session can be cleared from the scope + parentPort?.postMessage('session-ended'); + } catch (_) { + // ignore + } + } +} + +log('Started'); + +async function sendAnrEvent(frames?: StackFrame[], traceContext?: TraceContext): Promise { + if (hasSentAnrEvent) { + return; + } + + hasSentAnrEvent = true; + + await sendAbnormalSession(); + + log('Sending event'); + + const event: Event = { + sdk: options.sdkMetadata.sdk, + contexts: { ...options.contexts, trace: traceContext }, + release: options.release, + environment: options.environment, + dist: options.dist, + platform: 'node', + level: 'error', + exception: { + values: [ + { + type: 'ApplicationNotResponding', + value: `Application Not Responding for at least ${options.anrThreshold} ms`, + stacktrace: { frames }, + // This ensures the UI doesn't say 'Crashed in' for the stack trace + mechanism: { type: 'ANR' }, + }, + ], + }, + tags: { 'process.name': 'ANR' }, + }; + + log(JSON.stringify(event)); + + const envelope = createEventEnvelope(event, options.dsn, options.sdkMetadata); + await transport.send(envelope); + await transport.flush(2000); + + // Delay for 5 seconds so that stdio can flush in the main event loop ever restarts. + // This is mainly for the benefit of logging/debugging issues. + setTimeout(() => { + process.exit(0); + }, 5_000); +} + +let debuggerPause: VoidFunction | undefined; + +if (options.captureStackTrace) { + log('Connecting to debugger'); + + const session = new InspectorSession() as InspectorSessionNodeV12; + session.connectToMainThread(); + + log('Connected to debugger'); + + // Collect scriptId -> url map so we can look up the filenames later + const scripts = new Map(); + + session.on('Debugger.scriptParsed', event => { + scripts.set(event.params.scriptId, event.params.url); + }); + + session.on('Debugger.paused', event => { + if (event.params.reason !== 'other') { + return; + } + + try { + log('Debugger paused'); + + // copy the frames + const callFrames = [...event.params.callFrames]; + + const stackFrames = stripSentryFramesAndReverse( + callFrames.map(frame => callFrameToStackFrame(frame, scripts.get(frame.location.scriptId), () => undefined)), + ); + + // Evaluate a script in the currently paused context + session.post( + 'Runtime.evaluate', + { + // Grab the trace context from the current scope + expression: + 'const ctx = __SENTRY__.hub.getScope().getPropagationContext(); ctx.traceId + "-" + ctx.spanId + "-" + ctx.parentSpanId', + // Don't re-trigger the debugger if this causes an error + silent: true, + }, + (_, param) => { + const traceId = param && param.result ? (param.result.value as string) : '--'; + const [trace_id, span_id, parent_span_id] = traceId.split('-') as (string | undefined)[]; + + session.post('Debugger.resume'); + session.post('Debugger.disable'); + + const context = trace_id?.length && span_id?.length ? { trace_id, span_id, parent_span_id } : undefined; + sendAnrEvent(stackFrames, context).then(null, () => { + log('Sending ANR event failed.'); + }); + }, + ); + } catch (e) { + session.post('Debugger.resume'); + session.post('Debugger.disable'); + throw e; + } + }); + + debuggerPause = () => { + try { + session.post('Debugger.enable', () => { + session.post('Debugger.pause'); + }); + } catch (_) { + // + } + }; +} + +function createHrTimer(): { getTimeMs: () => number; reset: VoidFunction } { + // TODO (v8): We can use process.hrtime.bigint() after we drop node v8 + let lastPoll = process.hrtime(); + + return { + getTimeMs: (): number => { + const [seconds, nanoSeconds] = process.hrtime(lastPoll); + return Math.floor(seconds * 1e3 + nanoSeconds / 1e6); + }, + reset: (): void => { + lastPoll = process.hrtime(); + }, + }; +} + +function watchdogTimeout(): void { + log('Watchdog timeout'); + + if (debuggerPause) { + log('Pausing debugger to capture stack trace'); + debuggerPause(); + } else { + log('Capturing event without a stack trace'); + sendAnrEvent().then(null, () => { + log('Sending ANR event failed on watchdog timeout.'); + }); + } +} + +const { poll } = watchdogTimer(createHrTimer, options.pollInterval, options.anrThreshold, watchdogTimeout); + +parentPort?.on('message', (msg: { session: Session | undefined }) => { + if (msg.session) { + session = makeSession(msg.session); + } + + poll(); +}); diff --git a/packages/node/src/integrations/index.ts b/packages/node/src/integrations/index.ts index f2ac9c25b807..63cf685d2bc5 100644 --- a/packages/node/src/integrations/index.ts +++ b/packages/node/src/integrations/index.ts @@ -9,4 +9,5 @@ export { RequestData } from '@sentry/core'; export { LocalVariables } from './localvariables'; export { Undici } from './undici'; export { Spotlight } from './spotlight'; +export { Anr } from './anr'; export { Hapi } from './hapi'; diff --git a/packages/node/src/sdk.ts b/packages/node/src/sdk.ts index 07fd3f8b024a..c9a1c108dfdd 100644 --- a/packages/node/src/sdk.ts +++ b/packages/node/src/sdk.ts @@ -17,7 +17,6 @@ import { tracingContextFromHeaders, } from '@sentry/utils'; -import { isAnrChildProcess } from './anr'; import { setNodeAsyncContextStrategy } from './async'; import { NodeClient } from './client'; import { @@ -114,11 +113,6 @@ export const defaultIntegrations = [ */ // eslint-disable-next-line complexity export function init(options: NodeOptions = {}): void { - if (isAnrChildProcess()) { - options.autoSessionTracking = false; - options.tracesSampleRate = 0; - } - const carrier = getMainCarrier(); setNodeAsyncContextStrategy(); diff --git a/packages/rollup-utils/bundleHelpers.mjs b/packages/rollup-utils/bundleHelpers.mjs index 93a84670a6ff..b6ca7c8fcbc7 100644 --- a/packages/rollup-utils/bundleHelpers.mjs +++ b/packages/rollup-utils/bundleHelpers.mjs @@ -102,6 +102,15 @@ export function makeBaseBundleConfig(options) { external: builtinModules, }; + const workerBundleConfig = { + output: { + format: 'esm', + }, + plugins: [commonJSPlugin, makeTerserPlugin()], + // Don't bundle any of Node's core modules + external: builtinModules, + }; + // used by all bundles const sharedBundleConfig = { input: entrypoints, @@ -123,6 +132,7 @@ export function makeBaseBundleConfig(options) { standalone: standAloneBundleConfig, addon: addOnBundleConfig, node: nodeBundleConfig, + 'node-worker': workerBundleConfig, }; return deepMerge.all([sharedBundleConfig, bundleTypeConfigMap[bundleType], packageSpecificConfig || {}], { diff --git a/packages/utils/src/anr.ts b/packages/utils/src/anr.ts index 89990c3414f7..d962107bcb0e 100644 --- a/packages/utils/src/anr.ts +++ b/packages/utils/src/anr.ts @@ -1,7 +1,7 @@ import type { StackFrame } from '@sentry/types'; import { dropUndefinedKeys } from './object'; -import { filenameIsInApp, stripSentryFramesAndReverse } from './stacktrace'; +import { filenameIsInApp } from './stacktrace'; type WatchdogReturn = { /** Resets the watchdog timer */ @@ -67,20 +67,10 @@ interface CallFrame { url: string; } -interface ScriptParsedEventDataType { - scriptId: string; - url: string; -} - -interface PausedEventDataType { - callFrames: CallFrame[]; - reason: string; -} - /** * Converts Debugger.CallFrame to Sentry StackFrame */ -function callFrameToStackFrame( +export function callFrameToStackFrame( frame: CallFrame, url: string | undefined, getModuleFromFilename: (filename: string | undefined) => string | undefined, @@ -100,40 +90,3 @@ function callFrameToStackFrame( in_app: filename ? filenameIsInApp(filename) : undefined, }); } - -// The only messages we care about -type DebugMessage = - | { method: 'Debugger.scriptParsed'; params: ScriptParsedEventDataType } - | { method: 'Debugger.paused'; params: PausedEventDataType }; - -/** - * Creates a message handler from the v8 debugger protocol and passed stack frames to the callback when paused. - */ -export function createDebugPauseMessageHandler( - sendCommand: (message: string) => void, - getModuleFromFilename: (filename?: string) => string | undefined, - pausedStackFrames: (frames: StackFrame[]) => void, -): (message: DebugMessage) => void { - // Collect scriptId -> url map so we can look up the filenames later - const scripts = new Map(); - - return message => { - if (message.method === 'Debugger.scriptParsed') { - scripts.set(message.params.scriptId, message.params.url); - } else if (message.method === 'Debugger.paused') { - // copy the frames - const callFrames = [...message.params.callFrames]; - // and resume immediately - sendCommand('Debugger.resume'); - sendCommand('Debugger.disable'); - - const stackFrames = stripSentryFramesAndReverse( - callFrames.map(frame => - callFrameToStackFrame(frame, scripts.get(frame.location.scriptId), getModuleFromFilename), - ), - ); - - pausedStackFrames(stackFrames); - } - }; -} From eb9bc5632a97b6e093d44c798103db6159af8d74 Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Thu, 21 Dec 2023 11:29:01 +0100 Subject: [PATCH 10/24] feat(core): Add `getGlobalScope()` method (#9920) This scope lives in module scope and is applied to _all_ events. Please review this carefully, as it is important that data is correctly applied etc. There should be a decent amount of tests covering all of this, but just to make sure. This was mostly ported/extracted from node-experimental. --- packages/core/src/index.ts | 4 +- packages/core/src/scope.ts | 34 +- .../core/src/utils/applyScopeDataToEvent.ts | 98 ++++++ packages/core/src/utils/prepareEvent.ts | 44 ++- packages/core/test/lib/base.test.ts | 12 +- packages/core/test/lib/prepareEvent.test.ts | 188 ++++++++++- packages/core/test/lib/scope.test.ts | 190 +++++++++++ .../lib/utils/applyScopeDataToEvent.test.ts | 201 ++++++++++++ packages/hub/test/scope.test.ts | 36 +-- .../src/otel/asyncContextStrategy.ts | 2 +- packages/node-experimental/src/sdk/scope.ts | 166 ++-------- packages/node-experimental/src/sdk/types.ts | 1 - .../test/integration/scope.test.ts | 3 +- .../node-experimental/test/sdk/scope.test.ts | 295 ++++-------------- 14 files changed, 847 insertions(+), 427 deletions(-) create mode 100644 packages/core/test/lib/scope.test.ts create mode 100644 packages/core/test/lib/utils/applyScopeDataToEvent.test.ts diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 08f18f38ade6..dbf18135ef67 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -42,7 +42,7 @@ export { } from './hub'; export { makeSession, closeSession, updateSession } from './session'; export { SessionFlusher } from './sessionflusher'; -export { Scope } from './scope'; +export { Scope, getGlobalScope, setGlobalScope } from './scope'; export { notifyEventProcessors, // eslint-disable-next-line deprecation/deprecation @@ -63,7 +63,7 @@ export { convertIntegrationFnToClass, } from './integration'; export { FunctionToString, InboundFilters, LinkedErrors } from './integrations'; -export { applyScopeDataToEvent } from './utils/applyScopeDataToEvent'; +export { applyScopeDataToEvent, mergeScopeData } from './utils/applyScopeDataToEvent'; export { prepareEvent } from './utils/prepareEvent'; export { createCheckInEnvelope } from './checkin'; export { hasTracingEnabled } from './utils/hasTracingEnabled'; diff --git a/packages/core/src/scope.ts b/packages/core/src/scope.ts index 599b5c0f8d57..3fc78081f188 100644 --- a/packages/core/src/scope.ts +++ b/packages/core/src/scope.ts @@ -23,7 +23,7 @@ import type { Transaction, User, } from '@sentry/types'; -import { arrayify, dateTimestampInSeconds, isPlainObject, uuid4 } from '@sentry/utils'; +import { dateTimestampInSeconds, isPlainObject, uuid4 } from '@sentry/utils'; import { getGlobalEventProcessors, notifyEventProcessors } from './eventProcessors'; import { updateSession } from './session'; @@ -34,6 +34,12 @@ import { applyScopeDataToEvent } from './utils/applyScopeDataToEvent'; */ const DEFAULT_MAX_BREADCRUMBS = 100; +/** + * The global scope is kept in this module. + * When accessing this via `getGlobalScope()` we'll make sure to set one if none is currently present. + */ +let globalScope: ScopeInterface | undefined; + /** * Holds additional event information. {@link Scope.applyToEvent} will be * called by the client before an event will be sent. @@ -455,9 +461,12 @@ export class Scope implements ScopeInterface { /** * @inheritDoc + * @deprecated Use `getScopeData()` instead. */ public getAttachments(): Attachment[] { - return this._attachments; + const data = this.getScopeData(); + + return data.attachments; } /** @@ -570,6 +579,27 @@ export class Scope implements ScopeInterface { } } +/** + * Get the global scope. + * This scope is applied to _all_ events. + */ +export function getGlobalScope(): ScopeInterface { + if (!globalScope) { + globalScope = new Scope(); + } + + return globalScope; +} + +/** + * This is mainly needed for tests. + * DO NOT USE this, as this is an internal API and subject to change. + * @hidden + */ +export function setGlobalScope(scope: ScopeInterface | undefined): void { + globalScope = scope; +} + function generatePropagationContext(): PropagationContext { return { traceId: uuid4(), diff --git a/packages/core/src/utils/applyScopeDataToEvent.ts b/packages/core/src/utils/applyScopeDataToEvent.ts index cc63e7c26cb6..1dab87aea866 100644 --- a/packages/core/src/utils/applyScopeDataToEvent.ts +++ b/packages/core/src/utils/applyScopeDataToEvent.ts @@ -22,6 +22,104 @@ export function applyScopeDataToEvent(event: Event, data: ScopeData): void { applySdkMetadataToEvent(event, sdkProcessingMetadata, propagationContext); } +/** Merge data of two scopes together. */ +export function mergeScopeData(data: ScopeData, mergeData: ScopeData): void { + const { + extra, + tags, + user, + contexts, + level, + sdkProcessingMetadata, + breadcrumbs, + fingerprint, + eventProcessors, + attachments, + propagationContext, + transactionName, + span, + } = mergeData; + + mergePropOverwrite(data, 'extra', extra); + mergePropOverwrite(data, 'tags', tags); + mergePropOverwrite(data, 'user', user); + mergePropOverwrite(data, 'contexts', contexts); + mergePropOverwrite(data, 'sdkProcessingMetadata', sdkProcessingMetadata); + + if (level) { + data.level = level; + } + + if (transactionName) { + data.transactionName = transactionName; + } + + if (span) { + data.span = span; + } + + if (breadcrumbs.length) { + data.breadcrumbs = [...data.breadcrumbs, ...breadcrumbs]; + } + + if (fingerprint.length) { + data.fingerprint = [...data.fingerprint, ...fingerprint]; + } + + if (eventProcessors.length) { + data.eventProcessors = [...data.eventProcessors, ...eventProcessors]; + } + + if (attachments.length) { + data.attachments = [...data.attachments, ...attachments]; + } + + data.propagationContext = { ...data.propagationContext, ...propagationContext }; +} + +/** + * Merge properties, overwriting existing keys. + * Exported only for tests. + */ +export function mergePropOverwrite< + Prop extends 'extra' | 'tags' | 'user' | 'contexts' | 'sdkProcessingMetadata', + Data extends ScopeData | Event, +>(data: Data, prop: Prop, mergeVal: Data[Prop]): void { + if (mergeVal && Object.keys(mergeVal).length) { + data[prop] = { ...data[prop], ...mergeVal }; + } +} + +/** + * Merge properties, keeping existing keys. + * Exported only for tests. + */ +export function mergePropKeep< + Prop extends 'extra' | 'tags' | 'user' | 'contexts' | 'sdkProcessingMetadata', + Data extends ScopeData | Event, +>(data: Data, prop: Prop, mergeVal: Data[Prop]): void { + if (mergeVal && Object.keys(mergeVal).length) { + data[prop] = { ...mergeVal, ...data[prop] }; + } +} + +/** Exported only for tests */ +export function mergeArray( + event: Event, + prop: Prop, + mergeVal: ScopeData[Prop], +): void { + const prevVal = event[prop]; + // If we are not merging any new values, + // we only need to proceed if there was an empty array before (as we want to replace it with undefined) + if (!mergeVal.length && (!prevVal || prevVal.length)) { + return; + } + + const merged = [...(prevVal || []), ...mergeVal] as ScopeData[Prop]; + event[prop] = merged.length ? merged : undefined; +} + function applyDataToEvent(event: Event, data: ScopeData): void { const { extra, tags, user, contexts, level, transactionName } = data; diff --git a/packages/core/src/utils/prepareEvent.ts b/packages/core/src/utils/prepareEvent.ts index 76307b4f45e6..a4584c34066a 100644 --- a/packages/core/src/utils/prepareEvent.ts +++ b/packages/core/src/utils/prepareEvent.ts @@ -13,8 +13,8 @@ import { GLOBAL_OBJ, addExceptionMechanism, dateTimestampInSeconds, normalize, t import { DEFAULT_ENVIRONMENT } from '../constants'; import { getGlobalEventProcessors, notifyEventProcessors } from '../eventProcessors'; -import { Scope } from '../scope'; -import { applyScopeDataToEvent } from './applyScopeDataToEvent'; +import { Scope, getGlobalScope } from '../scope'; +import { applyScopeDataToEvent, mergeScopeData } from './applyScopeDataToEvent'; /** * This type makes sure that we get either a CaptureContext, OR an EventHint. @@ -74,36 +74,32 @@ export function prepareEvent( } const clientEventProcessors = client && client.getEventProcessors ? client.getEventProcessors() : []; - // TODO (v8): Update this order to be: Global > Client > Scope - const eventProcessors = [ - ...clientEventProcessors, - // eslint-disable-next-line deprecation/deprecation - ...getGlobalEventProcessors(), - ]; // This should be the last thing called, since we want that // {@link Hub.addEventProcessor} gets the finished prepared event. - // - // We need to check for the existence of `finalScope.getAttachments` - // because `getAttachments` can be undefined if users are using an older version - // of `@sentry/core` that does not have the `getAttachments` method. - // See: https://github.com/getsentry/sentry-javascript/issues/5229 + // Merge scope data together + const data = getGlobalScope().getScopeData(); + if (finalScope) { - // Collect attachments from the hint and scope - if (finalScope.getAttachments) { - const attachments = [...(hint.attachments || []), ...finalScope.getAttachments()]; + const finalScopeData = finalScope.getScopeData(); + mergeScopeData(data, finalScopeData); + } - if (attachments.length) { - hint.attachments = attachments; - } - } + const attachments = [...(hint.attachments || []), ...data.attachments]; + if (attachments.length) { + hint.attachments = attachments; + } - const scopeData = finalScope.getScopeData(); - applyScopeDataToEvent(prepared, scopeData); + applyScopeDataToEvent(prepared, data); + // TODO (v8): Update this order to be: Global > Client > Scope + const eventProcessors = [ + ...clientEventProcessors, + // eslint-disable-next-line deprecation/deprecation + ...getGlobalEventProcessors(), // Run scope event processors _after_ all other processors - eventProcessors.push(...scopeData.eventProcessors); - } + ...data.eventProcessors, + ]; const result = notifyEventProcessors(eventProcessors, prepared, hint); diff --git a/packages/core/test/lib/base.test.ts b/packages/core/test/lib/base.test.ts index baeb8bfd0044..0b39216e19a5 100644 --- a/packages/core/test/lib/base.test.ts +++ b/packages/core/test/lib/base.test.ts @@ -1,7 +1,7 @@ import type { Client, Envelope, Event, Span, Transaction } from '@sentry/types'; import { SentryError, SyncPromise, dsnToString, logger } from '@sentry/utils'; -import { Hub, Scope, makeSession } from '../../src'; +import { Hub, Scope, makeSession, setGlobalScope } from '../../src'; import * as integrationModule from '../../src/integration'; import { TestClient, getDefaultTestClientOptions } from '../mocks/client'; import { AdHocIntegration, TestIntegration } from '../mocks/integration'; @@ -54,6 +54,7 @@ describe('BaseClient', () => { beforeEach(() => { TestClient.sendEventCalled = undefined; TestClient.instance = undefined; + setGlobalScope(undefined); }); afterEach(() => { @@ -756,7 +757,8 @@ describe('BaseClient', () => { expect(TestClient.instance!.event!).toEqual( expect.objectContaining({ breadcrumbs: [normalizedBreadcrumb, normalizedBreadcrumb, normalizedBreadcrumb], - contexts: normalizedObject, + // also has trace context from global scope + contexts: { ...normalizedObject, trace: expect.anything() }, environment: 'production', event_id: '42', extra: normalizedObject, @@ -805,7 +807,8 @@ describe('BaseClient', () => { expect(TestClient.instance!.event!).toEqual( expect.objectContaining({ breadcrumbs: [normalizedBreadcrumb, normalizedBreadcrumb, normalizedBreadcrumb], - contexts: normalizedObject, + // also has trace context from global scope + contexts: { ...normalizedObject, trace: expect.anything() }, environment: 'production', event_id: '42', extra: normalizedObject, @@ -859,7 +862,8 @@ describe('BaseClient', () => { expect(TestClient.instance!.event!).toEqual( expect.objectContaining({ breadcrumbs: [normalizedBreadcrumb, normalizedBreadcrumb, normalizedBreadcrumb], - contexts: normalizedObject, + // also has trace context from global scope + contexts: { ...normalizedObject, trace: expect.anything() }, environment: 'production', event_id: '42', extra: normalizedObject, diff --git a/packages/core/test/lib/prepareEvent.test.ts b/packages/core/test/lib/prepareEvent.test.ts index b98305dea604..09c48afffc35 100644 --- a/packages/core/test/lib/prepareEvent.test.ts +++ b/packages/core/test/lib/prepareEvent.test.ts @@ -1,8 +1,23 @@ -import type { Event, EventHint, ScopeContext } from '@sentry/types'; +import type { + Attachment, + Breadcrumb, + Client, + ClientOptions, + Event, + EventHint, + EventProcessor, + ScopeContext, +} from '@sentry/types'; import { GLOBAL_OBJ, createStackParser } from '@sentry/utils'; +import { setGlobalScope } from '../../src'; -import { Scope } from '../../src/scope'; -import { applyDebugIds, applyDebugMeta, parseEventHintOrCaptureContext } from '../../src/utils/prepareEvent'; +import { Scope, getGlobalScope } from '../../src/scope'; +import { + applyDebugIds, + applyDebugMeta, + parseEventHintOrCaptureContext, + prepareEvent, +} from '../../src/utils/prepareEvent'; describe('applyDebugIds', () => { afterEach(() => { @@ -173,3 +188,170 @@ describe('parseEventHintOrCaptureContext', () => { }); }); }); + +describe('prepareEvent', () => { + beforeEach(() => { + setGlobalScope(undefined); + }); + + it('works without any scope data', async () => { + const eventProcessor = jest.fn((a: unknown) => a) as EventProcessor; + + const scope = new Scope(); + + const event = { message: 'foo' }; + + const options = {} as ClientOptions; + const client = { + getEventProcessors() { + return [eventProcessor]; + }, + } as Client; + const processedEvent = await prepareEvent( + options, + event, + { + integrations: [], + }, + scope, + client, + ); + + expect(eventProcessor).toHaveBeenCalledWith(processedEvent, { + integrations: [], + // no attachments are added to hint + }); + + expect(processedEvent).toEqual({ + timestamp: expect.any(Number), + event_id: expect.any(String), + environment: 'production', + message: 'foo', + sdkProcessingMetadata: { + propagationContext: { + spanId: expect.any(String), + traceId: expect.any(String), + }, + }, + }); + }); + + it('merges scope data', async () => { + const breadcrumb1 = { message: '1', timestamp: 111 } as Breadcrumb; + const breadcrumb2 = { message: '2', timestamp: 222 } as Breadcrumb; + const breadcrumb3 = { message: '3', timestamp: 123 } as Breadcrumb; + + const eventProcessor1 = jest.fn((a: unknown) => a) as EventProcessor; + const eventProcessor2 = jest.fn((b: unknown) => b) as EventProcessor; + + const attachment1 = { filename: '1' } as Attachment; + const attachment2 = { filename: '2' } as Attachment; + + const scope = new Scope(); + scope.update({ + user: { id: '1', email: 'test@example.com' }, + tags: { tag1: 'aa', tag2: 'aa' }, + extra: { extra1: 'aa', extra2: 'aa' }, + contexts: { os: { name: 'os1' }, culture: { display_name: 'name1' } }, + propagationContext: { spanId: '1', traceId: '1' }, + fingerprint: ['aa'], + }); + scope.addBreadcrumb(breadcrumb1); + scope.addEventProcessor(eventProcessor1); + scope.addAttachment(attachment1); + + const globalScope = getGlobalScope(); + + globalScope.addBreadcrumb(breadcrumb2); + globalScope.addEventProcessor(eventProcessor2); + globalScope.setSDKProcessingMetadata({ aa: 'aa' }); + globalScope.addAttachment(attachment2); + + const event = { message: 'foo', breadcrumbs: [breadcrumb3], fingerprint: ['dd'] }; + + const options = {} as ClientOptions; + const processedEvent = await prepareEvent( + options, + event, + { + integrations: [], + }, + scope, + ); + + expect(eventProcessor1).toHaveBeenCalledTimes(1); + expect(eventProcessor2).toHaveBeenCalledTimes(1); + + // Test that attachments are correctly merged + expect(eventProcessor1).toHaveBeenCalledWith(processedEvent, { + integrations: [], + attachments: [attachment2, attachment1], + }); + + expect(processedEvent).toEqual({ + timestamp: expect.any(Number), + event_id: expect.any(String), + environment: 'production', + message: 'foo', + user: { id: '1', email: 'test@example.com' }, + tags: { tag1: 'aa', tag2: 'aa' }, + extra: { extra1: 'aa', extra2: 'aa' }, + contexts: { os: { name: 'os1' }, culture: { display_name: 'name1' } }, + fingerprint: ['dd', 'aa'], + breadcrumbs: [breadcrumb3, breadcrumb2, breadcrumb1], + sdkProcessingMetadata: { + aa: 'aa', + propagationContext: { + spanId: '1', + traceId: '1', + }, + }, + }); + }); + + it('works without a scope', async () => { + const breadcrumb1 = { message: '1', timestamp: 111 } as Breadcrumb; + const breadcrumb2 = { message: '2', timestamp: 222 } as Breadcrumb; + + const eventProcessor1 = jest.fn((a: unknown) => a) as EventProcessor; + + const attachment1 = { filename: '1' } as Attachment; + const attachment2 = { filename: '2' } as Attachment; + + const globalScope = getGlobalScope(); + + globalScope.addBreadcrumb(breadcrumb1); + globalScope.addEventProcessor(eventProcessor1); + globalScope.setSDKProcessingMetadata({ aa: 'aa' }); + globalScope.addAttachment(attachment1); + + const event = { message: 'foo', breadcrumbs: [breadcrumb2], fingerprint: ['dd'] }; + + const options = {} as ClientOptions; + const processedEvent = await prepareEvent(options, event, { + integrations: [], + attachments: [attachment2], + }); + + expect(eventProcessor1).toHaveBeenCalledTimes(1); + + // Test that attachments are correctly merged + expect(eventProcessor1).toHaveBeenCalledWith(processedEvent, { + integrations: [], + attachments: [attachment2, attachment1], + }); + + expect(processedEvent).toEqual({ + timestamp: expect.any(Number), + event_id: expect.any(String), + environment: 'production', + message: 'foo', + fingerprint: ['dd'], + breadcrumbs: [breadcrumb2, breadcrumb1], + sdkProcessingMetadata: { + aa: 'aa', + propagationContext: globalScope.getPropagationContext(), + }, + }); + }); +}); diff --git a/packages/core/test/lib/scope.test.ts b/packages/core/test/lib/scope.test.ts new file mode 100644 index 000000000000..e04f5638c5d0 --- /dev/null +++ b/packages/core/test/lib/scope.test.ts @@ -0,0 +1,190 @@ +import type { Attachment, Breadcrumb } from '@sentry/types'; +import { applyScopeDataToEvent } from '../../src'; +import { Scope, getGlobalScope, setGlobalScope } from '../../src/scope'; + +describe('Unit | Scope', () => { + beforeEach(() => { + setGlobalScope(undefined); + }); + + it('allows to create & update a scope', () => { + const scope = new Scope(); + + expect(scope.getScopeData()).toEqual({ + breadcrumbs: [], + attachments: [], + contexts: {}, + tags: {}, + extra: {}, + user: {}, + level: undefined, + fingerprint: [], + eventProcessors: [], + propagationContext: { + traceId: expect.any(String), + spanId: expect.any(String), + }, + sdkProcessingMetadata: {}, + }); + + scope.update({ + tags: { foo: 'bar' }, + extra: { foo2: 'bar2' }, + }); + + expect(scope.getScopeData()).toEqual({ + breadcrumbs: [], + attachments: [], + contexts: {}, + tags: { + foo: 'bar', + }, + extra: { + foo2: 'bar2', + }, + user: {}, + level: undefined, + fingerprint: [], + eventProcessors: [], + propagationContext: { + traceId: expect.any(String), + spanId: expect.any(String), + }, + sdkProcessingMetadata: {}, + }); + }); + + it('allows to clone a scope', () => { + const scope = new Scope(); + + scope.update({ + tags: { foo: 'bar' }, + extra: { foo2: 'bar2' }, + }); + + const newScope = scope.clone(); + expect(newScope).toBeInstanceOf(Scope); + expect(newScope).not.toBe(scope); + + expect(newScope.getScopeData()).toEqual({ + breadcrumbs: [], + attachments: [], + contexts: {}, + tags: { + foo: 'bar', + }, + extra: { + foo2: 'bar2', + }, + user: {}, + level: undefined, + fingerprint: [], + eventProcessors: [], + propagationContext: { + traceId: expect.any(String), + spanId: expect.any(String), + }, + sdkProcessingMetadata: {}, + }); + }); + + describe('global scope', () => { + beforeEach(() => { + setGlobalScope(undefined); + }); + + it('works', () => { + const globalScope = getGlobalScope(); + expect(globalScope).toBeDefined(); + expect(globalScope).toBeInstanceOf(Scope); + + // Repeatedly returns the same instance + expect(getGlobalScope()).toBe(globalScope); + + globalScope.setTag('tag1', 'val1'); + globalScope.setTag('tag2', 'val2'); + + expect(globalScope.getScopeData().tags).toEqual({ tag1: 'val1', tag2: 'val2' }); + }); + }); + + describe('applyScopeDataToEvent', () => { + it('works without any data', async () => { + const scope = new Scope(); + + const event = { message: 'foo' }; + applyScopeDataToEvent(event, scope.getScopeData()); + + expect(event).toEqual({ + message: 'foo', + sdkProcessingMetadata: { + propagationContext: { + spanId: expect.any(String), + traceId: expect.any(String), + }, + }, + }); + }); + + it('works with data', async () => { + const breadcrumb1 = { message: '1', timestamp: 111 } as Breadcrumb; + const breadcrumb2 = { message: '1', timestamp: 111 } as Breadcrumb; + + const scope = new Scope(); + scope.update({ + user: { id: '1', email: 'test@example.com' }, + tags: { tag1: 'aa', tag2: 'aa' }, + extra: { extra1: 'aa', extra2: 'aa' }, + contexts: { os: { name: 'os1' }, culture: { display_name: 'name1' } }, + propagationContext: { spanId: '1', traceId: '1' }, + fingerprint: ['aa'], + }); + scope.addBreadcrumb(breadcrumb1); + scope.setSDKProcessingMetadata({ aa: 'aa' }); + + const event = { message: 'foo', breadcrumbs: [breadcrumb2], fingerprint: ['dd'] }; + + applyScopeDataToEvent(event, scope.getScopeData()); + + expect(event).toEqual({ + message: 'foo', + user: { id: '1', email: 'test@example.com' }, + tags: { tag1: 'aa', tag2: 'aa' }, + extra: { extra1: 'aa', extra2: 'aa' }, + contexts: { os: { name: 'os1' }, culture: { display_name: 'name1' } }, + fingerprint: ['dd', 'aa'], + breadcrumbs: [breadcrumb2, breadcrumb1], + sdkProcessingMetadata: { + aa: 'aa', + propagationContext: { + spanId: '1', + traceId: '1', + }, + }, + }); + }); + }); + + describe('getAttachments', () => { + /* eslint-disable deprecation/deprecation */ + it('works without any data', async () => { + const scope = new Scope(); + + const actual = scope.getAttachments(); + expect(actual).toEqual([]); + }); + + it('works with attachments', async () => { + const attachment1 = { filename: '1' } as Attachment; + const attachment2 = { filename: '2' } as Attachment; + + const scope = new Scope(); + scope.addAttachment(attachment1); + scope.addAttachment(attachment2); + + const actual = scope.getAttachments(); + expect(actual).toEqual([attachment1, attachment2]); + }); + /* eslint-enable deprecation/deprecation */ + }); +}); diff --git a/packages/core/test/lib/utils/applyScopeDataToEvent.test.ts b/packages/core/test/lib/utils/applyScopeDataToEvent.test.ts new file mode 100644 index 000000000000..f4fa38ee8b8b --- /dev/null +++ b/packages/core/test/lib/utils/applyScopeDataToEvent.test.ts @@ -0,0 +1,201 @@ +import type { Attachment, Breadcrumb, EventProcessor, ScopeData } from '@sentry/types'; +import { + mergeArray, + mergePropKeep, + mergePropOverwrite, + mergeScopeData, +} from '../../../src/utils/applyScopeDataToEvent'; + +describe('mergeArray', () => { + it.each([ + [[], [], undefined], + [undefined, [], undefined], + [['a'], [], ['a']], + [['a'], ['b', 'c'], ['a', 'b', 'c']], + [[], ['b', 'c'], ['b', 'c']], + [undefined, ['b', 'c'], ['b', 'c']], + ])('works with %s and %s', (a, b, expected) => { + const data = { fingerprint: a }; + mergeArray(data, 'fingerprint', b); + expect(data.fingerprint).toEqual(expected); + }); + + it('does not mutate the original array if no changes are made', () => { + const fingerprint = ['a']; + const data = { fingerprint }; + mergeArray(data, 'fingerprint', []); + expect(data.fingerprint).toBe(fingerprint); + }); +}); + +describe('mergePropKeep', () => { + it.each([ + [{}, {}, {}], + [{ a: 'aa' }, {}, { a: 'aa' }], + [{ a: 'aa' }, { b: 'bb' }, { a: 'aa', b: 'bb' }], + // Does not overwrite existing keys + [{ a: 'aa' }, { b: 'bb', a: 'cc' }, { a: 'aa', b: 'bb' }], + ])('works with %s and %s', (a, b, expected) => { + const data = { tags: a } as unknown as ScopeData; + mergePropKeep(data, 'tags', b); + expect(data.tags).toEqual(expected); + }); + + it('does not deep merge', () => { + const data = { + contexts: { + app: { app_version: 'v1' }, + culture: { display_name: 'name1' }, + }, + } as unknown as ScopeData; + mergePropKeep(data, 'contexts', { + os: { name: 'os1' }, + app: { app_name: 'name1' }, + }); + expect(data.contexts).toEqual({ + os: { name: 'os1' }, + culture: { display_name: 'name1' }, + app: { app_version: 'v1' }, + }); + }); + + it('does not mutate the original object if no changes are made', () => { + const tags = { a: 'aa' }; + const data = { tags } as unknown as ScopeData; + mergePropKeep(data, 'tags', {}); + expect(data.tags).toBe(tags); + }); +}); + +describe('mergePropOverwrite', () => { + it.each([ + [{}, {}, {}], + [{ a: 'aa' }, {}, { a: 'aa' }], + [{ a: 'aa' }, { b: 'bb' }, { a: 'aa', b: 'bb' }], + // overwrites existing keys + [{ a: 'aa' }, { b: 'bb', a: 'cc' }, { a: 'cc', b: 'bb' }], + ])('works with %s and %s', (a, b, expected) => { + const data = { tags: a } as unknown as ScopeData; + mergePropOverwrite(data, 'tags', b); + expect(data.tags).toEqual(expected); + }); + + it('does not deep merge', () => { + const data = { + contexts: { + app: { app_version: 'v1' }, + culture: { display_name: 'name1' }, + }, + } as unknown as ScopeData; + mergePropOverwrite(data, 'contexts', { + os: { name: 'os1' }, + app: { app_name: 'name1' }, + }); + expect(data.contexts).toEqual({ + os: { name: 'os1' }, + culture: { display_name: 'name1' }, + app: { app_name: 'name1' }, + }); + }); + + it('does not mutate the original object if no changes are made', () => { + const tags = { a: 'aa' }; + const data = { tags } as unknown as ScopeData; + mergePropOverwrite(data, 'tags', {}); + expect(data.tags).toBe(tags); + }); +}); + +describe('mergeScopeData', () => { + it('works with empty data', () => { + const data1: ScopeData = { + eventProcessors: [], + breadcrumbs: [], + user: {}, + tags: {}, + extra: {}, + contexts: {}, + attachments: [], + propagationContext: { spanId: '1', traceId: '1' }, + sdkProcessingMetadata: {}, + fingerprint: [], + }; + const data2: ScopeData = { + eventProcessors: [], + breadcrumbs: [], + user: {}, + tags: {}, + extra: {}, + contexts: {}, + attachments: [], + propagationContext: { spanId: '1', traceId: '1' }, + sdkProcessingMetadata: {}, + fingerprint: [], + }; + mergeScopeData(data1, data2); + expect(data1).toEqual({ + eventProcessors: [], + breadcrumbs: [], + user: {}, + tags: {}, + extra: {}, + contexts: {}, + attachments: [], + propagationContext: { spanId: '1', traceId: '1' }, + sdkProcessingMetadata: {}, + fingerprint: [], + }); + }); + + it('merges data correctly', () => { + const attachment1 = { filename: '1' } as Attachment; + const attachment2 = { filename: '2' } as Attachment; + const attachment3 = { filename: '3' } as Attachment; + + const breadcrumb1 = { message: '1' } as Breadcrumb; + const breadcrumb2 = { message: '2' } as Breadcrumb; + const breadcrumb3 = { message: '3' } as Breadcrumb; + + const eventProcessor1 = ((a: unknown) => null) as EventProcessor; + const eventProcessor2 = ((b: unknown) => null) as EventProcessor; + const eventProcessor3 = ((c: unknown) => null) as EventProcessor; + + const data1: ScopeData = { + eventProcessors: [eventProcessor1], + breadcrumbs: [breadcrumb1], + user: { id: '1', email: 'test@example.com' }, + tags: { tag1: 'aa', tag2: 'aa' }, + extra: { extra1: 'aa', extra2: 'aa' }, + contexts: { os: { name: 'os1' }, culture: { display_name: 'name1' } }, + attachments: [attachment1], + propagationContext: { spanId: '1', traceId: '1' }, + sdkProcessingMetadata: { aa: 'aa', bb: 'aa' }, + fingerprint: ['aa', 'bb'], + }; + const data2: ScopeData = { + eventProcessors: [eventProcessor2, eventProcessor3], + breadcrumbs: [breadcrumb2, breadcrumb3], + user: { id: '2', name: 'foo' }, + tags: { tag2: 'bb', tag3: 'bb' }, + extra: { extra2: 'bb', extra3: 'bb' }, + contexts: { os: { name: 'os2' } }, + attachments: [attachment2, attachment3], + propagationContext: { spanId: '2', traceId: '2' }, + sdkProcessingMetadata: { bb: 'bb', cc: 'bb' }, + fingerprint: ['cc'], + }; + mergeScopeData(data1, data2); + expect(data1).toEqual({ + eventProcessors: [eventProcessor1, eventProcessor2, eventProcessor3], + breadcrumbs: [breadcrumb1, breadcrumb2, breadcrumb3], + user: { id: '2', name: 'foo', email: 'test@example.com' }, + tags: { tag1: 'aa', tag2: 'bb', tag3: 'bb' }, + extra: { extra1: 'aa', extra2: 'bb', extra3: 'bb' }, + contexts: { os: { name: 'os2' }, culture: { display_name: 'name1' } }, + attachments: [attachment1, attachment2, attachment3], + propagationContext: { spanId: '2', traceId: '2' }, + sdkProcessingMetadata: { aa: 'aa', bb: 'bb', cc: 'bb' }, + fingerprint: ['aa', 'bb', 'cc'], + }); + }); +}); diff --git a/packages/hub/test/scope.test.ts b/packages/hub/test/scope.test.ts index a039a839dcc6..a2be18b0526d 100644 --- a/packages/hub/test/scope.test.ts +++ b/packages/hub/test/scope.test.ts @@ -237,8 +237,6 @@ describe('Scope', () => { describe('applyToEvent', () => { test('basic usage', async () => { - expect.assertions(9); - const scope = new Scope(); scope.setExtra('a', 2); scope.setTag('a', 'b'); @@ -251,20 +249,20 @@ describe('Scope', () => { scope.setSDKProcessingMetadata({ dogs: 'are great!' }); const event: Event = {}; - return scope.applyToEvent(event).then(processedEvent => { - expect(processedEvent!.extra).toEqual({ a: 2 }); - expect(processedEvent!.tags).toEqual({ a: 'b' }); - expect(processedEvent!.user).toEqual({ id: '1' }); - expect(processedEvent!.fingerprint).toEqual(['abcd']); - expect(processedEvent!.level).toEqual('warning'); - expect(processedEvent!.transaction).toEqual('/abc'); - expect(processedEvent!.breadcrumbs![0]).toHaveProperty('message', 'test'); - expect(processedEvent!.contexts).toEqual({ os: { id: '1' } }); - expect(processedEvent!.sdkProcessingMetadata).toEqual({ - dogs: 'are great!', - // @ts-expect-error accessing private property for test - propagationContext: scope._propagationContext, - }); + const processedEvent = await scope.applyToEvent(event); + + expect(processedEvent!.extra).toEqual({ a: 2 }); + expect(processedEvent!.tags).toEqual({ a: 'b' }); + expect(processedEvent!.user).toEqual({ id: '1' }); + expect(processedEvent!.fingerprint).toEqual(['abcd']); + expect(processedEvent!.level).toEqual('warning'); + expect(processedEvent!.transaction).toEqual('/abc'); + expect(processedEvent!.breadcrumbs![0]).toHaveProperty('message', 'test'); + expect(processedEvent!.contexts).toEqual({ os: { id: '1' } }); + expect(processedEvent!.sdkProcessingMetadata).toEqual({ + dogs: 'are great!', + // @ts-expect-error accessing private property for test + propagationContext: scope._propagationContext, }); }); @@ -360,7 +358,6 @@ describe('Scope', () => { }); test('adds trace context', async () => { - expect.assertions(1); const scope = new Scope(); const span = { fake: 'span', @@ -368,9 +365,8 @@ describe('Scope', () => { } as any; scope.setSpan(span); const event: Event = {}; - return scope.applyToEvent(event).then(processedEvent => { - expect((processedEvent!.contexts!.trace as any).a).toEqual('b'); - }); + const processedEvent = await scope.applyToEvent(event); + expect((processedEvent!.contexts!.trace as any).a).toEqual('b'); }); test('existing trace context in event should take precedence', async () => { diff --git a/packages/node-experimental/src/otel/asyncContextStrategy.ts b/packages/node-experimental/src/otel/asyncContextStrategy.ts index e0d976c71ff1..10b4ea6d7b91 100644 --- a/packages/node-experimental/src/otel/asyncContextStrategy.ts +++ b/packages/node-experimental/src/otel/asyncContextStrategy.ts @@ -1,6 +1,6 @@ import * as api from '@opentelemetry/api'; -import { setAsyncContextStrategy } from './../sdk/globals'; +import { setAsyncContextStrategy } from '../sdk/globals'; import { getCurrentHub } from './../sdk/hub'; import type { CurrentScopes } from './../sdk/types'; import { getScopesFromContext } from './../utils/contextData'; diff --git a/packages/node-experimental/src/sdk/scope.ts b/packages/node-experimental/src/sdk/scope.ts index 5bf220708a6a..f169f95250e2 100644 --- a/packages/node-experimental/src/sdk/scope.ts +++ b/packages/node-experimental/src/sdk/scope.ts @@ -1,5 +1,6 @@ +import { getGlobalScope as _getGlobalScope, mergeScopeData, setGlobalScope } from '@sentry/core'; import { OpenTelemetryScope } from '@sentry/opentelemetry'; -import type { Attachment, Breadcrumb, Client, Event, EventHint, Severity, SeverityLevel } from '@sentry/types'; +import type { Breadcrumb, Client, Event, EventHint, Severity, SeverityLevel } from '@sentry/types'; import { uuid4 } from '@sentry/utils'; import { getGlobalCarrier } from './globals'; @@ -18,15 +19,22 @@ export function setCurrentScope(scope: Scope): void { getScopes().scope = scope; } -/** Get the global scope. */ +/** + * Get the global scope. + * We overwrite this from the core implementation to make sure we get the correct Scope class. + */ export function getGlobalScope(): Scope { - const carrier = getGlobalCarrier(); + const globalScope = _getGlobalScope(); - if (!carrier.globalScope) { - carrier.globalScope = new Scope(); + // If we have a default Scope here by chance, make sure to "upgrade" it to our custom Scope + if (!(globalScope instanceof Scope)) { + const newScope = new Scope(); + newScope.update(globalScope); + setGlobalScope(newScope); + return newScope; } - return carrier.globalScope as Scope; + return globalScope; } /** Get the currently active isolation scope. */ @@ -86,6 +94,7 @@ export class Scope extends OpenTelemetryScope implements ScopeInterface { newScope._attachments = [...this['_attachments']]; newScope._sdkProcessingMetadata = { ...this['_sdkProcessingMetadata'] }; newScope._propagationContext = { ...this['_propagationContext'] }; + newScope._client = this._client; return newScope; } @@ -104,13 +113,6 @@ export class Scope extends OpenTelemetryScope implements ScopeInterface { return this._client; } - /** @inheritdoc */ - public getAttachments(): Attachment[] { - const data = this.getScopeData(); - - return data.attachments; - } - /** Capture an exception for this scope. */ public captureException(exception: unknown, hint?: EventHint): string { const eventId = hint && hint.event_id ? hint.event_id : uuid4(); @@ -183,46 +185,28 @@ export class Scope extends OpenTelemetryScope implements ScopeInterface { return this._addBreadcrumb(breadcrumb, maxBreadcrumbs); } - /** Get all relevant data for this scope. */ - public getPerScopeData(): ScopeData { - const { - _breadcrumbs, - _attachments, - _contexts, - _tags, - _extra, - _user, - _level, - _fingerprint, - _eventProcessors, - _propagationContext, - _sdkProcessingMetadata, - } = this; - - return { - breadcrumbs: _breadcrumbs, - attachments: _attachments, - contexts: _contexts, - tags: _tags, - extra: _extra, - user: _user, - level: _level, - fingerprint: _fingerprint || [], - eventProcessors: _eventProcessors, - propagationContext: _propagationContext, - sdkProcessingMetadata: _sdkProcessingMetadata, - }; + /** Get scope data for this scope only. */ + public getOwnScopeData(): ScopeData { + return super.getScopeData(); } /** @inheritdoc */ public getScopeData(): ScopeData { - const data = getGlobalScope().getPerScopeData(); - const isolationScopeData = this._getIsolationScope().getPerScopeData(); - const scopeData = this.getPerScopeData(); + const globalScope = getGlobalScope(); + const isolationScope = this._getIsolationScope(); + + // Special case: If this is the global/isolation scope, no need to merge other data in here + if (this === globalScope || this === isolationScope) { + return this.getOwnScopeData(); + } + + // Global scope is applied anyhow in prepareEvent, + // but we need to merge the isolation scope in here + const data = isolationScope.getOwnScopeData(); + const scopeData = this.getOwnScopeData(); // Merge data together, in order - mergeData(data, isolationScopeData); - mergeData(data, scopeData); + mergeScopeData(data, scopeData); return data; } @@ -233,94 +217,6 @@ export class Scope extends OpenTelemetryScope implements ScopeInterface { } } -/** Exported only for tests */ -export function mergeData(data: ScopeData, mergeData: ScopeData): void { - const { - extra, - tags, - user, - contexts, - level, - sdkProcessingMetadata, - breadcrumbs, - fingerprint, - eventProcessors, - attachments, - propagationContext, - } = mergeData; - - mergePropOverwrite(data, 'extra', extra); - mergePropOverwrite(data, 'tags', tags); - mergePropOverwrite(data, 'user', user); - mergePropOverwrite(data, 'contexts', contexts); - mergePropOverwrite(data, 'sdkProcessingMetadata', sdkProcessingMetadata); - - if (level) { - data.level = level; - } - - if (breadcrumbs.length) { - data.breadcrumbs = [...data.breadcrumbs, ...breadcrumbs]; - } - - if (fingerprint.length) { - data.fingerprint = [...data.fingerprint, ...fingerprint]; - } - - if (eventProcessors.length) { - data.eventProcessors = [...data.eventProcessors, ...eventProcessors]; - } - - if (attachments.length) { - data.attachments = [...data.attachments, ...attachments]; - } - - data.propagationContext = { ...data.propagationContext, ...propagationContext }; -} - -/** - * Merge properties, overwriting existing keys. - * Exported only for tests. - */ -export function mergePropOverwrite< - Prop extends 'extra' | 'tags' | 'user' | 'contexts' | 'sdkProcessingMetadata', - Data extends ScopeData | Event, ->(data: Data, prop: Prop, mergeVal: Data[Prop]): void { - if (mergeVal && Object.keys(mergeVal).length) { - data[prop] = { ...data[prop], ...mergeVal }; - } -} - -/** - * Merge properties, keeping existing keys. - * Exported only for tests. - */ -export function mergePropKeep< - Prop extends 'extra' | 'tags' | 'user' | 'contexts' | 'sdkProcessingMetadata', - Data extends ScopeData | Event, ->(data: Data, prop: Prop, mergeVal: Data[Prop]): void { - if (mergeVal && Object.keys(mergeVal).length) { - data[prop] = { ...mergeVal, ...data[prop] }; - } -} - -/** Exported only for tests */ -export function mergeArray( - event: Event, - prop: Prop, - mergeVal: ScopeData[Prop], -): void { - const prevVal = event[prop]; - // If we are not merging any new values, - // we only need to proceed if there was an empty array before (as we want to replace it with undefined) - if (!mergeVal.length && (!prevVal || prevVal.length)) { - return; - } - - const merged = [...(prevVal || []), ...mergeVal] as ScopeData[Prop]; - event[prop] = merged.length ? merged : undefined; -} - function getScopes(): CurrentScopes { const carrier = getGlobalCarrier(); diff --git a/packages/node-experimental/src/sdk/types.ts b/packages/node-experimental/src/sdk/types.ts index 773c404d65ce..140056e583ca 100644 --- a/packages/node-experimental/src/sdk/types.ts +++ b/packages/node-experimental/src/sdk/types.ts @@ -74,7 +74,6 @@ export interface AsyncContextStrategy { } export interface SentryCarrier { - globalScope?: Scope; scopes?: CurrentScopes; acs?: AsyncContextStrategy; diff --git a/packages/node-experimental/test/integration/scope.test.ts b/packages/node-experimental/test/integration/scope.test.ts index 57be6126bcae..3efb3d09506d 100644 --- a/packages/node-experimental/test/integration/scope.test.ts +++ b/packages/node-experimental/test/integration/scope.test.ts @@ -1,3 +1,4 @@ +import { setGlobalScope } from '@sentry/core'; import { getCurrentHub, getSpanScope } from '@sentry/opentelemetry'; import * as Sentry from '../../src/'; @@ -230,7 +231,7 @@ describe('Integration | Scope', () => { describe('global scope', () => { beforeEach(() => { - resetGlobals(); + setGlobalScope(undefined); }); it('works before calling init', () => { diff --git a/packages/node-experimental/test/sdk/scope.test.ts b/packages/node-experimental/test/sdk/scope.test.ts index 9fb5c02bea69..c38437121959 100644 --- a/packages/node-experimental/test/sdk/scope.test.ts +++ b/packages/node-experimental/test/sdk/scope.test.ts @@ -1,8 +1,7 @@ -import { applyScopeDataToEvent } from '@sentry/core'; -import type { Attachment, Breadcrumb, Client, EventProcessor } from '@sentry/types'; +import { prepareEvent } from '@sentry/core'; +import type { Attachment, Breadcrumb, Client, ClientOptions, EventProcessor } from '@sentry/types'; import { Scope, getIsolationScope } from '../../src'; -import { getGlobalScope, mergeArray, mergeData, mergePropKeep, mergePropOverwrite } from '../../src/sdk/scope'; -import type { ScopeData } from '../../src/sdk/types'; +import { getGlobalScope } from '../../src/sdk/scope'; import { mockSdkInit, resetGlobals } from '../helpers/mockSdkInit'; describe('Unit | Scope', () => { @@ -109,210 +108,41 @@ describe('Unit | Scope', () => { expect(scope['_getIsolationScope']()).toBe(customIsolationScope); }); - describe('mergeArray', () => { - it.each([ - [[], [], undefined], - [undefined, [], undefined], - [['a'], [], ['a']], - [['a'], ['b', 'c'], ['a', 'b', 'c']], - [[], ['b', 'c'], ['b', 'c']], - [undefined, ['b', 'c'], ['b', 'c']], - ])('works with %s and %s', (a, b, expected) => { - const data = { fingerprint: a }; - mergeArray(data, 'fingerprint', b); - expect(data.fingerprint).toEqual(expected); - }); - - it('does not mutate the original array if no changes are made', () => { - const fingerprint = ['a']; - const data = { fingerprint }; - mergeArray(data, 'fingerprint', []); - expect(data.fingerprint).toBe(fingerprint); - }); - }); - - describe('mergePropKeep', () => { - it.each([ - [{}, {}, {}], - [{ a: 'aa' }, {}, { a: 'aa' }], - [{ a: 'aa' }, { b: 'bb' }, { a: 'aa', b: 'bb' }], - // Does not overwrite existing keys - [{ a: 'aa' }, { b: 'bb', a: 'cc' }, { a: 'aa', b: 'bb' }], - ])('works with %s and %s', (a, b, expected) => { - const data = { tags: a } as unknown as ScopeData; - mergePropKeep(data, 'tags', b); - expect(data.tags).toEqual(expected); - }); + describe('prepareEvent', () => { + it('works without any scope data', async () => { + mockSdkInit(); - it('does not deep merge', () => { - const data = { - contexts: { - app: { app_version: 'v1' }, - culture: { display_name: 'name1' }, - }, - } as unknown as ScopeData; - mergePropKeep(data, 'contexts', { - os: { name: 'os1' }, - app: { app_name: 'name1' }, - }); - expect(data.contexts).toEqual({ - os: { name: 'os1' }, - culture: { display_name: 'name1' }, - app: { app_version: 'v1' }, - }); - }); + const eventProcessor = jest.fn((a: unknown) => a) as EventProcessor; - it('does not mutate the original object if no changes are made', () => { - const tags = { a: 'aa' }; - const data = { tags } as unknown as ScopeData; - mergePropKeep(data, 'tags', {}); - expect(data.tags).toBe(tags); - }); - }); + const scope = new Scope(); - describe('mergePropOverwrite', () => { - it.each([ - [{}, {}, {}], - [{ a: 'aa' }, {}, { a: 'aa' }], - [{ a: 'aa' }, { b: 'bb' }, { a: 'aa', b: 'bb' }], - // overwrites existing keys - [{ a: 'aa' }, { b: 'bb', a: 'cc' }, { a: 'cc', b: 'bb' }], - ])('works with %s and %s', (a, b, expected) => { - const data = { tags: a } as unknown as ScopeData; - mergePropOverwrite(data, 'tags', b); - expect(data.tags).toEqual(expected); - }); + const event = { message: 'foo' }; - it('does not deep merge', () => { - const data = { - contexts: { - app: { app_version: 'v1' }, - culture: { display_name: 'name1' }, + const options = {} as ClientOptions; + const client = { + getEventProcessors() { + return [eventProcessor]; }, - } as unknown as ScopeData; - mergePropOverwrite(data, 'contexts', { - os: { name: 'os1' }, - app: { app_name: 'name1' }, - }); - expect(data.contexts).toEqual({ - os: { name: 'os1' }, - culture: { display_name: 'name1' }, - app: { app_name: 'name1' }, - }); - }); - - it('does not mutate the original object if no changes are made', () => { - const tags = { a: 'aa' }; - const data = { tags } as unknown as ScopeData; - mergePropOverwrite(data, 'tags', {}); - expect(data.tags).toBe(tags); - }); - }); - - describe('mergeData', () => { - it('works with empty data', () => { - const data1: ScopeData = { - eventProcessors: [], - breadcrumbs: [], - user: {}, - tags: {}, - extra: {}, - contexts: {}, - attachments: [], - propagationContext: { spanId: '1', traceId: '1' }, - sdkProcessingMetadata: {}, - fingerprint: [], - }; - const data2: ScopeData = { - eventProcessors: [], - breadcrumbs: [], - user: {}, - tags: {}, - extra: {}, - contexts: {}, - attachments: [], - propagationContext: { spanId: '1', traceId: '1' }, - sdkProcessingMetadata: {}, - fingerprint: [], - }; - mergeData(data1, data2); - expect(data1).toEqual({ - eventProcessors: [], - breadcrumbs: [], - user: {}, - tags: {}, - extra: {}, - contexts: {}, - attachments: [], - propagationContext: { spanId: '1', traceId: '1' }, - sdkProcessingMetadata: {}, - fingerprint: [], - }); - }); - - it('merges data correctly', () => { - const attachment1 = { filename: '1' } as Attachment; - const attachment2 = { filename: '2' } as Attachment; - const attachment3 = { filename: '3' } as Attachment; - - const breadcrumb1 = { message: '1' } as Breadcrumb; - const breadcrumb2 = { message: '2' } as Breadcrumb; - const breadcrumb3 = { message: '3' } as Breadcrumb; - - const eventProcessor1 = ((a: unknown) => null) as EventProcessor; - const eventProcessor2 = ((b: unknown) => null) as EventProcessor; - const eventProcessor3 = ((c: unknown) => null) as EventProcessor; + } as Client; + const processedEvent = await prepareEvent( + options, + event, + { + integrations: [], + }, + scope, + client, + ); - const data1: ScopeData = { - eventProcessors: [eventProcessor1], - breadcrumbs: [breadcrumb1], - user: { id: '1', email: 'test@example.com' }, - tags: { tag1: 'aa', tag2: 'aa' }, - extra: { extra1: 'aa', extra2: 'aa' }, - contexts: { os: { name: 'os1' }, culture: { display_name: 'name1' } }, - attachments: [attachment1], - propagationContext: { spanId: '1', traceId: '1' }, - sdkProcessingMetadata: { aa: 'aa', bb: 'aa' }, - fingerprint: ['aa', 'bb'], - }; - const data2: ScopeData = { - eventProcessors: [eventProcessor2, eventProcessor3], - breadcrumbs: [breadcrumb2, breadcrumb3], - user: { id: '2', name: 'foo' }, - tags: { tag2: 'bb', tag3: 'bb' }, - extra: { extra2: 'bb', extra3: 'bb' }, - contexts: { os: { name: 'os2' } }, - attachments: [attachment2, attachment3], - propagationContext: { spanId: '2', traceId: '2' }, - sdkProcessingMetadata: { bb: 'bb', cc: 'bb' }, - fingerprint: ['cc'], - }; - mergeData(data1, data2); - expect(data1).toEqual({ - eventProcessors: [eventProcessor1, eventProcessor2, eventProcessor3], - breadcrumbs: [breadcrumb1, breadcrumb2, breadcrumb3], - user: { id: '2', name: 'foo', email: 'test@example.com' }, - tags: { tag1: 'aa', tag2: 'bb', tag3: 'bb' }, - extra: { extra1: 'aa', extra2: 'bb', extra3: 'bb' }, - contexts: { os: { name: 'os2' }, culture: { display_name: 'name1' } }, - attachments: [attachment1, attachment2, attachment3], - propagationContext: { spanId: '2', traceId: '2' }, - sdkProcessingMetadata: { aa: 'aa', bb: 'bb', cc: 'bb' }, - fingerprint: ['aa', 'bb', 'cc'], + expect(eventProcessor).toHaveBeenCalledWith(processedEvent, { + integrations: [], + // no attachments are added to hint }); - }); - }); - describe('applyToEvent', () => { - it('works without any data', async () => { - mockSdkInit(); - - const scope = new Scope(); - - const event = { message: 'foo' }; - applyScopeDataToEvent(event, scope.getScopeData()); - - expect(event).toEqual({ + expect(processedEvent).toEqual({ + timestamp: expect.any(Number), + event_id: expect.any(String), + environment: 'production', message: 'foo', sdkProcessingMetadata: { propagationContext: { @@ -335,6 +165,10 @@ describe('Unit | Scope', () => { const eventProcessor2 = jest.fn((b: unknown) => b) as EventProcessor; const eventProcessor3 = jest.fn((c: unknown) => c) as EventProcessor; + const attachment1 = { filename: '1' } as Attachment; + const attachment2 = { filename: '2' } as Attachment; + const attachment3 = { filename: '3' } as Attachment; + const scope = new Scope(); scope.update({ user: { id: '1', email: 'test@example.com' }, @@ -346,6 +180,7 @@ describe('Unit | Scope', () => { }); scope.addBreadcrumb(breadcrumb1); scope.addEventProcessor(eventProcessor1); + scope.addAttachment(attachment1); const globalScope = getGlobalScope(); const isolationScope = getIsolationScope(); @@ -353,16 +188,39 @@ describe('Unit | Scope', () => { globalScope.addBreadcrumb(breadcrumb2); globalScope.addEventProcessor(eventProcessor2); globalScope.setSDKProcessingMetadata({ aa: 'aa' }); + globalScope.addAttachment(attachment2); isolationScope.addBreadcrumb(breadcrumb3); isolationScope.addEventProcessor(eventProcessor3); - globalScope.setSDKProcessingMetadata({ bb: 'bb' }); + isolationScope.setSDKProcessingMetadata({ bb: 'bb' }); + isolationScope.addAttachment(attachment3); const event = { message: 'foo', breadcrumbs: [breadcrumb4], fingerprint: ['dd'] }; - applyScopeDataToEvent(event, scope.getScopeData()); + const options = {} as ClientOptions; + const processedEvent = await prepareEvent( + options, + event, + { + integrations: [], + }, + scope, + ); + + expect(eventProcessor1).toHaveBeenCalledTimes(1); + expect(eventProcessor2).toHaveBeenCalledTimes(1); + expect(eventProcessor3).toHaveBeenCalledTimes(1); + + // Test that attachments are correctly merged + expect(eventProcessor1).toHaveBeenCalledWith(processedEvent, { + integrations: [], + attachments: [attachment2, attachment3, attachment1], + }); - expect(event).toEqual({ + expect(processedEvent).toEqual({ + timestamp: expect.any(Number), + event_id: expect.any(String), + environment: 'production', message: 'foo', user: { id: '1', email: 'test@example.com' }, tags: { tag1: 'aa', tag2: 'aa' }, @@ -381,35 +239,4 @@ describe('Unit | Scope', () => { }); }); }); - - describe('getAttachments', () => { - it('works without any data', async () => { - mockSdkInit(); - - const scope = new Scope(); - - const actual = scope.getAttachments(); - expect(actual).toEqual([]); - }); - - it('merges attachments data', async () => { - mockSdkInit(); - - const attachment1 = { filename: '1' } as Attachment; - const attachment2 = { filename: '2' } as Attachment; - const attachment3 = { filename: '3' } as Attachment; - - const scope = new Scope(); - scope.addAttachment(attachment1); - - const globalScope = getGlobalScope(); - const isolationScope = getIsolationScope(); - - globalScope.addAttachment(attachment2); - isolationScope.addAttachment(attachment3); - - const actual = scope.getAttachments(); - expect(actual).toEqual([attachment2, attachment3, attachment1]); - }); - }); }); From 12a4fce26a3ae38741dae9c5c0f7e2290f40133e Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Thu, 21 Dec 2023 12:35:17 +0100 Subject: [PATCH 11/24] ref(deno): Refactor deno integrations to use functional syntax (#9929) Refactors deno integrations to functional syntax. --- packages/deno/src/integrations/context.ts | 35 +++--- .../deno/src/integrations/contextlines.ts | 105 +++++++---------- packages/deno/src/integrations/deno-cron.ts | 110 +++++++++--------- .../deno/src/integrations/globalhandlers.ts | 66 ++++------- .../deno/src/integrations/normalizepaths.ts | 73 ++++++------ 5 files changed, 177 insertions(+), 212 deletions(-) diff --git a/packages/deno/src/integrations/context.ts b/packages/deno/src/integrations/context.ts index f1c29dddda2b..e54f4ec21a87 100644 --- a/packages/deno/src/integrations/context.ts +++ b/packages/deno/src/integrations/context.ts @@ -1,4 +1,7 @@ -import type { Event, EventProcessor, Integration } from '@sentry/types'; +import { convertIntegrationFnToClass } from '@sentry/core'; +import type { Event, IntegrationFn } from '@sentry/types'; + +const INTEGRATION_NAME = 'DenoContext'; function getOSName(): string { switch (Deno.build.os) { @@ -19,7 +22,7 @@ function getOSRelease(): string | undefined { : undefined; } -async function denoRuntime(event: Event): Promise { +async function addDenoRuntimeContext(event: Event): Promise { event.contexts = { ...{ app: { @@ -49,21 +52,15 @@ async function denoRuntime(event: Event): Promise { return event; } -/** Adds Electron context to events. */ -export class DenoContext implements Integration { - /** @inheritDoc */ - public static id = 'DenoContext'; - - /** @inheritDoc */ - public name: string = DenoContext.id; - - /** @inheritDoc */ - public setupOnce(_addGlobalEventProcessor: (callback: EventProcessor) => void): void { - // noop - } +const denoContextIntegration: IntegrationFn = () => { + return { + name: INTEGRATION_NAME, + processEvent(event) { + return addDenoRuntimeContext(event); + }, + }; +}; - /** @inheritDoc */ - public processEvent(event: Event): Promise { - return denoRuntime(event); - } -} +/** Adds Deno context to events. */ +// eslint-disable-next-line deprecation/deprecation +export const DenoContext = convertIntegrationFnToClass(INTEGRATION_NAME, denoContextIntegration); diff --git a/packages/deno/src/integrations/contextlines.ts b/packages/deno/src/integrations/contextlines.ts index 8c6ef510fd2e..38fe0efd3433 100644 --- a/packages/deno/src/integrations/contextlines.ts +++ b/packages/deno/src/integrations/contextlines.ts @@ -1,6 +1,8 @@ -import type { Event, EventProcessor, Integration, StackFrame } from '@sentry/types'; +import { convertIntegrationFnToClass } from '@sentry/core'; +import type { Event, IntegrationFn, StackFrame } from '@sentry/types'; import { LRUMap, addContextToFrame } from '@sentry/utils'; +const INTEGRATION_NAME = 'ContextLines'; const FILE_CONTENT_CACHE = new LRUMap(100); const DEFAULT_LINES_OF_CONTEXT = 7; @@ -45,73 +47,54 @@ interface ContextLinesOptions { frameContextLines?: number; } -/** Add node modules / packages to the event */ -export class ContextLines implements Integration { - /** - * @inheritDoc - */ - public static id = 'ContextLines'; - - /** - * @inheritDoc - */ - public name: string = ContextLines.id; - - public constructor(private readonly _options: ContextLinesOptions = {}) {} - - /** Get's the number of context lines to add */ - private get _contextLines(): number { - return this._options.frameContextLines !== undefined ? this._options.frameContextLines : DEFAULT_LINES_OF_CONTEXT; - } - - /** - * @inheritDoc - */ - public setupOnce(_addGlobalEventProcessor: (callback: EventProcessor) => void): void { - // noop - } +const denoContextLinesIntegration: IntegrationFn = (options: ContextLinesOptions = {}) => { + const contextLines = options.frameContextLines !== undefined ? options.frameContextLines : DEFAULT_LINES_OF_CONTEXT; - /** @inheritDoc */ - public processEvent(event: Event): Promise { - return this.addSourceContext(event); - } + return { + name: INTEGRATION_NAME, + processEvent(event) { + return addSourceContext(event, contextLines); + }, + }; +}; - /** Processes an event and adds context lines */ - public async addSourceContext(event: Event): Promise { - if (this._contextLines > 0 && event.exception && event.exception.values) { - for (const exception of event.exception.values) { - if (exception.stacktrace && exception.stacktrace.frames) { - await this.addSourceContextToFrames(exception.stacktrace.frames); - } +/** Add node modules / packages to the event */ +// eslint-disable-next-line deprecation/deprecation +export const ContextLines = convertIntegrationFnToClass(INTEGRATION_NAME, denoContextLinesIntegration); + +/** Processes an event and adds context lines */ +async function addSourceContext(event: Event, contextLines: number): Promise { + if (contextLines > 0 && event.exception && event.exception.values) { + for (const exception of event.exception.values) { + if (exception.stacktrace && exception.stacktrace.frames) { + await addSourceContextToFrames(exception.stacktrace.frames, contextLines); } } - - return event; } - /** Adds context lines to frames */ - public async addSourceContextToFrames(frames: StackFrame[]): Promise { - const contextLines = this._contextLines; - - for (const frame of frames) { - // Only add context if we have a filename and it hasn't already been added - if (frame.filename && frame.in_app && frame.context_line === undefined) { - const permission = await Deno.permissions.query({ - name: 'read', - path: frame.filename, - }); - - if (permission.state == 'granted') { - const sourceFile = await readSourceFile(frame.filename); + return event; +} - if (sourceFile) { - try { - const lines = sourceFile.split('\n'); - addContextToFrame(lines, frame, contextLines); - } catch (_) { - // anomaly, being defensive in case - // unlikely to ever happen in practice but can definitely happen in theory - } +/** Adds context lines to frames */ +async function addSourceContextToFrames(frames: StackFrame[], contextLines: number): Promise { + for (const frame of frames) { + // Only add context if we have a filename and it hasn't already been added + if (frame.filename && frame.in_app && frame.context_line === undefined) { + const permission = await Deno.permissions.query({ + name: 'read', + path: frame.filename, + }); + + if (permission.state == 'granted') { + const sourceFile = await readSourceFile(frame.filename); + + if (sourceFile) { + try { + const lines = sourceFile.split('\n'); + addContextToFrame(lines, frame, contextLines); + } catch (_) { + // anomaly, being defensive in case + // unlikely to ever happen in practice but can definitely happen in theory } } } diff --git a/packages/deno/src/integrations/deno-cron.ts b/packages/deno/src/integrations/deno-cron.ts index 475d3e9131b7..a3bbcbaf59bd 100644 --- a/packages/deno/src/integrations/deno-cron.ts +++ b/packages/deno/src/integrations/deno-cron.ts @@ -1,6 +1,5 @@ -import { withMonitor } from '@sentry/core'; -import type { Integration } from '@sentry/types'; -import type { DenoClient } from '../client'; +import { convertIntegrationFnToClass, getClient, withMonitor } from '@sentry/core'; +import type { Client, IntegrationFn } from '@sentry/types'; import { parseScheduleToString } from './deno-cron-format'; type CronOptions = { backoffSchedule?: number[]; signal?: AbortSignal }; @@ -8,54 +7,59 @@ type CronFn = () => void | Promise; // Parameters doesn't work well with the overloads 🤔 type CronParams = [string, string | Deno.CronSchedule, CronFn | CronOptions, CronFn | CronOptions | undefined]; +const INTEGRATION_NAME = 'DenoCron'; + +const SETUP_CLIENTS: Client[] = []; + +const denoCronIntegration = (() => { + return { + name: INTEGRATION_NAME, + setupOnce() { + // eslint-disable-next-line deprecation/deprecation + if (!Deno.cron) { + // The cron API is not available in this Deno version use --unstable flag! + return; + } + + // eslint-disable-next-line deprecation/deprecation + Deno.cron = new Proxy(Deno.cron, { + apply(target, thisArg, argArray: CronParams) { + const [monitorSlug, schedule, opt1, opt2] = argArray; + let options: CronOptions | undefined; + let fn: CronFn; + + if (typeof opt1 === 'function' && typeof opt2 !== 'function') { + fn = opt1; + options = opt2; + } else if (typeof opt1 !== 'function' && typeof opt2 === 'function') { + fn = opt2; + options = opt1; + } + + async function cronCalled(): Promise { + if (SETUP_CLIENTS.includes(getClient() as Client)) { + return; + } + + await withMonitor(monitorSlug, async () => fn(), { + schedule: { type: 'crontab', value: parseScheduleToString(schedule) }, + // (minutes) so 12 hours - just a very high arbitrary number since we don't know the actual duration of the users cron job + maxRuntime: 60 * 12, + // Deno Deploy docs say that the cron job will be called within 1 minute of the scheduled time + checkinMargin: 1, + }); + } + + return target.call(thisArg, monitorSlug, schedule, options || {}, cronCalled); + }, + }); + }, + setup(client) { + SETUP_CLIENTS.push(client); + }, + }; +}) satisfies IntegrationFn; + /** Instruments Deno.cron to automatically capture cron check-ins */ -export class DenoCron implements Integration { - /** @inheritDoc */ - public static id = 'DenoCron'; - - /** @inheritDoc */ - public name: string = DenoCron.id; - - /** @inheritDoc */ - public setupOnce(): void { - // - } - - /** @inheritDoc */ - public setup(): void { - // eslint-disable-next-line deprecation/deprecation - if (!Deno.cron) { - // The cron API is not available in this Deno version use --unstable flag! - return; - } - - // eslint-disable-next-line deprecation/deprecation - Deno.cron = new Proxy(Deno.cron, { - apply(target, thisArg, argArray: CronParams) { - const [monitorSlug, schedule, opt1, opt2] = argArray; - let options: CronOptions | undefined; - let fn: CronFn; - - if (typeof opt1 === 'function' && typeof opt2 !== 'function') { - fn = opt1; - options = opt2; - } else if (typeof opt1 !== 'function' && typeof opt2 === 'function') { - fn = opt2; - options = opt1; - } - - async function cronCalled(): Promise { - await withMonitor(monitorSlug, async () => fn(), { - schedule: { type: 'crontab', value: parseScheduleToString(schedule) }, - // (minutes) so 12 hours - just a very high arbitrary number since we don't know the actual duration of the users cron job - maxRuntime: 60 * 12, - // Deno Deploy docs say that the cron job will be called within 1 minute of the scheduled time - checkinMargin: 1, - }); - } - - return target.call(thisArg, monitorSlug, schedule, options || {}, cronCalled); - }, - }); - } -} +// eslint-disable-next-line deprecation/deprecation +export const DenoCron = convertIntegrationFnToClass(INTEGRATION_NAME, denoCronIntegration); diff --git a/packages/deno/src/integrations/globalhandlers.ts b/packages/deno/src/integrations/globalhandlers.ts index 4160e3f4b3c6..06194037b6d1 100644 --- a/packages/deno/src/integrations/globalhandlers.ts +++ b/packages/deno/src/integrations/globalhandlers.ts @@ -1,57 +1,41 @@ import type { ServerRuntimeClient } from '@sentry/core'; +import { convertIntegrationFnToClass } from '@sentry/core'; import { captureEvent } from '@sentry/core'; import { getClient } from '@sentry/core'; import { flush } from '@sentry/core'; -import type { Client, Event, Integration, Primitive, StackParser } from '@sentry/types'; +import type { Client, Event, IntegrationFn, Primitive, StackParser } from '@sentry/types'; import { eventFromUnknownInput, isPrimitive } from '@sentry/utils'; type GlobalHandlersIntegrationsOptionKeys = 'error' | 'unhandledrejection'; -/** JSDoc */ type GlobalHandlersIntegrations = Record; +const INTEGRATION_NAME = 'GlobalHandlers'; let isExiting = false; -/** Global handlers */ -export class GlobalHandlers implements Integration { - /** - * @inheritDoc - */ - public static id = 'GlobalHandlers'; - - /** - * @inheritDoc - */ - public name: string = GlobalHandlers.id; - - /** JSDoc */ - private readonly _options: GlobalHandlersIntegrations; - - /** JSDoc */ - public constructor(options?: GlobalHandlersIntegrations) { - this._options = { - error: true, - unhandledrejection: true, - ...options, - }; - } - /** - * @inheritDoc - */ - public setupOnce(): void { - // noop - } +const globalHandlersIntegration: IntegrationFn = (options?: GlobalHandlersIntegrations) => { + const _options = { + error: true, + unhandledrejection: true, + ...options, + }; - /** @inheritdoc */ - public setup(client: Client): void { - if (this._options.error) { - installGlobalErrorHandler(client); - } - if (this._options.unhandledrejection) { - installGlobalUnhandledRejectionHandler(client); - } - } -} + return { + name: INTEGRATION_NAME, + setup(client) { + if (_options.error) { + installGlobalErrorHandler(client); + } + if (_options.unhandledrejection) { + installGlobalUnhandledRejectionHandler(client); + } + }, + }; +}; + +/** Global handlers */ +// eslint-disable-next-line deprecation/deprecation +export const GlobalHandlers = convertIntegrationFnToClass(INTEGRATION_NAME, globalHandlersIntegration); function installGlobalErrorHandler(client: Client): void { globalThis.addEventListener('error', data => { diff --git a/packages/deno/src/integrations/normalizepaths.ts b/packages/deno/src/integrations/normalizepaths.ts index ab705a3a20a0..a8143c8b078b 100644 --- a/packages/deno/src/integrations/normalizepaths.ts +++ b/packages/deno/src/integrations/normalizepaths.ts @@ -1,6 +1,9 @@ -import type { Event, EventProcessor, Integration } from '@sentry/types'; +import { convertIntegrationFnToClass } from '@sentry/core'; +import type { IntegrationFn } from '@sentry/types'; import { createStackParser, dirname, nodeStackLineParser } from '@sentry/utils'; +const INTEGRATION_NAME = 'NormalizePaths'; + function appRootFromErrorStack(error: Error): string | undefined { // We know at the other end of the stack from here is the entry point that called 'init' // We assume that this stacktrace will traverse the root of the app @@ -52,52 +55,46 @@ function getCwd(): string | undefined { return undefined; } -// Cached here -let appRoot: string | undefined; - -function getAppRoot(error: Error): string | undefined { - if (appRoot === undefined) { - appRoot = getCwd() || appRootFromErrorStack(error); - } - - return appRoot; -} - -/** Normalises paths to the app root directory. */ -export class NormalizePaths implements Integration { - /** @inheritDoc */ - public static id = 'NormalizePaths'; +const normalizePathsIntegration: IntegrationFn = () => { + // Cached here + let appRoot: string | undefined; - /** @inheritDoc */ - public name: string = NormalizePaths.id; + function getAppRoot(error: Error): string | undefined { + if (appRoot === undefined) { + appRoot = getCwd() || appRootFromErrorStack(error); + } - /** @inheritDoc */ - public setupOnce(_addGlobalEventProcessor: (callback: EventProcessor) => void): void { - // noop + return appRoot; } - /** @inheritDoc */ - public processEvent(event: Event): Event | null { - // This error.stack hopefully contains paths that traverse the app cwd - const error = new Error(); + return { + name: INTEGRATION_NAME, + processEvent(event) { + // This error.stack hopefully contains paths that traverse the app cwd + const error = new Error(); - const appRoot = getAppRoot(error); + const appRoot = getAppRoot(error); - if (appRoot) { - for (const exception of event.exception?.values || []) { - for (const frame of exception.stacktrace?.frames || []) { - if (frame.filename && frame.in_app) { - const startIndex = frame.filename.indexOf(appRoot); + if (appRoot) { + for (const exception of event.exception?.values || []) { + for (const frame of exception.stacktrace?.frames || []) { + if (frame.filename && frame.in_app) { + const startIndex = frame.filename.indexOf(appRoot); - if (startIndex > -1) { - const endIndex = startIndex + appRoot.length; - frame.filename = `app://${frame.filename.substring(endIndex)}`; + if (startIndex > -1) { + const endIndex = startIndex + appRoot.length; + frame.filename = `app://${frame.filename.substring(endIndex)}`; + } } } } } - } - return event; - } -} + return event; + }, + }; +}; + +/** Normalises paths to the app root directory. */ +// eslint-disable-next-line deprecation/deprecation +export const NormalizePaths = convertIntegrationFnToClass(INTEGRATION_NAME, normalizePathsIntegration); From a04e78077277620db9e08658b72c21b8b8baf743 Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Thu, 21 Dec 2023 15:48:30 +0100 Subject: [PATCH 12/24] feat(core): Add `span.end()` to replace `span.finish()` (#9954) To align with OTEL spans. This also deprecates `span.finish()`, and also makes sure that `end()` accepts both ms as well as s timestmaps (to align this with OTEL in the future, which accepts ms only). --------- Co-authored-by: Yagiz Nizipli --- packages/angular/src/tracing.ts | 8 +- packages/angular/test/tracing.test.ts | 14 +- .../startTransaction/basic_usage/subject.js | 8 +- .../startTransaction/circular_data/subject.js | 4 +- .../setMeasurement/subject.js | 2 +- .../browser/src/profiling/hubextensions.ts | 14 +- .../test/unit/profiling/integration.test.ts | 2 +- packages/core/src/exports.ts | 2 +- packages/core/src/tracing/idletransaction.ts | 35 ++-- packages/core/src/tracing/span.ts | 11 +- packages/core/src/tracing/trace.ts | 6 +- packages/core/src/tracing/transaction.ts | 11 +- packages/core/src/tracing/utils.ts | 8 + packages/core/test/lib/tracing/errors.test.ts | 6 +- .../create-next-app/pages/api/success.ts | 4 +- .../components/transaction-context.tsx | 2 +- .../node-express-app/src/app.ts | 4 +- packages/ember/addon/index.ts | 2 +- .../sentry-performance.ts | 16 +- .../appRouterRoutingInstrumentation.ts | 2 +- .../pagesRouterRoutingInstrumentation.ts | 4 +- .../src/common/utils/edgeWrapperUtils.ts | 2 +- .../nextjs/src/common/utils/responseEnd.ts | 2 +- .../nextjs/src/common/utils/wrapperUtils.ts | 4 +- packages/nextjs/test/clientSdk.test.ts | 2 +- .../nextjs/test/edge/withSentryAPI.test.ts | 2 +- .../pagesRouterInstrumentation.test.ts | 4 +- packages/nextjs/test/serverSdk.test.ts | 4 +- .../startTransaction/basic-usage/scenario.ts | 2 +- .../with-nested-spans/scenario.ts | 8 +- .../tracing-new/apollo-graphql/scenario.ts | 2 +- .../auto-instrument/mongodb/scenario.ts | 2 +- .../mysql/withConnect/scenario.ts | 2 +- .../mysql/withoutCallback/scenario.ts | 2 +- .../mysql/withoutConnect/scenario.ts | 2 +- .../auto-instrument/pg/scenario.ts | 2 +- .../suites/tracing-new/prisma-orm/scenario.ts | 2 +- .../tracePropagationTargets/scenario.ts | 2 +- .../suites/tracing/apollo-graphql/scenario.ts | 2 +- .../auto-instrument/mongodb/scenario.ts | 2 +- .../tracing/auto-instrument/mysql/scenario.ts | 2 +- .../tracing/auto-instrument/pg/scenario.ts | 2 +- .../suites/tracing/prisma-orm/scenario.ts | 2 +- .../tracePropagationTargets/scenario.ts | 2 +- packages/node/src/handlers.ts | 2 +- packages/node/src/integrations/hapi/index.ts | 4 +- packages/node/src/integrations/http.ts | 4 +- .../node/src/integrations/undici/index.ts | 4 +- packages/node/test/handlers.test.ts | 12 +- .../opentelemetry-node/src/spanprocessor.ts | 2 +- packages/opentelemetry/src/spanExporter.ts | 2 +- packages/react/src/profiler.tsx | 6 +- packages/react/src/reactrouter.tsx | 2 +- packages/react/src/reactrouterv3.ts | 2 +- packages/react/src/reactrouterv6.tsx | 2 +- packages/react/test/profiler.test.tsx | 2 +- packages/react/test/reactrouterv3.test.tsx | 2 +- packages/react/test/reactrouterv4.test.tsx | 2 +- packages/react/test/reactrouterv5.test.tsx | 2 +- packages/react/test/reactrouterv6.4.test.tsx | 2 +- packages/react/test/reactrouterv6.test.tsx | 2 +- packages/remix/src/client/performance.tsx | 4 +- packages/remix/src/utils/instrumentServer.ts | 6 +- .../remix/src/utils/serverAdapters/express.ts | 2 +- .../common/routes/manual-tracing.$id.tsx | 2 +- packages/serverless/src/awslambda.ts | 2 +- packages/serverless/src/awsservices.ts | 2 +- .../src/gcpfunction/cloud_events.ts | 2 +- packages/serverless/src/gcpfunction/events.ts | 2 +- packages/serverless/src/gcpfunction/http.ts | 2 +- packages/serverless/src/google-cloud-grpc.ts | 2 +- packages/serverless/src/google-cloud-http.ts | 2 +- .../serverless/test/__mocks__/@sentry/node.ts | 8 +- packages/serverless/test/awslambda.test.ts | 14 +- packages/serverless/test/awsservices.test.ts | 2 +- packages/serverless/test/gcpfunction.test.ts | 24 +-- packages/svelte/src/performance.ts | 4 +- packages/svelte/test/performance.test.ts | 34 ++-- packages/sveltekit/src/client/router.ts | 4 +- packages/sveltekit/test/client/router.test.ts | 4 +- .../src/browser/backgroundtab.ts | 2 +- .../src/browser/browsertracing.ts | 2 +- .../tracing-internal/src/browser/request.ts | 2 +- .../tracing-internal/src/browser/router.ts | 2 +- packages/tracing-internal/src/common/fetch.ts | 2 +- .../src/node/integrations/apollo.ts | 4 +- .../src/node/integrations/express.ts | 6 +- .../src/node/integrations/graphql.ts | 4 +- .../src/node/integrations/mongo.ts | 10 +- .../src/node/integrations/mysql.ts | 2 +- .../src/node/integrations/postgres.ts | 8 +- .../test/browser/browsertracing.test.ts | 20 +- .../test/browser/router.test.ts | 2 +- packages/tracing/test/hub.test.ts | 14 +- packages/tracing/test/idletransaction.test.ts | 60 +++--- .../test/integrations/apollo-nestjs.test.ts | 6 +- .../tracing/test/integrations/apollo.test.ts | 6 +- .../tracing/test/integrations/graphql.test.ts | 4 +- .../test/integrations/node/mongo.test.ts | 8 +- .../test/integrations/node/postgres.test.ts | 8 +- packages/tracing/test/span.test.ts | 183 ++++++++++++++++-- packages/tracing/test/transaction.test.ts | 4 +- packages/types/src/hub.ts | 2 +- packages/types/src/span.ts | 5 + packages/vue/src/tracing.ts | 6 +- 105 files changed, 472 insertions(+), 299 deletions(-) diff --git a/packages/angular/src/tracing.ts b/packages/angular/src/tracing.ts index 25ddc2fd8dcf..3138f1be1c53 100644 --- a/packages/angular/src/tracing.ts +++ b/packages/angular/src/tracing.ts @@ -82,7 +82,7 @@ export class TraceService implements OnDestroy { if (activeTransaction) { if (this._routingSpan) { - this._routingSpan.finish(); + this._routingSpan.end(); } this._routingSpan = activeTransaction.startChild({ description: `${navigationEvent.url}`, @@ -131,7 +131,7 @@ export class TraceService implements OnDestroy { if (this._routingSpan) { runOutsideAngular(() => { // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - this._routingSpan!.finish(); + this._routingSpan!.end(); }); this._routingSpan = null; } @@ -196,7 +196,7 @@ export class TraceDirective implements OnInit, AfterViewInit { */ public ngAfterViewInit(): void { if (this._tracingSpan) { - this._tracingSpan.finish(); + this._tracingSpan.end(); } } } @@ -239,7 +239,7 @@ export function TraceClassDecorator(): ClassDecorator { // eslint-disable-next-line @typescript-eslint/no-explicit-any target.prototype.ngAfterViewInit = function (...args: any[]): ReturnType { if (tracingSpan) { - tracingSpan.finish(); + tracingSpan.end(); } if (originalAfterViewInit) { return originalAfterViewInit.apply(this, args); diff --git a/packages/angular/test/tracing.test.ts b/packages/angular/test/tracing.test.ts index 635c8847b9bf..a407539026c5 100644 --- a/packages/angular/test/tracing.test.ts +++ b/packages/angular/test/tracing.test.ts @@ -154,7 +154,7 @@ describe('Angular Tracing', () => { const finishMock = jest.fn(); transaction.startChild = jest.fn(() => ({ - finish: finishMock, + end: finishMock, })); await env.navigateInAngular('/'); @@ -173,7 +173,7 @@ describe('Angular Tracing', () => { const finishMock = jest.fn(); transaction.startChild = jest.fn(() => ({ - finish: finishMock, + end: finishMock, })); await env.navigateInAngular('/'); @@ -199,7 +199,7 @@ describe('Angular Tracing', () => { const finishMock = jest.fn(); transaction.startChild = jest.fn(() => ({ - finish: finishMock, + end: finishMock, })); await env.navigateInAngular('/somewhere'); @@ -233,7 +233,7 @@ describe('Angular Tracing', () => { const finishMock = jest.fn(); transaction.startChild = jest.fn(() => ({ - finish: finishMock, + end: finishMock, })); await env.navigateInAngular('/cancel'); @@ -376,7 +376,7 @@ describe('Angular Tracing', () => { }); transaction.startChild = jest.fn(() => ({ - finish: finishMock, + end: finishMock, })); directive.componentName = 'test-component'; @@ -403,7 +403,7 @@ describe('Angular Tracing', () => { }); transaction.startChild = jest.fn(() => ({ - finish: finishMock, + end: finishMock, })); directive.ngOnInit(); @@ -437,7 +437,7 @@ describe('Angular Tracing', () => { it('Instruments `ngOnInit` and `ngAfterViewInit` methods of the decorated class', async () => { const finishMock = jest.fn(); const startChildMock = jest.fn(() => ({ - finish: finishMock, + end: finishMock, })); const customStartTransaction = jest.fn((ctx: any) => { diff --git a/packages/browser-integration-tests/suites/public-api/startTransaction/basic_usage/subject.js b/packages/browser-integration-tests/suites/public-api/startTransaction/basic_usage/subject.js index e46009e46b35..0d79bddf53a5 100644 --- a/packages/browser-integration-tests/suites/public-api/startTransaction/basic_usage/subject.js +++ b/packages/browser-integration-tests/suites/public-api/startTransaction/basic_usage/subject.js @@ -11,7 +11,7 @@ async function run() { await new Promise(resolve => setTimeout(resolve, 1)); // span_1 finishes - span_1.finish(); + span_1.end(); // span_2 doesn't finish const span_2 = transaction.startChild({ op: 'span_2' }); @@ -24,12 +24,12 @@ async function run() { const span_4 = span_3.startChild({ op: 'span_4', data: { qux: 'quux' } }); // span_5 is another child of span_3 but finishes. - const span_5 = span_3.startChild({ op: 'span_5' }).finish(); + const span_5 = span_3.startChild({ op: 'span_5' }).end(); // span_3 also finishes - span_3.finish(); + span_3.end(); - transaction.finish(); + transaction.end(); } run(); diff --git a/packages/browser-integration-tests/suites/public-api/startTransaction/circular_data/subject.js b/packages/browser-integration-tests/suites/public-api/startTransaction/circular_data/subject.js index 50f8cef000be..d2ae465addf7 100644 --- a/packages/browser-integration-tests/suites/public-api/startTransaction/circular_data/subject.js +++ b/packages/browser-integration-tests/suites/public-api/startTransaction/circular_data/subject.js @@ -7,5 +7,5 @@ const circularObject = chicken; const transaction = Sentry.startTransaction({ name: 'circular_object_test_transaction', data: circularObject }); const span = transaction.startChild({ op: 'circular_object_test_span', data: circularObject }); -span.finish(); -transaction.finish(); +span.end(); +transaction.end(); diff --git a/packages/browser-integration-tests/suites/public-api/startTransaction/setMeasurement/subject.js b/packages/browser-integration-tests/suites/public-api/startTransaction/setMeasurement/subject.js index 036e86201b18..5b14dd7b680b 100644 --- a/packages/browser-integration-tests/suites/public-api/startTransaction/setMeasurement/subject.js +++ b/packages/browser-integration-tests/suites/public-api/startTransaction/setMeasurement/subject.js @@ -5,4 +5,4 @@ transaction.setMeasurement('metric.bar', 1337, 'nanoseconds'); transaction.setMeasurement('metric.baz', 99, 's'); transaction.setMeasurement('metric.baz', 1); -transaction.finish(); +transaction.end(); diff --git a/packages/browser/src/profiling/hubextensions.ts b/packages/browser/src/profiling/hubextensions.ts index f042ae016609..462929fff04b 100644 --- a/packages/browser/src/profiling/hubextensions.ts +++ b/packages/browser/src/profiling/hubextensions.ts @@ -143,34 +143,34 @@ export function startProfileForTransaction(transaction: Transaction): Transactio onProfileHandler(); }, MAX_PROFILE_DURATION_MS); - // We need to reference the original finish call to avoid creating an infinite loop - const originalFinish = transaction.finish.bind(transaction); + // We need to reference the original end call to avoid creating an infinite loop + const originalEnd = transaction.end.bind(transaction); /** * Wraps startTransaction and stopTransaction with profiling related logic. * startProfiling is called after the call to startTransaction in order to avoid our own code from * being profiled. Because of that same reason, stopProfiling is called before the call to stopTransaction. */ - function profilingWrappedTransactionFinish(): Transaction { + function profilingWrappedTransactionEnd(): Transaction { if (!transaction) { - return originalFinish(); + return originalEnd(); } // onProfileHandler should always return the same profile even if this is called multiple times. // Always call onProfileHandler to ensure stopProfiling is called and the timeout is cleared. void onProfileHandler().then( () => { transaction.setContext('profile', { profile_id: profileId, start_timestamp: startTimestamp }); - originalFinish(); + originalEnd(); }, () => { // If onProfileHandler fails, we still want to call the original finish method. - originalFinish(); + originalEnd(); }, ); return transaction; } - transaction.finish = profilingWrappedTransactionFinish; + transaction.end = profilingWrappedTransactionEnd; return transaction; } diff --git a/packages/browser/test/unit/profiling/integration.test.ts b/packages/browser/test/unit/profiling/integration.test.ts index 515398638048..ae95927ac2cd 100644 --- a/packages/browser/test/unit/profiling/integration.test.ts +++ b/packages/browser/test/unit/profiling/integration.test.ts @@ -52,7 +52,7 @@ describe('BrowserProfilingIntegration', () => { const currentTransaction = Sentry.getCurrentHub().getScope().getTransaction(); expect(currentTransaction?.op).toBe('pageload'); - currentTransaction?.finish(); + currentTransaction?.end(); await client?.flush(1000); expect(send).toHaveBeenCalledTimes(1); diff --git a/packages/core/src/exports.ts b/packages/core/src/exports.ts index 95c1e4b63de3..d7757e7d5d37 100644 --- a/packages/core/src/exports.ts +++ b/packages/core/src/exports.ts @@ -179,7 +179,7 @@ export function withScope(callback: (scope: Scope) => T): T { * * Every child span must be finished before the transaction is finished, otherwise the unfinished spans are discarded. * - * The transaction must be finished with a call to its `.finish()` method, at which point the transaction with all its + * The transaction must be finished with a call to its `.end()` method, at which point the transaction with all its * finished child spans will be sent to Sentry. * * NOTE: This function should only be used for *manual* instrumentation. Auto-instrumentation should call diff --git a/packages/core/src/tracing/idletransaction.ts b/packages/core/src/tracing/idletransaction.ts index 75630de373f1..458bd9281627 100644 --- a/packages/core/src/tracing/idletransaction.ts +++ b/packages/core/src/tracing/idletransaction.ts @@ -7,6 +7,7 @@ import type { Hub } from '../hub'; import type { Span } from './span'; import { SpanRecorder } from './span'; import { Transaction } from './transaction'; +import { ensureTimestampInSeconds } from './utils'; export const TRACING_DEFAULTS = { idleTimeout: 1000, @@ -45,10 +46,12 @@ export class IdleTransactionSpanRecorder extends SpanRecorder { // We should make sure we do not push and pop activities for // the transaction that this span recorder belongs to. if (span.spanId !== this.transactionSpanId) { - // We patch span.finish() to pop an activity after setting an endTimestamp. - span.finish = (endTimestamp?: number) => { - span.endTimestamp = typeof endTimestamp === 'number' ? endTimestamp : timestampInSeconds(); + // We patch span.end() to pop an activity after setting an endTimestamp. + // eslint-disable-next-line @typescript-eslint/unbound-method + const originalEnd = span.end; + span.end = (...rest: unknown[]) => { this._popActivity(span.spanId); + return originalEnd.apply(span, rest); }; // We should only push new activities if the span does not have an end timestamp. @@ -129,13 +132,15 @@ export class IdleTransaction extends Transaction { if (!this._finished) { this.setStatus('deadline_exceeded'); this._finishReason = IDLE_TRANSACTION_FINISH_REASONS[3]; - this.finish(); + this.end(); } }, this._finalTimeout); } /** {@inheritDoc} */ - public finish(endTimestamp: number = timestampInSeconds()): string | undefined { + public end(endTimestamp: number = timestampInSeconds()): string | undefined { + const endTimestampInS = ensureTimestampInSeconds(endTimestamp); + this._finished = true; this.activities = {}; @@ -145,7 +150,7 @@ export class IdleTransaction extends Transaction { if (this.spanRecorder) { DEBUG_BUILD && - logger.log('[Tracing] finishing IdleTransaction', new Date(endTimestamp * 1000).toISOString(), this.op); + logger.log('[Tracing] finishing IdleTransaction', new Date(endTimestampInS * 1000).toISOString(), this.op); for (const callback of this._beforeFinishCallbacks) { callback(this, endTimestamp); @@ -159,13 +164,13 @@ export class IdleTransaction extends Transaction { // We cancel all pending spans with status "cancelled" to indicate the idle transaction was finished early if (!span.endTimestamp) { - span.endTimestamp = endTimestamp; + span.endTimestamp = endTimestampInS; span.setStatus('cancelled'); DEBUG_BUILD && logger.log('[Tracing] cancelling span since transaction ended early', JSON.stringify(span, undefined, 2)); } - const spanStartedBeforeTransactionFinish = span.startTimestamp < endTimestamp; + const spanStartedBeforeTransactionFinish = span.startTimestamp < endTimestampInS; // Add a delta with idle timeout so that we prevent false positives const timeoutWithMarginOfError = (this._finalTimeout + this._idleTimeout) / 1000; @@ -196,7 +201,7 @@ export class IdleTransaction extends Transaction { } } - return super.finish(endTimestamp); + return super.end(endTimestamp); } /** @@ -244,7 +249,7 @@ export class IdleTransaction extends Transaction { * with the last child span. */ public cancelIdleTimeout( - endTimestamp?: Parameters[0], + endTimestamp?: Parameters[0], { restartOnChildSpanChange, }: { @@ -260,7 +265,7 @@ export class IdleTransaction extends Transaction { if (Object.keys(this.activities).length === 0 && this._idleTimeoutCanceledPermanently) { this._finishReason = IDLE_TRANSACTION_FINISH_REASONS[5]; - this.finish(endTimestamp); + this.end(endTimestamp); } } } @@ -281,12 +286,12 @@ export class IdleTransaction extends Transaction { /** * Restarts idle timeout, if there is no running idle timeout it will start one. */ - private _restartIdleTimeout(endTimestamp?: Parameters[0]): void { + private _restartIdleTimeout(endTimestamp?: Parameters[0]): void { this.cancelIdleTimeout(); this._idleTimeoutID = setTimeout(() => { if (!this._finished && Object.keys(this.activities).length === 0) { this._finishReason = IDLE_TRANSACTION_FINISH_REASONS[1]; - this.finish(endTimestamp); + this.end(endTimestamp); } }, this._idleTimeout); } @@ -318,7 +323,7 @@ export class IdleTransaction extends Transaction { const endTimestamp = timestampInSeconds(); if (this._idleTimeoutCanceledPermanently) { this._finishReason = IDLE_TRANSACTION_FINISH_REASONS[5]; - this.finish(endTimestamp); + this.end(endTimestamp); } else { // We need to add the timeout here to have the real endtimestamp of the transaction // Remember timestampInSeconds is in seconds, timeout is in ms @@ -351,7 +356,7 @@ export class IdleTransaction extends Transaction { DEBUG_BUILD && logger.log('[Tracing] Transaction finished because of no change for 3 heart beats'); this.setStatus('deadline_exceeded'); this._finishReason = IDLE_TRANSACTION_FINISH_REASONS[0]; - this.finish(); + this.end(); } else { this._pingHeartbeat(); } diff --git a/packages/core/src/tracing/span.ts b/packages/core/src/tracing/span.ts index 4b341a71e2c2..80704d44c9a7 100644 --- a/packages/core/src/tracing/span.ts +++ b/packages/core/src/tracing/span.ts @@ -11,6 +11,7 @@ import type { import { dropUndefinedKeys, generateSentryTraceHeader, logger, timestampInSeconds, uuid4 } from '@sentry/utils'; import { DEBUG_BUILD } from '../debug-build'; +import { ensureTimestampInSeconds } from './utils'; /** * Keeps track of finished spans for a given transaction @@ -259,8 +260,15 @@ export class Span implements SpanInterface { /** * @inheritDoc + * + * @deprecated Use `.end()` instead. */ public finish(endTimestamp?: number): void { + return this.end(endTimestamp); + } + + /** @inheritdoc */ + public end(endTimestamp?: number): void { if ( DEBUG_BUILD && // Don't call this for transactions @@ -273,7 +281,8 @@ export class Span implements SpanInterface { } } - this.endTimestamp = typeof endTimestamp === 'number' ? endTimestamp : timestampInSeconds(); + this.endTimestamp = + typeof endTimestamp === 'number' ? ensureTimestampInSeconds(endTimestamp) : timestampInSeconds(); } /** diff --git a/packages/core/src/tracing/trace.ts b/packages/core/src/tracing/trace.ts index cc73fe009e3d..52c915c30189 100644 --- a/packages/core/src/tracing/trace.ts +++ b/packages/core/src/tracing/trace.ts @@ -39,7 +39,7 @@ export function trace( scope.setSpan(activeSpan); function finishAndSetSpan(): void { - activeSpan && activeSpan.finish(); + activeSpan && activeSpan.end(); scope.setSpan(parentSpan); } @@ -97,7 +97,7 @@ export function startSpan(context: TransactionContext, callback: (span: Span scope.setSpan(activeSpan); function finishAndSetSpan(): void { - activeSpan && activeSpan.finish(); + activeSpan && activeSpan.end(); scope.setSpan(parentSpan); } @@ -157,7 +157,7 @@ export function startSpanManual( scope.setSpan(activeSpan); function finishAndSetSpan(): void { - activeSpan && activeSpan.finish(); + activeSpan && activeSpan.end(); scope.setSpan(parentSpan); } diff --git a/packages/core/src/tracing/transaction.ts b/packages/core/src/tracing/transaction.ts index be2e7324769a..59591971d24e 100644 --- a/packages/core/src/tracing/transaction.ts +++ b/packages/core/src/tracing/transaction.ts @@ -9,13 +9,14 @@ import type { TransactionEvent, TransactionMetadata, } from '@sentry/types'; -import { dropUndefinedKeys, logger } from '@sentry/utils'; +import { dropUndefinedKeys, logger, timestampInSeconds } from '@sentry/utils'; import { DEBUG_BUILD } from '../debug-build'; import type { Hub } from '../hub'; import { getCurrentHub } from '../hub'; import { getDynamicSamplingContextFromClient } from './dynamicSamplingContext'; import { Span as SpanClass, SpanRecorder } from './span'; +import { ensureTimestampInSeconds } from './utils'; /** JSDoc */ export class Transaction extends SpanClass implements TransactionInterface { @@ -134,8 +135,10 @@ export class Transaction extends SpanClass implements TransactionInterface { /** * @inheritDoc */ - public finish(endTimestamp?: number): string | undefined { - const transaction = this._finishTransaction(endTimestamp); + public end(endTimestamp?: number): string | undefined { + const timestampInS = + typeof endTimestamp === 'number' ? ensureTimestampInSeconds(endTimestamp) : timestampInSeconds(); + const transaction = this._finishTransaction(timestampInS); if (!transaction) { return undefined; } @@ -232,7 +235,7 @@ export class Transaction extends SpanClass implements TransactionInterface { } // just sets the end timestamp - super.finish(endTimestamp); + super.end(endTimestamp); const client = this._hub.getClient(); if (client && client.emit) { diff --git a/packages/core/src/tracing/utils.ts b/packages/core/src/tracing/utils.ts index f1b4c0f1ae06..4c1d49780554 100644 --- a/packages/core/src/tracing/utils.ts +++ b/packages/core/src/tracing/utils.ts @@ -27,3 +27,11 @@ export { stripUrlQueryAndFragment } from '@sentry/utils'; * @deprecated Import this function from `@sentry/utils` instead */ export const extractTraceparentData = _extractTraceparentData; + +/** + * Converts a timestamp to second, if it was in milliseconds, or keeps it as second. + */ +export function ensureTimestampInSeconds(timestamp: number): number { + const isMs = timestamp > 9999999999; + return isMs ? timestamp / 1000 : timestamp; +} diff --git a/packages/core/test/lib/tracing/errors.test.ts b/packages/core/test/lib/tracing/errors.test.ts index 20db043865a9..60b5db5c0c1d 100644 --- a/packages/core/test/lib/tracing/errors.test.ts +++ b/packages/core/test/lib/tracing/errors.test.ts @@ -61,7 +61,7 @@ describe('registerErrorHandlers()', () => { mockUnhandledRejectionCallback({}); expect(transaction.status).toBe(undefined); - transaction.finish(); + transaction.end(); }); it('sets status for transaction on scope on error', () => { @@ -72,7 +72,7 @@ describe('registerErrorHandlers()', () => { mockErrorCallback({} as HandlerDataError); expect(transaction.status).toBe('internal_error'); - transaction.finish(); + transaction.end(); }); it('sets status for transaction on scope on unhandledrejection', () => { @@ -82,6 +82,6 @@ describe('registerErrorHandlers()', () => { mockUnhandledRejectionCallback({}); expect(transaction.status).toBe('internal_error'); - transaction.finish(); + transaction.end(); }); }); diff --git a/packages/e2e-tests/test-applications/create-next-app/pages/api/success.ts b/packages/e2e-tests/test-applications/create-next-app/pages/api/success.ts index 7585c88f0ab1..8a5a53f2e4dc 100644 --- a/packages/e2e-tests/test-applications/create-next-app/pages/api/success.ts +++ b/packages/e2e-tests/test-applications/create-next-app/pages/api/success.ts @@ -8,8 +8,8 @@ export default function handler(req: NextApiRequest, res: NextApiResponse) { const span = transaction.startChild(); - span.finish(); - transaction.finish(); + span.end(); + transaction.end(); Sentry.flush().then(() => { res.status(200).json({ diff --git a/packages/e2e-tests/test-applications/nextjs-app-dir/components/transaction-context.tsx b/packages/e2e-tests/test-applications/nextjs-app-dir/components/transaction-context.tsx index 4102a1a422a8..ef1915b98af7 100644 --- a/packages/e2e-tests/test-applications/nextjs-app-dir/components/transaction-context.tsx +++ b/packages/e2e-tests/test-applications/nextjs-app-dir/components/transaction-context.tsx @@ -21,7 +21,7 @@ export function TransactionContextProvider({ children }: PropsWithChildren) { ? { transactionActive: true, stop: () => { - transaction.finish(); + transaction.end(); setTransaction(undefined); }, } diff --git a/packages/e2e-tests/test-applications/node-express-app/src/app.ts b/packages/e2e-tests/test-applications/node-express-app/src/app.ts index 330a425cb494..269f8df45bbe 100644 --- a/packages/e2e-tests/test-applications/node-express-app/src/app.ts +++ b/packages/e2e-tests/test-applications/node-express-app/src/app.ts @@ -39,8 +39,8 @@ app.get('/test-transaction', async function (req, res) { const span = transaction.startChild(); - span.finish(); - transaction.finish(); + span.end(); + transaction.end(); await Sentry.flush(); diff --git a/packages/ember/addon/index.ts b/packages/ember/addon/index.ts index ef38d3c382b3..2a5da643c984 100644 --- a/packages/ember/addon/index.ts +++ b/packages/ember/addon/index.ts @@ -94,7 +94,7 @@ export const instrumentRoutePerformance = (BaseRoute origin: 'auto.ui.ember', startTimestamp, }) - .finish(); + .end(); return result; }; diff --git a/packages/ember/addon/instance-initializers/sentry-performance.ts b/packages/ember/addon/instance-initializers/sentry-performance.ts index c6c2eb9e4325..41d10842fa7c 100644 --- a/packages/ember/addon/instance-initializers/sentry-performance.ts +++ b/packages/ember/addon/instance-initializers/sentry-performance.ts @@ -132,13 +132,13 @@ export function _instrumentEmberRouter( if (nextInstance) { return; } - activeTransaction?.finish(); + activeTransaction?.end(); getBackburner().off('end', finishActiveTransaction); }; routerService.on('routeWillChange', (transition: Transition) => { const { fromRoute, toRoute } = getTransitionInformation(transition, routerService); - activeTransaction?.finish(); + activeTransaction?.end(); activeTransaction = startTransaction({ name: `route:${toRoute}`, op: 'navigation', @@ -160,10 +160,10 @@ export function _instrumentEmberRouter( if (!transitionSpan || !activeTransaction) { return; } - transitionSpan.finish(); + transitionSpan.end(); if (disableRunloopPerformance) { - activeTransaction.finish(); + activeTransaction.end(); return; } @@ -200,7 +200,7 @@ function _instrumentEmberRunloop(config: EmberSentryConfig): void { return; } if (currentQueueSpan) { - currentQueueSpan.finish(); + currentQueueSpan.end(); } currentQueueStart = timestampInSeconds(); @@ -218,7 +218,7 @@ function _instrumentEmberRunloop(config: EmberSentryConfig): void { startTimestamp: currentQueueStart, endTimestamp: now, }) - .finish(); + .end(); } currentQueueStart = undefined; } @@ -241,7 +241,7 @@ function _instrumentEmberRunloop(config: EmberSentryConfig): void { return; } if (currentQueueSpan) { - currentQueueSpan.finish(); + currentQueueSpan.end(); currentQueueSpan = undefined; } }); @@ -378,7 +378,7 @@ function _instrumentInitialLoad(config: EmberSentryConfig): void { origin: 'auto.ui.ember', startTimestamp, }); - span?.finish(endTimestamp); + span?.end(endTimestamp); performance.clearMarks(startName); performance.clearMarks(endName); diff --git a/packages/nextjs/src/client/routing/appRouterRoutingInstrumentation.ts b/packages/nextjs/src/client/routing/appRouterRoutingInstrumentation.ts index 8d45f4b9ddb3..3083013e084a 100644 --- a/packages/nextjs/src/client/routing/appRouterRoutingInstrumentation.ts +++ b/packages/nextjs/src/client/routing/appRouterRoutingInstrumentation.ts @@ -63,7 +63,7 @@ export function appRouterInstrumentation( prevLocationName = transactionName; if (activeTransaction) { - activeTransaction.finish(); + activeTransaction.end(); } startTransactionCb({ diff --git a/packages/nextjs/src/client/routing/pagesRouterRoutingInstrumentation.ts b/packages/nextjs/src/client/routing/pagesRouterRoutingInstrumentation.ts index a7c3d5bd2344..fae0624e7005 100644 --- a/packages/nextjs/src/client/routing/pagesRouterRoutingInstrumentation.ts +++ b/packages/nextjs/src/client/routing/pagesRouterRoutingInstrumentation.ts @@ -169,7 +169,7 @@ export function pagesRouterInstrumentation( prevLocationName = transactionName; if (activeTransaction) { - activeTransaction.finish(); + activeTransaction.end(); } const navigationTransaction = startTransactionCb({ @@ -193,7 +193,7 @@ export function pagesRouterInstrumentation( }); const finishRouteChangeSpan = (): void => { - nextRouteChangeSpan.finish(); + nextRouteChangeSpan.end(); Router.events.off('routeChangeComplete', finishRouteChangeSpan); }; diff --git a/packages/nextjs/src/common/utils/edgeWrapperUtils.ts b/packages/nextjs/src/common/utils/edgeWrapperUtils.ts index afdf686499c5..8763f87854d3 100644 --- a/packages/nextjs/src/common/utils/edgeWrapperUtils.ts +++ b/packages/nextjs/src/common/utils/edgeWrapperUtils.ts @@ -96,7 +96,7 @@ export function withEdgeWrapping( throw objectifiedErr; } finally { - span?.finish(); + span?.end(); currentScope?.setSpan(prevSpan); await flushQueue(); } diff --git a/packages/nextjs/src/common/utils/responseEnd.ts b/packages/nextjs/src/common/utils/responseEnd.ts index 64a3d2c59c52..e59a99fb0ebb 100644 --- a/packages/nextjs/src/common/utils/responseEnd.ts +++ b/packages/nextjs/src/common/utils/responseEnd.ts @@ -42,7 +42,7 @@ export function autoEndTransactionOnResponseEnd(transaction: Transaction, res: S export function finishTransaction(transaction: Transaction | undefined, res: ServerResponse): void { if (transaction) { transaction.setHttpStatus(res.statusCode); - transaction.finish(); + transaction.end(); } } diff --git a/packages/nextjs/src/common/utils/wrapperUtils.ts b/packages/nextjs/src/common/utils/wrapperUtils.ts index 5451b1264723..e25220ce61c2 100644 --- a/packages/nextjs/src/common/utils/wrapperUtils.ts +++ b/packages/nextjs/src/common/utils/wrapperUtils.ts @@ -162,7 +162,7 @@ export function withTracedServerSideDataFetcher Pr previousSpan?.setStatus('internal_error'); throw e; } finally { - dataFetcherSpan.finish(); + dataFetcherSpan.end(); scope.setSpan(previousSpan); if (!platformSupportsStreaming()) { await flushQueue(); @@ -219,7 +219,7 @@ export async function callDataFetcherTraced Promis // that set the transaction status, we need to manually set the status of the span & transaction transaction.setStatus('internal_error'); span.setStatus('internal_error'); - span.finish(); + span.end(); // TODO Copy more robust error handling over from `withSentry` captureException(err, { mechanism: { handled: false } }); diff --git a/packages/nextjs/test/clientSdk.test.ts b/packages/nextjs/test/clientSdk.test.ts index ebedef2506c0..1b35f82cbfe8 100644 --- a/packages/nextjs/test/clientSdk.test.ts +++ b/packages/nextjs/test/clientSdk.test.ts @@ -90,7 +90,7 @@ describe('Client init()', () => { const transportSend = jest.spyOn(hub.getClient()!.getTransport()!, 'send'); const transaction = hub.startTransaction({ name: '/404' }); - transaction.finish(); + transaction.end(); expect(transportSend).not.toHaveBeenCalled(); expect(captureEvent.mock.results[0].value).toBeUndefined(); diff --git a/packages/nextjs/test/edge/withSentryAPI.test.ts b/packages/nextjs/test/edge/withSentryAPI.test.ts index d2dbc9385c44..cd860d886826 100644 --- a/packages/nextjs/test/edge/withSentryAPI.test.ts +++ b/packages/nextjs/test/edge/withSentryAPI.test.ts @@ -91,7 +91,7 @@ describe('wrapApiHandlerWithSentry', () => { }), ); - testTransaction.finish(); + testTransaction.end(); coreSdk.getCurrentHub().getScope().setSpan(undefined); }); }); diff --git a/packages/nextjs/test/performance/pagesRouterInstrumentation.test.ts b/packages/nextjs/test/performance/pagesRouterInstrumentation.test.ts index 107f02eb9af6..592df911bde2 100644 --- a/packages/nextjs/test/performance/pagesRouterInstrumentation.test.ts +++ b/packages/nextjs/test/performance/pagesRouterInstrumentation.test.ts @@ -50,9 +50,9 @@ function createMockStartTransaction() { () => ({ startChild: () => ({ - finish: () => undefined, + end: () => undefined, }), - finish: () => undefined, + end: () => undefined, }) as Transaction, ); } diff --git a/packages/nextjs/test/serverSdk.test.ts b/packages/nextjs/test/serverSdk.test.ts index 3467f2baa548..0813d4931874 100644 --- a/packages/nextjs/test/serverSdk.test.ts +++ b/packages/nextjs/test/serverSdk.test.ts @@ -106,9 +106,9 @@ describe('Server init()', () => { const transportSend = jest.spyOn(hub.getClient()!.getTransport()!, 'send'); const transaction = hub.startTransaction({ name: '/404' }); - transaction.finish(); + transaction.end(); - // We need to flush because the event processor pipeline is async whereas transaction.finish() is sync. + // We need to flush because the event processor pipeline is async whereas transaction.end() is sync. await SentryNode.flush(); expect(transportSend).not.toHaveBeenCalled(); diff --git a/packages/node-integration-tests/suites/public-api/startTransaction/basic-usage/scenario.ts b/packages/node-integration-tests/suites/public-api/startTransaction/basic-usage/scenario.ts index c5134123351b..1e4931a2bae7 100644 --- a/packages/node-integration-tests/suites/public-api/startTransaction/basic-usage/scenario.ts +++ b/packages/node-integration-tests/suites/public-api/startTransaction/basic-usage/scenario.ts @@ -11,4 +11,4 @@ Sentry.init({ const transaction = Sentry.startTransaction({ name: 'test_transaction_1' }); -transaction.finish(); +transaction.end(); diff --git a/packages/node-integration-tests/suites/public-api/startTransaction/with-nested-spans/scenario.ts b/packages/node-integration-tests/suites/public-api/startTransaction/with-nested-spans/scenario.ts index 5fe8dd195de6..a340de7b21fe 100644 --- a/packages/node-integration-tests/suites/public-api/startTransaction/with-nested-spans/scenario.ts +++ b/packages/node-integration-tests/suites/public-api/startTransaction/with-nested-spans/scenario.ts @@ -19,7 +19,7 @@ const span_1 = transaction.startChild({ for (let i = 0; i < 2000; i++); // span_1 finishes -span_1.finish(); +span_1.end(); // span_2 doesn't finish transaction.startChild({ op: 'span_2' }); @@ -32,9 +32,9 @@ for (let i = 0; i < 4000; i++); span_3.startChild({ op: 'span_4', data: { qux: 'quux' } }); // span_5 is another child of span_3 but finishes. -span_3.startChild({ op: 'span_5' }).finish(); +span_3.startChild({ op: 'span_5' }).end(); // span_3 also finishes -span_3.finish(); +span_3.end(); -transaction.finish(); +transaction.end(); diff --git a/packages/node-integration-tests/suites/tracing-new/apollo-graphql/scenario.ts b/packages/node-integration-tests/suites/tracing-new/apollo-graphql/scenario.ts index 0c53294d1f4f..b37bd6df6fc9 100644 --- a/packages/node-integration-tests/suites/tracing-new/apollo-graphql/scenario.ts +++ b/packages/node-integration-tests/suites/tracing-new/apollo-graphql/scenario.ts @@ -38,5 +38,5 @@ Sentry.getCurrentScope().setSpan(transaction); query: '{hello}', }); - transaction.finish(); + transaction.end(); })(); diff --git a/packages/node-integration-tests/suites/tracing-new/auto-instrument/mongodb/scenario.ts b/packages/node-integration-tests/suites/tracing-new/auto-instrument/mongodb/scenario.ts index 9d747e2eff4b..36f8c3503832 100644 --- a/packages/node-integration-tests/suites/tracing-new/auto-instrument/mongodb/scenario.ts +++ b/packages/node-integration-tests/suites/tracing-new/auto-instrument/mongodb/scenario.ts @@ -36,7 +36,7 @@ async function run(): Promise { await collection.find({ title: 'South Park' }).toArray(); } finally { - if (transaction) transaction.finish(); + if (transaction) transaction.end(); await client.close(); } } diff --git a/packages/node-integration-tests/suites/tracing-new/auto-instrument/mysql/withConnect/scenario.ts b/packages/node-integration-tests/suites/tracing-new/auto-instrument/mysql/withConnect/scenario.ts index 7d94099ea30c..9b033e72a669 100644 --- a/packages/node-integration-tests/suites/tracing-new/auto-instrument/mysql/withConnect/scenario.ts +++ b/packages/node-integration-tests/suites/tracing-new/auto-instrument/mysql/withConnect/scenario.ts @@ -28,7 +28,7 @@ Sentry.getCurrentScope().setSpan(transaction); connection.query('SELECT 1 + 1 AS solution', function () { connection.query('SELECT NOW()', ['1', '2'], () => { - if (transaction) transaction.finish(); + if (transaction) transaction.end(); connection.end(); }); }); diff --git a/packages/node-integration-tests/suites/tracing-new/auto-instrument/mysql/withoutCallback/scenario.ts b/packages/node-integration-tests/suites/tracing-new/auto-instrument/mysql/withoutCallback/scenario.ts index 4b3346caed20..23d07f346875 100644 --- a/packages/node-integration-tests/suites/tracing-new/auto-instrument/mysql/withoutCallback/scenario.ts +++ b/packages/node-integration-tests/suites/tracing-new/auto-instrument/mysql/withoutCallback/scenario.ts @@ -37,7 +37,7 @@ query.on('end', () => { // Wait a bit to ensure the queries completed setTimeout(() => { - transaction.finish(); + transaction.end(); }, 500); }); }); diff --git a/packages/node-integration-tests/suites/tracing-new/auto-instrument/mysql/withoutConnect/scenario.ts b/packages/node-integration-tests/suites/tracing-new/auto-instrument/mysql/withoutConnect/scenario.ts index 2e13bf49b9ac..bf9e4bf90e35 100644 --- a/packages/node-integration-tests/suites/tracing-new/auto-instrument/mysql/withoutConnect/scenario.ts +++ b/packages/node-integration-tests/suites/tracing-new/auto-instrument/mysql/withoutConnect/scenario.ts @@ -22,7 +22,7 @@ Sentry.getCurrentScope().setSpan(transaction); connection.query('SELECT 1 + 1 AS solution', function () { connection.query('SELECT NOW()', ['1', '2'], () => { - if (transaction) transaction.finish(); + if (transaction) transaction.end(); connection.end(); }); }); diff --git a/packages/node-integration-tests/suites/tracing-new/auto-instrument/pg/scenario.ts b/packages/node-integration-tests/suites/tracing-new/auto-instrument/pg/scenario.ts index c10661094981..b41be87c9550 100644 --- a/packages/node-integration-tests/suites/tracing-new/auto-instrument/pg/scenario.ts +++ b/packages/node-integration-tests/suites/tracing-new/auto-instrument/pg/scenario.ts @@ -18,6 +18,6 @@ Sentry.getCurrentScope().setSpan(transaction); const client = new pg.Client(); client.query('SELECT * FROM foo where bar ilike "baz%"', ['a', 'b'], () => client.query('SELECT * FROM bazz', () => { - client.query('SELECT NOW()', () => transaction.finish()); + client.query('SELECT NOW()', () => transaction.end()); }), ); diff --git a/packages/node-integration-tests/suites/tracing-new/prisma-orm/scenario.ts b/packages/node-integration-tests/suites/tracing-new/prisma-orm/scenario.ts index ee73dc922747..20847871e7a1 100644 --- a/packages/node-integration-tests/suites/tracing-new/prisma-orm/scenario.ts +++ b/packages/node-integration-tests/suites/tracing-new/prisma-orm/scenario.ts @@ -38,7 +38,7 @@ async function run(): Promise { }, }); } finally { - if (transaction) transaction.finish(); + if (transaction) transaction.end(); } } diff --git a/packages/node-integration-tests/suites/tracing-new/tracePropagationTargets/scenario.ts b/packages/node-integration-tests/suites/tracing-new/tracePropagationTargets/scenario.ts index d1eb5fe017ed..7c86686cbba8 100644 --- a/packages/node-integration-tests/suites/tracing-new/tracePropagationTargets/scenario.ts +++ b/packages/node-integration-tests/suites/tracing-new/tracePropagationTargets/scenario.ts @@ -19,4 +19,4 @@ http.get('http://match-this-url.com/api/v1'); http.get('http://dont-match-this-url.com/api/v2'); http.get('http://dont-match-this-url.com/api/v3'); -transaction.finish(); +transaction.end(); diff --git a/packages/node-integration-tests/suites/tracing/apollo-graphql/scenario.ts b/packages/node-integration-tests/suites/tracing/apollo-graphql/scenario.ts index 0e5e0bd9edd0..7b34ffab0613 100644 --- a/packages/node-integration-tests/suites/tracing/apollo-graphql/scenario.ts +++ b/packages/node-integration-tests/suites/tracing/apollo-graphql/scenario.ts @@ -40,5 +40,5 @@ Sentry.getCurrentScope().setSpan(transaction); query: '{hello}', }); - transaction.finish(); + transaction.end(); })(); diff --git a/packages/node-integration-tests/suites/tracing/auto-instrument/mongodb/scenario.ts b/packages/node-integration-tests/suites/tracing/auto-instrument/mongodb/scenario.ts index 7979ea57483b..cff8329d22a3 100644 --- a/packages/node-integration-tests/suites/tracing/auto-instrument/mongodb/scenario.ts +++ b/packages/node-integration-tests/suites/tracing/auto-instrument/mongodb/scenario.ts @@ -37,7 +37,7 @@ async function run(): Promise { await collection.find({ title: 'South Park' }).toArray(); } finally { - if (transaction) transaction.finish(); + if (transaction) transaction.end(); await client.close(); } } diff --git a/packages/node-integration-tests/suites/tracing/auto-instrument/mysql/scenario.ts b/packages/node-integration-tests/suites/tracing/auto-instrument/mysql/scenario.ts index 2cf161c0ab78..30f9fb368b3a 100644 --- a/packages/node-integration-tests/suites/tracing/auto-instrument/mysql/scenario.ts +++ b/packages/node-integration-tests/suites/tracing/auto-instrument/mysql/scenario.ts @@ -29,7 +29,7 @@ Sentry.getCurrentScope().setSpan(transaction); connection.query('SELECT 1 + 1 AS solution', function () { connection.query('SELECT NOW()', ['1', '2'], () => { - if (transaction) transaction.finish(); + if (transaction) transaction.end(); connection.end(); }); }); diff --git a/packages/node-integration-tests/suites/tracing/auto-instrument/pg/scenario.ts b/packages/node-integration-tests/suites/tracing/auto-instrument/pg/scenario.ts index c39069909082..95248c82f075 100644 --- a/packages/node-integration-tests/suites/tracing/auto-instrument/pg/scenario.ts +++ b/packages/node-integration-tests/suites/tracing/auto-instrument/pg/scenario.ts @@ -19,6 +19,6 @@ Sentry.getCurrentScope().setSpan(transaction); const client = new pg.Client(); client.query('SELECT * FROM foo where bar ilike "baz%"', ['a', 'b'], () => client.query('SELECT * FROM bazz', () => { - client.query('SELECT NOW()', () => transaction.finish()); + client.query('SELECT NOW()', () => transaction.end()); }), ); diff --git a/packages/node-integration-tests/suites/tracing/prisma-orm/scenario.ts b/packages/node-integration-tests/suites/tracing/prisma-orm/scenario.ts index 578c5802fea0..7e8a7c6eca5f 100644 --- a/packages/node-integration-tests/suites/tracing/prisma-orm/scenario.ts +++ b/packages/node-integration-tests/suites/tracing/prisma-orm/scenario.ts @@ -40,7 +40,7 @@ async function run(): Promise { }, }); } finally { - if (transaction) transaction.finish(); + if (transaction) transaction.end(); } } diff --git a/packages/node-integration-tests/suites/tracing/tracePropagationTargets/scenario.ts b/packages/node-integration-tests/suites/tracing/tracePropagationTargets/scenario.ts index c07faeeb9a3f..9fdeba1fcb95 100644 --- a/packages/node-integration-tests/suites/tracing/tracePropagationTargets/scenario.ts +++ b/packages/node-integration-tests/suites/tracing/tracePropagationTargets/scenario.ts @@ -21,4 +21,4 @@ http.get('http://match-this-url.com/api/v1'); http.get('http://dont-match-this-url.com/api/v2'); http.get('http://dont-match-this-url.com/api/v3'); -transaction.finish(); +transaction.end(); diff --git a/packages/node/src/handlers.ts b/packages/node/src/handlers.ts index 832d87139f83..857e6c4892d5 100644 --- a/packages/node/src/handlers.ts +++ b/packages/node/src/handlers.ts @@ -98,7 +98,7 @@ export function tracingHandler(): ( setImmediate(() => { addRequestDataToTransaction(transaction, req); transaction.setHttpStatus(res.statusCode); - transaction.finish(); + transaction.end(); }); }); diff --git a/packages/node/src/integrations/hapi/index.ts b/packages/node/src/integrations/hapi/index.ts index 5e158af810ca..d63b831da4e2 100644 --- a/packages/node/src/integrations/hapi/index.ts +++ b/packages/node/src/integrations/hapi/index.ts @@ -53,7 +53,7 @@ export const hapiErrorPlugin = { if (transaction) { transaction.setStatus('internal_error'); - transaction.finish(); + transaction.end(); } }); }, @@ -114,7 +114,7 @@ export const hapiTracingPlugin = { } if (transaction) { - transaction.finish(); + transaction.end(); } return h.continue; diff --git a/packages/node/src/integrations/http.ts b/packages/node/src/integrations/http.ts index 02e79d06b942..5d6b8857f93b 100644 --- a/packages/node/src/integrations/http.ts +++ b/packages/node/src/integrations/http.ts @@ -292,7 +292,7 @@ function _createWrappedRequestMethodFactory( requestSpan.setHttpStatus(res.statusCode); } requestSpan.description = cleanSpanDescription(requestSpan.description, requestOptions, req); - requestSpan.finish(); + requestSpan.end(); } }) .once('error', function (this: http.ClientRequest): void { @@ -305,7 +305,7 @@ function _createWrappedRequestMethodFactory( if (requestSpan) { requestSpan.setHttpStatus(500); requestSpan.description = cleanSpanDescription(requestSpan.description, requestOptions, req); - requestSpan.finish(); + requestSpan.end(); } }); }; diff --git a/packages/node/src/integrations/undici/index.ts b/packages/node/src/integrations/undici/index.ts index b67562843d84..4f67993f7321 100644 --- a/packages/node/src/integrations/undici/index.ts +++ b/packages/node/src/integrations/undici/index.ts @@ -211,7 +211,7 @@ export class Undici implements Integration { const span = request.__sentry_span__; if (span) { span.setHttpStatus(response.statusCode); - span.finish(); + span.end(); } if (this._options.breadcrumbs) { @@ -251,7 +251,7 @@ export class Undici implements Integration { const span = request.__sentry_span__; if (span) { span.setStatus('internal_error'); - span.finish(); + span.end(); } if (this._options.breadcrumbs) { diff --git a/packages/node/test/handlers.test.ts b/packages/node/test/handlers.test.ts index cf6dab4d9338..14de421db5f7 100644 --- a/packages/node/test/handlers.test.ts +++ b/packages/node/test/handlers.test.ts @@ -362,7 +362,7 @@ describe('tracingHandler', () => { it('pulls status code from the response', done => { const transaction = new Transaction({ name: 'mockTransaction' }); jest.spyOn(sentryCore, 'startTransaction').mockReturnValue(transaction as Transaction); - const finishTransaction = jest.spyOn(transaction, 'finish'); + const finishTransaction = jest.spyOn(transaction, 'end'); sentryTracingMiddleware(req, res, next); res.statusCode = 200; @@ -410,7 +410,7 @@ describe('tracingHandler', () => { it('closes the transaction when request processing is done', done => { const transaction = new Transaction({ name: 'mockTransaction' }); jest.spyOn(sentryCore, 'startTransaction').mockReturnValue(transaction as Transaction); - const finishTransaction = jest.spyOn(transaction, 'finish'); + const finishTransaction = jest.spyOn(transaction, 'end'); sentryTracingMiddleware(req, res, next); res.emit('finish'); @@ -421,7 +421,7 @@ describe('tracingHandler', () => { }); }); - it('waits to finish transaction until all spans are finished, even though `transaction.finish()` is registered on `res.finish` event first', done => { + it('waits to finish transaction until all spans are finished, even though `transaction.end()` is registered on `res.finish` event first', done => { const transaction = new Transaction({ name: 'mockTransaction', sampled: true }); transaction.initSpanRecorder(); const span = transaction.startChild({ @@ -429,8 +429,8 @@ describe('tracingHandler', () => { op: 'middleware', }); jest.spyOn(sentryCore, 'startTransaction').mockReturnValue(transaction as Transaction); - const finishSpan = jest.spyOn(span, 'finish'); - const finishTransaction = jest.spyOn(transaction, 'finish'); + const finishSpan = jest.spyOn(span, 'end'); + const finishTransaction = jest.spyOn(transaction, 'end'); let sentEvent: Event; jest.spyOn((transaction as any)._hub, 'captureEvent').mockImplementation(event => { @@ -439,7 +439,7 @@ describe('tracingHandler', () => { sentryTracingMiddleware(req, res, next); res.once('finish', () => { - span.finish(); + span.end(); }); res.emit('finish'); diff --git a/packages/opentelemetry-node/src/spanprocessor.ts b/packages/opentelemetry-node/src/spanprocessor.ts index 772bd861cc2a..f220d5893e93 100644 --- a/packages/opentelemetry-node/src/spanprocessor.ts +++ b/packages/opentelemetry-node/src/spanprocessor.ts @@ -124,7 +124,7 @@ export class SentrySpanProcessor implements OtelSpanProcessor { // Ensure we do not capture any OTEL spans for finishing (and sending) this context.with(suppressTracing(context.active()), () => { - sentrySpan.finish(convertOtelTimeToSeconds(otelSpan.endTime)); + sentrySpan.end(convertOtelTimeToSeconds(otelSpan.endTime)); }); clearSpan(otelSpanId); diff --git a/packages/opentelemetry/src/spanExporter.ts b/packages/opentelemetry/src/spanExporter.ts index c15bd4483a9b..fdb63636452f 100644 --- a/packages/opentelemetry/src/spanExporter.ts +++ b/packages/opentelemetry/src/spanExporter.ts @@ -222,7 +222,7 @@ function createAndFinishSpanForOtelSpan(node: SpanNode, sentryParentSpan: Sentry createAndFinishSpanForOtelSpan(child, sentrySpan, remaining); }); - sentrySpan.finish(convertOtelTimeToSeconds(span.endTime)); + sentrySpan.end(convertOtelTimeToSeconds(span.endTime)); } function getSpanData(span: ReadableSpan): { diff --git a/packages/react/src/profiler.tsx b/packages/react/src/profiler.tsx index 9647d34f0fb4..11ae1e83dabf 100644 --- a/packages/react/src/profiler.tsx +++ b/packages/react/src/profiler.tsx @@ -70,7 +70,7 @@ class Profiler extends React.Component { // If a component mounted, we can finish the mount activity. public componentDidMount(): void { if (this._mountSpan) { - this._mountSpan.finish(); + this._mountSpan.end(); } } @@ -101,7 +101,7 @@ class Profiler extends React.Component { public componentDidUpdate(): void { if (this._updateSpan) { - this._updateSpan.finish(); + this._updateSpan.end(); this._updateSpan = undefined; } } @@ -192,7 +192,7 @@ function useProfiler( React.useEffect(() => { if (mountSpan) { - mountSpan.finish(); + mountSpan.end(); } return (): void => { diff --git a/packages/react/src/reactrouter.tsx b/packages/react/src/reactrouter.tsx index 9e7077c59898..3234cdc7871d 100644 --- a/packages/react/src/reactrouter.tsx +++ b/packages/react/src/reactrouter.tsx @@ -105,7 +105,7 @@ function createReactRouterInstrumentation( history.listen((location, action) => { if (action && (action === 'PUSH' || action === 'POP')) { if (activeTransaction) { - activeTransaction.finish(); + activeTransaction.end(); } const [name, source] = normalizeTransactionName(location.pathname); diff --git a/packages/react/src/reactrouterv3.ts b/packages/react/src/reactrouterv3.ts index f185a025a649..db1ce1320508 100644 --- a/packages/react/src/reactrouterv3.ts +++ b/packages/react/src/reactrouterv3.ts @@ -69,7 +69,7 @@ export function reactRouterV3Instrumentation( history.listen(location => { if (location.action === 'PUSH' || location.action === 'POP') { if (activeTransaction) { - activeTransaction.finish(); + activeTransaction.end(); } const tags: Record = { 'routing.instrumentation': 'react-router-v3', diff --git a/packages/react/src/reactrouterv6.tsx b/packages/react/src/reactrouterv6.tsx index 98cac5294ead..14c975a90105 100644 --- a/packages/react/src/reactrouterv6.tsx +++ b/packages/react/src/reactrouterv6.tsx @@ -149,7 +149,7 @@ function handleNavigation( if (_startTransactionOnLocationChange && (navigationType === 'PUSH' || navigationType === 'POP') && branches) { if (activeTransaction) { - activeTransaction.finish(); + activeTransaction.end(); } const [name, source] = getNormalizedName(routes, location, branches, basename); diff --git a/packages/react/test/profiler.test.tsx b/packages/react/test/profiler.test.tsx index 70eaff2d2c8b..847d2e4603ed 100644 --- a/packages/react/test/profiler.test.tsx +++ b/packages/react/test/profiler.test.tsx @@ -18,7 +18,7 @@ class MockSpan { return new MockSpan(ctx); } - public finish(): void { + public end(): void { mockFinish(); } } diff --git a/packages/react/test/reactrouterv3.test.tsx b/packages/react/test/reactrouterv3.test.tsx index dad1b4793d26..21b9054e45ce 100644 --- a/packages/react/test/reactrouterv3.test.tsx +++ b/packages/react/test/reactrouterv3.test.tsx @@ -19,7 +19,7 @@ declare module 'react-router-3' { function createMockStartTransaction(opts: { finish?: jest.FunctionLike; setMetadata?: jest.FunctionLike } = {}) { const { finish = jest.fn(), setMetadata = jest.fn() } = opts; return jest.fn().mockReturnValue({ - finish, + end: finish, setMetadata, }); } diff --git a/packages/react/test/reactrouterv4.test.tsx b/packages/react/test/reactrouterv4.test.tsx index e1c8a71e66b0..1d3d4df4f170 100644 --- a/packages/react/test/reactrouterv4.test.tsx +++ b/packages/react/test/reactrouterv4.test.tsx @@ -22,7 +22,7 @@ describe('React Router v4', () => { const history = createMemoryHistory(); const mockFinish = jest.fn(); const mockSetName = jest.fn(); - const mockStartTransaction = jest.fn().mockReturnValue({ setName: mockSetName, finish: mockFinish }); + const mockStartTransaction = jest.fn().mockReturnValue({ setName: mockSetName, end: mockFinish }); reactRouterV4Instrumentation(history, options.routes, options.matchPath)( mockStartTransaction, options.startTransactionOnPageLoad, diff --git a/packages/react/test/reactrouterv5.test.tsx b/packages/react/test/reactrouterv5.test.tsx index 3ab94aea0401..e61306bc8b40 100644 --- a/packages/react/test/reactrouterv5.test.tsx +++ b/packages/react/test/reactrouterv5.test.tsx @@ -22,7 +22,7 @@ describe('React Router v5', () => { const history = createMemoryHistory(); const mockFinish = jest.fn(); const mockSetName = jest.fn(); - const mockStartTransaction = jest.fn().mockReturnValue({ setName: mockSetName, finish: mockFinish }); + const mockStartTransaction = jest.fn().mockReturnValue({ setName: mockSetName, end: mockFinish }); reactRouterV5Instrumentation(history, options.routes, options.matchPath)( mockStartTransaction, options.startTransactionOnPageLoad, diff --git a/packages/react/test/reactrouterv6.4.test.tsx b/packages/react/test/reactrouterv6.4.test.tsx index f97b9a82253a..5c2c9e7b3d5b 100644 --- a/packages/react/test/reactrouterv6.4.test.tsx +++ b/packages/react/test/reactrouterv6.4.test.tsx @@ -34,7 +34,7 @@ describe('React Router v6.4', () => { }; const mockFinish = jest.fn(); const mockSetName = jest.fn(); - const mockStartTransaction = jest.fn().mockReturnValue({ setName: mockSetName, finish: mockFinish }); + const mockStartTransaction = jest.fn().mockReturnValue({ setName: mockSetName, end: mockFinish }); reactRouterV6Instrumentation( React.useEffect, diff --git a/packages/react/test/reactrouterv6.test.tsx b/packages/react/test/reactrouterv6.test.tsx index 86f6ae9b66e0..fd6ad444125e 100644 --- a/packages/react/test/reactrouterv6.test.tsx +++ b/packages/react/test/reactrouterv6.test.tsx @@ -30,7 +30,7 @@ describe('React Router v6', () => { }; const mockFinish = jest.fn(); const mockSetName = jest.fn(); - const mockStartTransaction = jest.fn().mockReturnValue({ setName: mockSetName, finish: mockFinish }); + const mockStartTransaction = jest.fn().mockReturnValue({ setName: mockSetName, end: mockFinish }); reactRouterV6Instrumentation( React.useEffect, diff --git a/packages/remix/src/client/performance.tsx b/packages/remix/src/client/performance.tsx index 2e597d10fa2b..af6344165b6a 100644 --- a/packages/remix/src/client/performance.tsx +++ b/packages/remix/src/client/performance.tsx @@ -134,7 +134,7 @@ export function withSentry

, R extends React.Co _useEffect(() => { if (isBaseLocation) { if (activeTransaction) { - activeTransaction.finish(); + activeTransaction.end(); } return; @@ -142,7 +142,7 @@ export function withSentry

, R extends React.Co if (_startTransactionOnLocationChange && matches && matches.length) { if (activeTransaction) { - activeTransaction.finish(); + activeTransaction.end(); } activeTransaction = _customStartTransaction({ diff --git a/packages/remix/src/utils/instrumentServer.ts b/packages/remix/src/utils/instrumentServer.ts index 36f41968edb5..90f72296d2a1 100644 --- a/packages/remix/src/utils/instrumentServer.ts +++ b/packages/remix/src/utils/instrumentServer.ts @@ -198,7 +198,7 @@ function makeWrappedDocumentRequestFunction(remixVersion?: number) { loadContext, ); - span?.finish(); + span?.end(); } catch (err) { const isRemixV1 = !FUTURE_FLAGS?.v2_errorBoundary && remixVersion !== 2; @@ -246,7 +246,7 @@ function makeWrappedDataFunction( res = await origFn.call(this, args); currentScope.setSpan(activeTransaction); - span?.finish(); + span?.end(); } catch (err) { const isRemixV2 = FUTURE_FLAGS?.v2_errorBoundary || remixVersion === 2; @@ -463,7 +463,7 @@ function wrapRequestHandler(origRequestHandler: RequestHandler, build: ServerBui transaction.setHttpStatus(res.status); } - transaction.finish(); + transaction.end(); return res; }); diff --git a/packages/remix/src/utils/serverAdapters/express.ts b/packages/remix/src/utils/serverAdapters/express.ts index b60b74a8e0ff..ea765d35a353 100644 --- a/packages/remix/src/utils/serverAdapters/express.ts +++ b/packages/remix/src/utils/serverAdapters/express.ts @@ -143,7 +143,7 @@ async function finishSentryProcessing(res: AugmentedExpressResponse): Promise(resolve => { setImmediate(() => { - transaction.finish(); + transaction.end(); resolve(); }); }); diff --git a/packages/remix/test/integration/common/routes/manual-tracing.$id.tsx b/packages/remix/test/integration/common/routes/manual-tracing.$id.tsx index 2f925881b9cf..a7383f1b8f5a 100644 --- a/packages/remix/test/integration/common/routes/manual-tracing.$id.tsx +++ b/packages/remix/test/integration/common/routes/manual-tracing.$id.tsx @@ -2,6 +2,6 @@ import * as Sentry from '@sentry/remix'; export default function ManualTracing() { const transaction = Sentry.startTransaction({ name: 'test_transaction_1' }); - transaction.finish(); + transaction.end(); return

; } diff --git a/packages/serverless/src/awslambda.ts b/packages/serverless/src/awslambda.ts index 3f6ed2ad0ef8..9094709082dc 100644 --- a/packages/serverless/src/awslambda.ts +++ b/packages/serverless/src/awslambda.ts @@ -342,7 +342,7 @@ export function wrapHandler( throw e; } finally { clearTimeout(timeoutWarningTimer); - transaction?.finish(); + transaction?.end(); await flush(options.flushTimeout).catch(e => { DEBUG_BUILD && logger.error(e); }); diff --git a/packages/serverless/src/awsservices.ts b/packages/serverless/src/awsservices.ts index 33a3a25b8689..e7404b0695c4 100644 --- a/packages/serverless/src/awsservices.ts +++ b/packages/serverless/src/awsservices.ts @@ -71,7 +71,7 @@ function wrapMakeRequest( }); req.on('complete', () => { if (span) { - span.finish(); + span.end(); } }); diff --git a/packages/serverless/src/gcpfunction/cloud_events.ts b/packages/serverless/src/gcpfunction/cloud_events.ts index 0e89216a2fd7..547cf93b3a5f 100644 --- a/packages/serverless/src/gcpfunction/cloud_events.ts +++ b/packages/serverless/src/gcpfunction/cloud_events.ts @@ -52,7 +52,7 @@ function _wrapCloudEventFunction( if (args[0] !== null && args[0] !== undefined) { captureException(args[0], scope => markEventUnhandled(scope)); } - transaction?.finish(); + transaction?.end(); // eslint-disable-next-line @typescript-eslint/no-floating-promises flush(options.flushTimeout) diff --git a/packages/serverless/src/gcpfunction/events.ts b/packages/serverless/src/gcpfunction/events.ts index b69335b272d7..e3f6b30a4525 100644 --- a/packages/serverless/src/gcpfunction/events.ts +++ b/packages/serverless/src/gcpfunction/events.ts @@ -54,7 +54,7 @@ function _wrapEventFunction if (args[0] !== null && args[0] !== undefined) { captureException(args[0], scope => markEventUnhandled(scope)); } - transaction?.finish(); + transaction?.end(); // eslint-disable-next-line @typescript-eslint/no-floating-promises flush(options.flushTimeout) diff --git a/packages/serverless/src/gcpfunction/http.ts b/packages/serverless/src/gcpfunction/http.ts index 726f56c91a52..50bbcd76e782 100644 --- a/packages/serverless/src/gcpfunction/http.ts +++ b/packages/serverless/src/gcpfunction/http.ts @@ -108,7 +108,7 @@ function _wrapHttpFunction(fn: HttpFunction, wrapOptions: Partial void), encoding?: string | (() => void), cb?: () => void): any { transaction?.setHttpStatus(res.statusCode); - transaction?.finish(); + transaction?.end(); // eslint-disable-next-line @typescript-eslint/no-floating-promises flush(options.flushTimeout) diff --git a/packages/serverless/src/google-cloud-grpc.ts b/packages/serverless/src/google-cloud-grpc.ts index d475d9b3b421..9f2ea37203b4 100644 --- a/packages/serverless/src/google-cloud-grpc.ts +++ b/packages/serverless/src/google-cloud-grpc.ts @@ -119,7 +119,7 @@ function fillGrpcFunction(stub: Stub, serviceIdentifier: string, methodName: str } ret.on('status', () => { if (span) { - span.finish(); + span.end(); } }); return ret; diff --git a/packages/serverless/src/google-cloud-http.ts b/packages/serverless/src/google-cloud-http.ts index f9eb9a6cc3cd..6b522facf5ae 100644 --- a/packages/serverless/src/google-cloud-http.ts +++ b/packages/serverless/src/google-cloud-http.ts @@ -64,7 +64,7 @@ function wrapRequestFunction(orig: RequestFunction): RequestFunction { } orig.call(this, reqOpts, (...args: Parameters) => { if (span) { - span.finish(); + span.end(); } callback(...args); }); diff --git a/packages/serverless/test/__mocks__/@sentry/node.ts b/packages/serverless/test/__mocks__/@sentry/node.ts index f9322057f1d5..0dbf38c3d483 100644 --- a/packages/serverless/test/__mocks__/@sentry/node.ts +++ b/packages/serverless/test/__mocks__/@sentry/node.ts @@ -25,10 +25,10 @@ export const fakeScope = { setPropagationContext: jest.fn(), }; export const fakeSpan = { - finish: jest.fn(), + end: jest.fn(), }; export const fakeTransaction = { - finish: jest.fn(), + end: jest.fn(), setHttpStatus: jest.fn(), startChild: jest.fn(() => fakeSpan), }; @@ -45,9 +45,9 @@ export const getClient = jest.fn(() => ({})); export const resetMocks = (): void => { fakeTransaction.setHttpStatus.mockClear(); - fakeTransaction.finish.mockClear(); + fakeTransaction.end.mockClear(); fakeTransaction.startChild.mockClear(); - fakeSpan.finish.mockClear(); + fakeSpan.end.mockClear(); fakeHub.configureScope.mockClear(); fakeHub.pushScope.mockClear(); fakeHub.popScope.mockClear(); diff --git a/packages/serverless/test/awslambda.test.ts b/packages/serverless/test/awslambda.test.ts index 5c67c8481d4a..49f41e67f6fd 100644 --- a/packages/serverless/test/awslambda.test.ts +++ b/packages/serverless/test/awslambda.test.ts @@ -219,7 +219,7 @@ describe('AWSLambda', () => { expect(SentryNode.fakeHub.startTransaction).toBeCalledWith(fakeTransactionContext); expectScopeSettings(fakeTransactionContext); // @ts-expect-error see "Why @ts-expect-error" note - expect(SentryNode.fakeTransaction.finish).toBeCalled(); + expect(SentryNode.fakeTransaction.end).toBeCalled(); expect(SentryNode.flush).toBeCalledWith(2000); }); @@ -247,7 +247,7 @@ describe('AWSLambda', () => { expectScopeSettings(fakeTransactionContext); expect(SentryNode.captureException).toBeCalledWith(error, expect.any(Function)); // @ts-expect-error see "Why @ts-expect-error" note - expect(SentryNode.fakeTransaction.finish).toBeCalled(); + expect(SentryNode.fakeTransaction.end).toBeCalled(); expect(SentryNode.flush).toBeCalledWith(2000); } }); @@ -326,7 +326,7 @@ describe('AWSLambda', () => { expectScopeSettings(fakeTransactionContext); expect(SentryNode.captureException).toBeCalledWith(e, expect.any(Function)); // @ts-expect-error see "Why @ts-expect-error" note - expect(SentryNode.fakeTransaction.finish).toBeCalled(); + expect(SentryNode.fakeTransaction.end).toBeCalled(); expect(SentryNode.flush).toBeCalled(); } }); @@ -354,7 +354,7 @@ describe('AWSLambda', () => { expect(SentryNode.fakeHub.startTransaction).toBeCalledWith(fakeTransactionContext); expectScopeSettings(fakeTransactionContext); // @ts-expect-error see "Why @ts-expect-error" note - expect(SentryNode.fakeTransaction.finish).toBeCalled(); + expect(SentryNode.fakeTransaction.end).toBeCalled(); expect(SentryNode.flush).toBeCalled(); }); @@ -393,7 +393,7 @@ describe('AWSLambda', () => { expectScopeSettings(fakeTransactionContext); expect(SentryNode.captureException).toBeCalledWith(error, expect.any(Function)); // @ts-expect-error see "Why @ts-expect-error" note - expect(SentryNode.fakeTransaction.finish).toBeCalled(); + expect(SentryNode.fakeTransaction.end).toBeCalled(); expect(SentryNode.flush).toBeCalled(); } }); @@ -436,7 +436,7 @@ describe('AWSLambda', () => { expect(SentryNode.fakeHub.startTransaction).toBeCalledWith(fakeTransactionContext); expectScopeSettings(fakeTransactionContext); // @ts-expect-error see "Why @ts-expect-error" note - expect(SentryNode.fakeTransaction.finish).toBeCalled(); + expect(SentryNode.fakeTransaction.end).toBeCalled(); expect(SentryNode.flush).toBeCalled(); }); @@ -475,7 +475,7 @@ describe('AWSLambda', () => { expectScopeSettings(fakeTransactionContext); expect(SentryNode.captureException).toBeCalledWith(error, expect.any(Function)); // @ts-expect-error see "Why @ts-expect-error" note - expect(SentryNode.fakeTransaction.finish).toBeCalled(); + expect(SentryNode.fakeTransaction.end).toBeCalled(); expect(SentryNode.flush).toBeCalled(); } }); diff --git a/packages/serverless/test/awsservices.test.ts b/packages/serverless/test/awsservices.test.ts index cca793549c99..e4abda7c9364 100644 --- a/packages/serverless/test/awsservices.test.ts +++ b/packages/serverless/test/awsservices.test.ts @@ -37,7 +37,7 @@ describe('AWSServices', () => { description: 'aws.s3.getObject foo', }); // @ts-expect-error see "Why @ts-expect-error" note - expect(SentryNode.fakeSpan.finish).toBeCalled(); + expect(SentryNode.fakeSpan.end).toBeCalled(); }); test('getObject with callback', done => { diff --git a/packages/serverless/test/gcpfunction.test.ts b/packages/serverless/test/gcpfunction.test.ts index f60c26c00986..4f442c543678 100644 --- a/packages/serverless/test/gcpfunction.test.ts +++ b/packages/serverless/test/gcpfunction.test.ts @@ -127,7 +127,7 @@ describe('GCPFunction', () => { // @ts-expect-error see "Why @ts-expect-error" note expect(SentryNode.fakeTransaction.setHttpStatus).toBeCalledWith(200); // @ts-expect-error see "Why @ts-expect-error" note - expect(SentryNode.fakeTransaction.finish).toBeCalled(); + expect(SentryNode.fakeTransaction.end).toBeCalled(); expect(SentryNode.flush).toBeCalledWith(2000); }); @@ -197,7 +197,7 @@ describe('GCPFunction', () => { expect(SentryNode.fakeScope.setSpan).toBeCalledWith(fakeTransaction); expect(SentryNode.captureException).toBeCalledWith(error, expect.any(Function)); // @ts-expect-error see "Why @ts-expect-error" note - expect(SentryNode.fakeTransaction.finish).toBeCalled(); + expect(SentryNode.fakeTransaction.end).toBeCalled(); expect(SentryNode.flush).toBeCalled(); }); @@ -284,7 +284,7 @@ describe('GCPFunction', () => { // @ts-expect-error see "Why @ts-expect-error" note expect(SentryNode.fakeScope.setSpan).toBeCalledWith(fakeTransaction); // @ts-expect-error see "Why @ts-expect-error" note - expect(SentryNode.fakeTransaction.finish).toBeCalled(); + expect(SentryNode.fakeTransaction.end).toBeCalled(); expect(SentryNode.flush).toBeCalledWith(2000); }); @@ -313,7 +313,7 @@ describe('GCPFunction', () => { expect(SentryNode.fakeScope.setSpan).toBeCalledWith(fakeTransaction); expect(SentryNode.captureException).toBeCalledWith(error, expect.any(Function)); // @ts-expect-error see "Why @ts-expect-error" note - expect(SentryNode.fakeTransaction.finish).toBeCalled(); + expect(SentryNode.fakeTransaction.end).toBeCalled(); expect(SentryNode.flush).toBeCalled(); }); }); @@ -345,7 +345,7 @@ describe('GCPFunction', () => { // @ts-expect-error see "Why @ts-expect-error" note expect(SentryNode.fakeScope.setSpan).toBeCalledWith(fakeTransaction); // @ts-expect-error see "Why @ts-expect-error" note - expect(SentryNode.fakeTransaction.finish).toBeCalled(); + expect(SentryNode.fakeTransaction.end).toBeCalled(); expect(SentryNode.flush).toBeCalledWith(2000); }); @@ -378,7 +378,7 @@ describe('GCPFunction', () => { expect(SentryNode.fakeScope.setSpan).toBeCalledWith(fakeTransaction); expect(SentryNode.captureException).toBeCalledWith(error, expect.any(Function)); // @ts-expect-error see "Why @ts-expect-error" note - expect(SentryNode.fakeTransaction.finish).toBeCalled(); + expect(SentryNode.fakeTransaction.end).toBeCalled(); expect(SentryNode.flush).toBeCalled(); }); }); @@ -407,7 +407,7 @@ describe('GCPFunction', () => { // @ts-expect-error see "Why @ts-expect-error" note expect(SentryNode.fakeScope.setSpan).toBeCalledWith(fakeTransaction); // @ts-expect-error see "Why @ts-expect-error" note - expect(SentryNode.fakeTransaction.finish).toBeCalled(); + expect(SentryNode.fakeTransaction.end).toBeCalled(); expect(SentryNode.flush).toBeCalledWith(2000); }); @@ -436,7 +436,7 @@ describe('GCPFunction', () => { expect(SentryNode.fakeScope.setSpan).toBeCalledWith(fakeTransaction); expect(SentryNode.captureException).toBeCalledWith(error, expect.any(Function)); // @ts-expect-error see "Why @ts-expect-error" note - expect(SentryNode.fakeTransaction.finish).toBeCalled(); + expect(SentryNode.fakeTransaction.end).toBeCalled(); expect(SentryNode.flush).toBeCalled(); }); @@ -530,7 +530,7 @@ describe('GCPFunction', () => { // @ts-expect-error see "Why @ts-expect-error" note expect(SentryNode.fakeScope.setSpan).toBeCalledWith(fakeTransaction); // @ts-expect-error see "Why @ts-expect-error" note - expect(SentryNode.fakeTransaction.finish).toBeCalled(); + expect(SentryNode.fakeTransaction.end).toBeCalled(); expect(SentryNode.flush).toBeCalledWith(2000); }); @@ -559,7 +559,7 @@ describe('GCPFunction', () => { expect(SentryNode.fakeScope.setSpan).toBeCalledWith(fakeTransaction); expect(SentryNode.captureException).toBeCalledWith(error, expect.any(Function)); // @ts-expect-error see "Why @ts-expect-error" note - expect(SentryNode.fakeTransaction.finish).toBeCalled(); + expect(SentryNode.fakeTransaction.end).toBeCalled(); expect(SentryNode.flush).toBeCalled(); }); }); @@ -588,7 +588,7 @@ describe('GCPFunction', () => { // @ts-expect-error see "Why @ts-expect-error" note expect(SentryNode.fakeScope.setSpan).toBeCalledWith(fakeTransaction); // @ts-expect-error see "Why @ts-expect-error" note - expect(SentryNode.fakeTransaction.finish).toBeCalled(); + expect(SentryNode.fakeTransaction.end).toBeCalled(); expect(SentryNode.flush).toBeCalledWith(2000); }); @@ -617,7 +617,7 @@ describe('GCPFunction', () => { expect(SentryNode.fakeScope.setSpan).toBeCalledWith(fakeTransaction); expect(SentryNode.captureException).toBeCalledWith(error, expect.any(Function)); // @ts-expect-error see "Why @ts-expect-error" note - expect(SentryNode.fakeTransaction.finish).toBeCalled(); + expect(SentryNode.fakeTransaction.end).toBeCalled(); expect(SentryNode.flush).toBeCalled(); }); diff --git a/packages/svelte/src/performance.ts b/packages/svelte/src/performance.ts index 0afd5250a06f..e579b453f033 100644 --- a/packages/svelte/src/performance.ts +++ b/packages/svelte/src/performance.ts @@ -54,7 +54,7 @@ function recordInitSpan(transaction: Transaction, componentName: string): Span { }); onMount(() => { - initSpan.finish(); + initSpan.end(); }); return initSpan; @@ -86,7 +86,7 @@ function recordUpdateSpans(componentName: string, initSpan?: Span): void { if (!updateSpan) { return; } - updateSpan.finish(); + updateSpan.end(); updateSpan = undefined; }); } diff --git a/packages/svelte/test/performance.test.ts b/packages/svelte/test/performance.test.ts index e872ee7a283d..bde6b7a734db 100644 --- a/packages/svelte/test/performance.test.ts +++ b/packages/svelte/test/performance.test.ts @@ -6,15 +6,15 @@ import DummyComponent from './components/Dummy.svelte'; let returnUndefinedTransaction = false; -const testTransaction: { spans: any[]; startChild: jest.Mock; finish: jest.Mock } = { +const testTransaction: { spans: any[]; startChild: jest.Mock; end: jest.Mock } = { spans: [], startChild: jest.fn(), - finish: jest.fn(), + end: jest.fn(), }; -const testUpdateSpan = { finish: jest.fn() }; +const testUpdateSpan = { end: jest.fn() }; const testInitSpan: any = { transaction: testTransaction, - finish: jest.fn(), + end: jest.fn(), startChild: jest.fn(), }; @@ -47,7 +47,7 @@ describe('Sentry.trackComponent()', () => { return testUpdateSpan; }); - testInitSpan.finish = jest.fn(); + testInitSpan.end = jest.fn(); testInitSpan.endTimestamp = undefined; returnUndefinedTransaction = false; }); @@ -67,14 +67,14 @@ describe('Sentry.trackComponent()', () => { origin: 'auto.ui.svelte', }); - expect(testInitSpan.finish).toHaveBeenCalledTimes(1); - expect(testUpdateSpan.finish).toHaveBeenCalledTimes(1); + expect(testInitSpan.end).toHaveBeenCalledTimes(1); + expect(testUpdateSpan.end).toHaveBeenCalledTimes(1); expect(testTransaction.spans.length).toEqual(2); }); it('creates an update span, when the component is updated', async () => { - // Make the finish() function actually end the initSpan - testInitSpan.finish.mockImplementation(() => { + // Make the end() function actually end the initSpan + testInitSpan.end.mockImplementation(() => { testInitSpan.endTimestamp = Date.now(); }); @@ -107,7 +107,7 @@ describe('Sentry.trackComponent()', () => { expect(testInitSpan.startChild).not.toHaveBeenCalled(); - expect(testInitSpan.finish).toHaveBeenCalledTimes(1); + expect(testInitSpan.end).toHaveBeenCalledTimes(1); expect(testTransaction.spans.length).toEqual(1); }); @@ -122,7 +122,7 @@ describe('Sentry.trackComponent()', () => { expect(testInitSpan.startChild).not.toHaveBeenCalled(); - expect(testInitSpan.finish).toHaveBeenCalledTimes(1); + expect(testInitSpan.end).toHaveBeenCalledTimes(1); expect(testTransaction.spans.length).toEqual(1); }); @@ -151,8 +151,8 @@ describe('Sentry.trackComponent()', () => { origin: 'auto.ui.svelte', }); - expect(testInitSpan.finish).toHaveBeenCalledTimes(1); - expect(testUpdateSpan.finish).toHaveBeenCalledTimes(1); + expect(testInitSpan.end).toHaveBeenCalledTimes(1); + expect(testUpdateSpan.end).toHaveBeenCalledTimes(1); expect(testTransaction.spans.length).toEqual(2); }); @@ -163,14 +163,14 @@ describe('Sentry.trackComponent()', () => { props: { options: { componentName: 'CustomComponentName' } }, }); - expect(testInitSpan.finish).toHaveBeenCalledTimes(0); - expect(testUpdateSpan.finish).toHaveBeenCalledTimes(0); + expect(testInitSpan.end).toHaveBeenCalledTimes(0); + expect(testUpdateSpan.end).toHaveBeenCalledTimes(0); expect(testTransaction.spans.length).toEqual(0); }); it("doesn't record update spans, if there's no ongoing transaction at that time", async () => { - // Make the finish() function actually end the initSpan - testInitSpan.finish.mockImplementation(() => { + // Make the end() function actually end the initSpan + testInitSpan.end.mockImplementation(() => { testInitSpan.endTimestamp = Date.now(); }); diff --git a/packages/sveltekit/src/client/router.ts b/packages/sveltekit/src/client/router.ts index 1883e5587c08..ede1ace4ae75 100644 --- a/packages/sveltekit/src/client/router.ts +++ b/packages/sveltekit/src/client/router.ts @@ -74,7 +74,7 @@ function instrumentNavigations(startTransactionFn: (context: TransactionContext) // So in this case, we can finish the routing span. If the transaction was an IdleTransaction, // it will finish automatically and if it was user-created users also need to finish it. if (routingSpan) { - routingSpan.finish(); + routingSpan.end(); routingSpan = undefined; } return; @@ -114,7 +114,7 @@ function instrumentNavigations(startTransactionFn: (context: TransactionContext) if (activeTransaction) { if (routingSpan) { // If a routing span is still open from a previous navigation, we finish it. - routingSpan.finish(); + routingSpan.end(); } routingSpan = activeTransaction.startChild({ op: 'ui.sveltekit.routing', diff --git a/packages/sveltekit/test/client/router.test.ts b/packages/sveltekit/test/client/router.test.ts index 37ebebd8d837..10e8a0aa0744 100644 --- a/packages/sveltekit/test/client/router.test.ts +++ b/packages/sveltekit/test/client/router.test.ts @@ -36,10 +36,10 @@ describe('sveltekitRoutingInstrumentation', () => { }); const mockedRoutingSpan = { - finish: () => {}, + end: () => {}, }; - const routingSpanFinishSpy = vi.spyOn(mockedRoutingSpan, 'finish'); + const routingSpanFinishSpy = vi.spyOn(mockedRoutingSpan, 'end'); beforeEach(() => { navigatingStore = writable(); diff --git a/packages/tracing-internal/src/browser/backgroundtab.ts b/packages/tracing-internal/src/browser/backgroundtab.ts index 7d65941c8d43..e13b997b16db 100644 --- a/packages/tracing-internal/src/browser/backgroundtab.ts +++ b/packages/tracing-internal/src/browser/backgroundtab.ts @@ -26,7 +26,7 @@ export function registerBackgroundTabDetection(): void { activeTransaction.setStatus(statusType); } activeTransaction.setTag('visibilitychange', 'document.hidden'); - activeTransaction.finish(); + activeTransaction.end(); } }); } else { diff --git a/packages/tracing-internal/src/browser/browsertracing.ts b/packages/tracing-internal/src/browser/browsertracing.ts index 93dc8716d71f..3b99dd9603d6 100644 --- a/packages/tracing-internal/src/browser/browsertracing.ts +++ b/packages/tracing-internal/src/browser/browsertracing.ts @@ -394,7 +394,7 @@ export class BrowserTracing implements Integration { if (inflightInteractionTransaction) { inflightInteractionTransaction.setFinishReason('interactionInterrupted'); - inflightInteractionTransaction.finish(); + inflightInteractionTransaction.end(); inflightInteractionTransaction = undefined; } diff --git a/packages/tracing-internal/src/browser/request.ts b/packages/tracing-internal/src/browser/request.ts index ab2f73b127f0..cc77ca4889f9 100644 --- a/packages/tracing-internal/src/browser/request.ts +++ b/packages/tracing-internal/src/browser/request.ts @@ -256,7 +256,7 @@ export function xhrCallback( const span = spans[spanId]; if (span && sentryXhrData.status_code !== undefined) { span.setHttpStatus(sentryXhrData.status_code); - span.finish(); + span.end(); // eslint-disable-next-line @typescript-eslint/no-dynamic-delete delete spans[spanId]; diff --git a/packages/tracing-internal/src/browser/router.ts b/packages/tracing-internal/src/browser/router.ts index 472c2e9fec05..27ad22ad90cc 100644 --- a/packages/tracing-internal/src/browser/router.ts +++ b/packages/tracing-internal/src/browser/router.ts @@ -52,7 +52,7 @@ export function instrumentRoutingWithDefaults( if (activeTransaction) { DEBUG_BUILD && logger.log(`[Tracing] Finishing current transaction with op: ${activeTransaction.op}`); // If there's an open transaction on the scope, we need to finish it before creating an new one. - activeTransaction.finish(); + activeTransaction.end(); } activeTransaction = customStartTransaction({ name: WINDOW.location.pathname, diff --git a/packages/tracing-internal/src/common/fetch.ts b/packages/tracing-internal/src/common/fetch.ts index 63f7f8ede721..2150518ea570 100644 --- a/packages/tracing-internal/src/common/fetch.ts +++ b/packages/tracing-internal/src/common/fetch.ts @@ -57,7 +57,7 @@ export function instrumentFetchRequest( } else if (handlerData.error) { span.setStatus('internal_error'); } - span.finish(); + span.end(); // eslint-disable-next-line @typescript-eslint/no-dynamic-delete delete spans[spanId]; diff --git a/packages/tracing-internal/src/node/integrations/apollo.ts b/packages/tracing-internal/src/node/integrations/apollo.ts index 033e0648d624..f46de137680a 100644 --- a/packages/tracing-internal/src/node/integrations/apollo.ts +++ b/packages/tracing-internal/src/node/integrations/apollo.ts @@ -200,12 +200,12 @@ function wrapResolver( if (isThenable(rv)) { return rv.then((res: unknown) => { - span?.finish(); + span?.end(); return res; }); } - span?.finish(); + span?.end(); return rv; }; diff --git a/packages/tracing-internal/src/node/integrations/express.ts b/packages/tracing-internal/src/node/integrations/express.ts index 0cbbf5d6af07..ab6451f1ec1c 100644 --- a/packages/tracing-internal/src/node/integrations/express.ts +++ b/packages/tracing-internal/src/node/integrations/express.ts @@ -163,7 +163,7 @@ function wrap(fn: Function, method: Method): (...args: any[]) => void { origin: 'auto.middleware.express', }); res.once('finish', () => { - span.finish(); + span.end(); }); } return fn.call(this, req, res); @@ -183,7 +183,7 @@ function wrap(fn: Function, method: Method): (...args: any[]) => void { origin: 'auto.middleware.express', }); fn.call(this, req, res, function (this: NodeJS.Global, ...args: unknown[]): void { - span?.finish(); + span?.end(); next.call(this, ...args); }); }; @@ -203,7 +203,7 @@ function wrap(fn: Function, method: Method): (...args: any[]) => void { origin: 'auto.middleware.express', }); fn.call(this, err, req, res, function (this: NodeJS.Global, ...args: unknown[]): void { - span?.finish(); + span?.end(); next.call(this, ...args); }); }; diff --git a/packages/tracing-internal/src/node/integrations/graphql.ts b/packages/tracing-internal/src/node/integrations/graphql.ts index 1fafae134ad7..16773daf49b6 100644 --- a/packages/tracing-internal/src/node/integrations/graphql.ts +++ b/packages/tracing-internal/src/node/integrations/graphql.ts @@ -66,14 +66,14 @@ export class GraphQL implements LazyLoadedIntegration { if (isThenable(rv)) { return rv.then((res: unknown) => { - span?.finish(); + span?.end(); scope?.setSpan(parentSpan); return res; }); } - span?.finish(); + span?.end(); scope?.setSpan(parentSpan); return rv; }; diff --git a/packages/tracing-internal/src/node/integrations/mongo.ts b/packages/tracing-internal/src/node/integrations/mongo.ts index fe6bc971d72e..966231db2f74 100644 --- a/packages/tracing-internal/src/node/integrations/mongo.ts +++ b/packages/tracing-internal/src/node/integrations/mongo.ts @@ -185,7 +185,7 @@ export class Mongo implements LazyLoadedIntegration { if (isThenable(maybePromiseOrCursor)) { return maybePromiseOrCursor.then((res: unknown) => { - span?.finish(); + span?.end(); return res; }); } @@ -196,17 +196,17 @@ export class Mongo implements LazyLoadedIntegration { try { cursor.once('close', () => { - span?.finish(); + span?.end(); }); } catch (e) { // If the cursor is already closed, `once` will throw an error. In that case, we can // finish the span immediately. - span?.finish(); + span?.end(); } return cursor; } else { - span?.finish(); + span?.end(); return maybePromiseOrCursor; } } @@ -214,7 +214,7 @@ export class Mongo implements LazyLoadedIntegration { const span = parentSpan?.startChild(getSpanContext(this, operation, args.slice(0, -1))); return orig.call(this, ...args.slice(0, -1), function (err: Error, result: unknown) { - span?.finish(); + span?.end(); lastArg(err, result); }); }; diff --git a/packages/tracing-internal/src/node/integrations/mysql.ts b/packages/tracing-internal/src/node/integrations/mysql.ts index 18038982c03d..c85b0021d89a 100644 --- a/packages/tracing-internal/src/node/integrations/mysql.ts +++ b/packages/tracing-internal/src/node/integrations/mysql.ts @@ -94,7 +94,7 @@ export class Mysql implements LazyLoadedIntegration { span.setData(key, data[key]); }); - span.finish(); + span.end(); } // The original function will have one of these signatures: diff --git a/packages/tracing-internal/src/node/integrations/postgres.ts b/packages/tracing-internal/src/node/integrations/postgres.ts index 918f20664b92..810f07825653 100644 --- a/packages/tracing-internal/src/node/integrations/postgres.ts +++ b/packages/tracing-internal/src/node/integrations/postgres.ts @@ -137,14 +137,14 @@ export class Postgres implements LazyLoadedIntegration { if (typeof callback === 'function') { return orig.call(this, config, values, function (err: Error, result: unknown) { - span?.finish(); + span?.end(); callback(err, result); }); } if (typeof values === 'function') { return orig.call(this, config, function (err: Error, result: unknown) { - span?.finish(); + span?.end(); values(err, result); }); } @@ -153,12 +153,12 @@ export class Postgres implements LazyLoadedIntegration { if (isThenable(rv)) { return rv.then((res: unknown) => { - span?.finish(); + span?.end(); return res; }); } - span?.finish(); + span?.end(); return rv; }; }); diff --git a/packages/tracing-internal/test/browser/browsertracing.test.ts b/packages/tracing-internal/test/browser/browsertracing.test.ts index 65de2adbd85d..3cbd6eb9f6b4 100644 --- a/packages/tracing-internal/test/browser/browsertracing.test.ts +++ b/packages/tracing-internal/test/browser/browsertracing.test.ts @@ -70,7 +70,7 @@ conditionalTest({ min: 10 })('BrowserTracing', () => { const activeTransaction = getActiveTransaction(); if (activeTransaction) { // Should unset off of scope. - activeTransaction.finish(); + activeTransaction.end(); } }); @@ -178,10 +178,10 @@ conditionalTest({ min: 10 })('BrowserTracing', () => { const transaction = getActiveTransaction(hub) as IdleTransaction; const span = transaction.startChild(); - span.finish(); + span.end(); if (span.endTimestamp) { - transaction.finish(span.endTimestamp + 12345); + transaction.end(span.endTimestamp + 12345); } expect(transaction.endTimestamp).toBe(span.endTimestamp); }); @@ -418,10 +418,10 @@ conditionalTest({ min: 10 })('BrowserTracing', () => { createBrowserTracing(true, { routingInstrumentation: customInstrumentRouting }); const mockFinish = jest.fn(); const transaction = getActiveTransaction(hub) as IdleTransaction; - transaction.finish = mockFinish; + transaction.end = mockFinish; const span = transaction.startChild(); // activities = 1 - span.finish(); // activities = 0 + span.end(); // activities = 0 expect(mockFinish).toHaveBeenCalledTimes(0); jest.advanceTimersByTime(TRACING_DEFAULTS.idleTimeout); @@ -432,10 +432,10 @@ conditionalTest({ min: 10 })('BrowserTracing', () => { createBrowserTracing(true, { idleTimeout: 2000, routingInstrumentation: customInstrumentRouting }); const mockFinish = jest.fn(); const transaction = getActiveTransaction(hub) as IdleTransaction; - transaction.finish = mockFinish; + transaction.end = mockFinish; const span = transaction.startChild(); // activities = 1 - span.finish(); // activities = 0 + span.end(); // activities = 0 expect(mockFinish).toHaveBeenCalledTimes(0); jest.advanceTimersByTime(2000); @@ -447,7 +447,7 @@ conditionalTest({ min: 10 })('BrowserTracing', () => { const transaction = getActiveTransaction(hub) as IdleTransaction; const span = transaction.startChild(); // activities = 1 - span.finish(); // activities = 0 + span.end(); // activities = 0 jest.advanceTimersByTime(TRACING_DEFAULTS.idleTimeout); expect(mockStartTrackingWebVitals).toHaveBeenCalledTimes(1); @@ -460,10 +460,10 @@ conditionalTest({ min: 10 })('BrowserTracing', () => { createBrowserTracing(true, { heartbeatInterval: interval, routingInstrumentation: customInstrumentRouting }); const mockFinish = jest.fn(); const transaction = getActiveTransaction(hub) as IdleTransaction; - transaction.finish = mockFinish; + transaction.end = mockFinish; const span = transaction.startChild(); // activities = 1 - span.finish(); // activities = 0 + span.end(); // activities = 0 expect(mockFinish).toHaveBeenCalledTimes(0); jest.advanceTimersByTime(interval * 3); diff --git a/packages/tracing-internal/test/browser/router.test.ts b/packages/tracing-internal/test/browser/router.test.ts index a78d9e5631bd..a27926ad9803 100644 --- a/packages/tracing-internal/test/browser/router.test.ts +++ b/packages/tracing-internal/test/browser/router.test.ts @@ -17,7 +17,7 @@ jest.mock('@sentry/utils', () => { conditionalTest({ min: 16 })('instrumentRoutingWithDefaults', () => { const mockFinish = jest.fn(); - const customStartTransaction = jest.fn().mockReturnValue({ finish: mockFinish }); + const customStartTransaction = jest.fn().mockReturnValue({ end: mockFinish }); beforeEach(() => { const dom = new JSDOM(); // @ts-expect-error need to override global document diff --git a/packages/tracing/test/hub.test.ts b/packages/tracing/test/hub.test.ts index 817e88c9c55e..f7cf93cc5e32 100644 --- a/packages/tracing/test/hub.test.ts +++ b/packages/tracing/test/hub.test.ts @@ -415,11 +415,11 @@ describe('Hub', () => { makeMain(hub); const transaction = hub.startTransaction({ name: 'dogpark' }); - jest.spyOn(transaction, 'finish'); - transaction.finish(); + jest.spyOn(transaction, 'end'); + transaction.end(); expect(transaction.sampled).toBe(false); - expect(transaction.finish).toReturnWith(undefined); + expect(transaction.end).toReturnWith(undefined); expect(client.captureEvent).not.toBeCalled(); }); @@ -432,11 +432,11 @@ describe('Hub', () => { makeMain(hub); const transaction = hub.startTransaction({ name: 'dogpark' }); - jest.spyOn(transaction, 'finish'); - transaction.finish(); + jest.spyOn(transaction, 'end'); + transaction.end(); expect(transaction.sampled).toBe(false); - expect(transaction.finish).toReturnWith(undefined); + expect(transaction.end).toReturnWith(undefined); expect(client.captureEvent).not.toBeCalled(); expect(logger.error).toHaveBeenCalledWith( `A transaction was started with instrumenter=\`sentry\`, but the SDK is configured with the \`otel\` instrumenter. @@ -633,7 +633,7 @@ The transaction will not be sampled. Please use the otel instrumentation to star transaction.startChild({ op: 'test', startTimestamp: 1200, endTimestamp: 1500 }); - transaction.finish(2000); + transaction.end(2000); expect(captureEventSpy).toHaveBeenCalledTimes(1); expect(captureEventSpy.mock.calls[0][0].timestamp).toEqual(1500); diff --git a/packages/tracing/test/idletransaction.test.ts b/packages/tracing/test/idletransaction.test.ts index 30cd97f775b1..e0c5dd189cff 100644 --- a/packages/tracing/test/idletransaction.test.ts +++ b/packages/tracing/test/idletransaction.test.ts @@ -48,7 +48,7 @@ describe('IdleTransaction', () => { ); transaction.initSpanRecorder(10); - transaction.finish(); + transaction.end(); jest.runAllTimers(); const scope = hub.getScope(); @@ -65,7 +65,7 @@ describe('IdleTransaction', () => { true, ); - transaction.finish(); + transaction.end(); jest.runAllTimers(); const scope = hub.getScope(); @@ -87,7 +87,7 @@ describe('IdleTransaction', () => { const otherTransaction = new Transaction({ name: 'bar' }, hub); hub.getScope().setSpan(otherTransaction); - transaction.finish(); + transaction.end(); jest.runAllTimers(); const scope = hub.getScope(); @@ -101,7 +101,7 @@ describe('IdleTransaction', () => { it('push and pops activities', () => { const transaction = new IdleTransaction({ name: 'foo' }, hub); - const mockFinish = jest.spyOn(transaction, 'finish'); + const mockFinish = jest.spyOn(transaction, 'end'); transaction.initSpanRecorder(10); expect(transaction.activities).toMatchObject({}); @@ -110,7 +110,7 @@ describe('IdleTransaction', () => { expect(mockFinish).toHaveBeenCalledTimes(0); - span.finish(); + span.end(); expect(transaction.activities).toMatchObject({}); jest.runOnlyPendingTimers(); @@ -128,7 +128,7 @@ describe('IdleTransaction', () => { it('does not finish if there are still active activities', () => { const transaction = new IdleTransaction({ name: 'foo' }, hub); - const mockFinish = jest.spyOn(transaction, 'finish'); + const mockFinish = jest.spyOn(transaction, 'end'); transaction.initSpanRecorder(10); expect(transaction.activities).toMatchObject({}); @@ -136,7 +136,7 @@ describe('IdleTransaction', () => { const childSpan = span.startChild(); expect(transaction.activities).toMatchObject({ [span.spanId]: true, [childSpan.spanId]: true }); - span.finish(); + span.end(); jest.advanceTimersByTime(TRACING_DEFAULTS.idleTimeout + 1); expect(mockFinish).toHaveBeenCalledTimes(0); @@ -155,7 +155,7 @@ describe('IdleTransaction', () => { expect(mockCallback2).toHaveBeenCalledTimes(0); const span = transaction.startChild(); - span.finish(); + span.end(); jest.runOnlyPendingTimers(); expect(mockCallback1).toHaveBeenCalledTimes(1); @@ -177,8 +177,8 @@ describe('IdleTransaction', () => { // Should be cancelled - will not finish const cancelledSpan = transaction.startChild({ startTimestamp: transaction.startTimestamp + 4 }); - regularSpan.finish(regularSpan.startTimestamp + 4); - transaction.finish(transaction.startTimestamp + 10); + regularSpan.end(regularSpan.startTimestamp + 4); + transaction.end(transaction.startTimestamp + 10); expect(transaction.spanRecorder).toBeDefined(); if (transaction.spanRecorder) { @@ -202,9 +202,9 @@ describe('IdleTransaction', () => { transaction.initSpanRecorder(10); const span = transaction.startChild({ startTimestamp: transaction.startTimestamp + 2 }); - span.finish(span.startTimestamp + 10 + 30 + 1); + span.end(span.startTimestamp + 10 + 30 + 1); - transaction.finish(transaction.startTimestamp + 50); + transaction.end(transaction.startTimestamp + 50); expect(transaction.spanRecorder).toBeDefined(); expect(transaction.spanRecorder!.spans).toHaveLength(1); @@ -218,7 +218,7 @@ describe('IdleTransaction', () => { const recordDroppedEventSpy = jest.spyOn(client, 'recordDroppedEvent'); transaction.initSpanRecorder(10); - transaction.finish(transaction.startTimestamp + 10); + transaction.end(transaction.startTimestamp + 10); expect(recordDroppedEventSpy).toHaveBeenCalledWith('sample_rate', 'transaction'); }); @@ -247,12 +247,12 @@ describe('IdleTransaction', () => { transaction.initSpanRecorder(10); const span = transaction.startChild({}); - span.finish(); + span.end(); jest.advanceTimersByTime(2); const span2 = transaction.startChild({}); - span2.finish(); + span2.end(); jest.advanceTimersByTime(8); @@ -265,12 +265,12 @@ describe('IdleTransaction', () => { transaction.initSpanRecorder(10); const span = transaction.startChild({}); - span.finish(); + span.end(); jest.advanceTimersByTime(2); const span2 = transaction.startChild({}); - span2.finish(); + span2.end(); jest.advanceTimersByTime(10); @@ -287,8 +287,8 @@ describe('IdleTransaction', () => { const firstSpan = transaction.startChild({}); transaction.cancelIdleTimeout(undefined, { restartOnChildSpanChange: false }); const secondSpan = transaction.startChild({}); - firstSpan.finish(); - secondSpan.finish(); + firstSpan.end(); + secondSpan.end(); expect(transaction.endTimestamp).toBeDefined(); }); @@ -303,13 +303,13 @@ describe('IdleTransaction', () => { const secondSpan = transaction.startChild({}); const thirdSpan = transaction.startChild({}); - firstSpan.finish(); + firstSpan.end(); expect(transaction.endTimestamp).toBeUndefined(); - secondSpan.finish(); + secondSpan.end(); expect(transaction.endTimestamp).toBeUndefined(); - thirdSpan.finish(); + thirdSpan.end(); expect(transaction.endTimestamp).toBeDefined(); }); @@ -319,7 +319,7 @@ describe('IdleTransaction', () => { transaction.initSpanRecorder(10); const span = transaction.startChild({}); - span.finish(); + span.end(); jest.advanceTimersByTime(2); @@ -334,14 +334,14 @@ describe('IdleTransaction', () => { transaction.initSpanRecorder(10); const span = transaction.startChild({}); - span.finish(); + span.end(); jest.advanceTimersByTime(2); transaction.cancelIdleTimeout(); const span2 = transaction.startChild({}); - span2.finish(); + span2.end(); jest.advanceTimersByTime(8); expect(transaction.endTimestamp).toBeUndefined(); @@ -355,7 +355,7 @@ describe('IdleTransaction', () => { it('does not mark transaction as `DeadlineExceeded` if idle timeout has not been reached', () => { // 20s to exceed 3 heartbeats const transaction = new IdleTransaction({ name: 'foo' }, hub, 20000); - const mockFinish = jest.spyOn(transaction, 'finish'); + const mockFinish = jest.spyOn(transaction, 'end'); expect(transaction.status).not.toEqual('deadline_exceeded'); expect(mockFinish).toHaveBeenCalledTimes(0); @@ -378,7 +378,7 @@ describe('IdleTransaction', () => { it('finishes a transaction after 3 beats', () => { const transaction = new IdleTransaction({ name: 'foo' }, hub, TRACING_DEFAULTS.idleTimeout); - const mockFinish = jest.spyOn(transaction, 'finish'); + const mockFinish = jest.spyOn(transaction, 'end'); transaction.initSpanRecorder(10); expect(mockFinish).toHaveBeenCalledTimes(0); @@ -399,7 +399,7 @@ describe('IdleTransaction', () => { it('resets after new activities are added', () => { const transaction = new IdleTransaction({ name: 'foo' }, hub, TRACING_DEFAULTS.idleTimeout, 50000); - const mockFinish = jest.spyOn(transaction, 'finish'); + const mockFinish = jest.spyOn(transaction, 'end'); transaction.initSpanRecorder(10); expect(mockFinish).toHaveBeenCalledTimes(0); @@ -430,7 +430,7 @@ describe('IdleTransaction', () => { jest.advanceTimersByTime(TRACING_DEFAULTS.heartbeatInterval); expect(mockFinish).toHaveBeenCalledTimes(0); - span.finish(); // pop activity + span.end(); // pop activity // Beat 1 jest.advanceTimersByTime(TRACING_DEFAULTS.heartbeatInterval); @@ -469,7 +469,7 @@ describe('IdleTransactionSpanRecorder', () => { expect(mockPushActivity).toHaveBeenLastCalledWith(span.spanId); expect(mockPopActivity).toHaveBeenCalledTimes(0); - span.finish(); + span.end(); expect(mockPushActivity).toHaveBeenCalledTimes(1); expect(mockPopActivity).toHaveBeenCalledTimes(1); expect(mockPushActivity).toHaveBeenLastCalledWith(span.spanId); diff --git a/packages/tracing/test/integrations/apollo-nestjs.test.ts b/packages/tracing/test/integrations/apollo-nestjs.test.ts index 703edcaad23d..7e9866146385 100644 --- a/packages/tracing/test/integrations/apollo-nestjs.test.ts +++ b/packages/tracing/test/integrations/apollo-nestjs.test.ts @@ -84,7 +84,7 @@ describe('setupOnce', () => { jest.spyOn(scope, 'getSpan').mockReturnValueOnce(parentSpan); jest.spyOn(scope, 'setSpan'); jest.spyOn(parentSpan, 'startChild').mockReturnValueOnce(childSpan); - jest.spyOn(childSpan, 'finish'); + jest.spyOn(childSpan, 'end'); }); it('should wrap a simple resolver', () => { @@ -95,7 +95,7 @@ describe('setupOnce', () => { op: 'graphql.resolve', origin: 'auto.graphql.apollo', }); - expect(childSpan.finish).toBeCalled(); + expect(childSpan.end).toBeCalled(); }); it('should wrap another simple resolver', () => { @@ -106,7 +106,7 @@ describe('setupOnce', () => { op: 'graphql.resolve', origin: 'auto.graphql.apollo', }); - expect(childSpan.finish).toBeCalled(); + expect(childSpan.end).toBeCalled(); }); it("doesn't attach when using otel instrumenter", () => { diff --git a/packages/tracing/test/integrations/apollo.test.ts b/packages/tracing/test/integrations/apollo.test.ts index 90da1f8e0b21..ea861dcdec1f 100644 --- a/packages/tracing/test/integrations/apollo.test.ts +++ b/packages/tracing/test/integrations/apollo.test.ts @@ -84,7 +84,7 @@ describe('setupOnce', () => { jest.spyOn(scope, 'getSpan').mockReturnValueOnce(parentSpan); jest.spyOn(scope, 'setSpan'); jest.spyOn(parentSpan, 'startChild').mockReturnValueOnce(childSpan); - jest.spyOn(childSpan, 'finish'); + jest.spyOn(childSpan, 'end'); }); it('should wrap a simple resolver', () => { @@ -95,7 +95,7 @@ describe('setupOnce', () => { op: 'graphql.resolve', origin: 'auto.graphql.apollo', }); - expect(childSpan.finish).toBeCalled(); + expect(childSpan.end).toBeCalled(); }); it('should wrap another simple resolver', () => { @@ -106,7 +106,7 @@ describe('setupOnce', () => { op: 'graphql.resolve', origin: 'auto.graphql.apollo', }); - expect(childSpan.finish).toBeCalled(); + expect(childSpan.end).toBeCalled(); }); it("doesn't attach when using otel instrumenter", () => { diff --git a/packages/tracing/test/integrations/graphql.test.ts b/packages/tracing/test/integrations/graphql.test.ts index c61f287c3e86..06b9495d8061 100644 --- a/packages/tracing/test/integrations/graphql.test.ts +++ b/packages/tracing/test/integrations/graphql.test.ts @@ -46,7 +46,7 @@ describe('setupOnce', () => { jest.spyOn(scope, 'getSpan').mockReturnValueOnce(parentSpan); jest.spyOn(scope, 'setSpan'); jest.spyOn(parentSpan, 'startChild').mockReturnValueOnce(childSpan); - jest.spyOn(childSpan, 'finish'); + jest.spyOn(childSpan, 'end'); }); it('should wrap execute method', async () => { @@ -57,7 +57,7 @@ describe('setupOnce', () => { op: 'graphql.execute', origin: 'auto.graphql.graphql', }); - expect(childSpan.finish).toBeCalled(); + expect(childSpan.end).toBeCalled(); expect(scope.setSpan).toHaveBeenCalledTimes(2); }); diff --git a/packages/tracing/test/integrations/node/mongo.test.ts b/packages/tracing/test/integrations/node/mongo.test.ts index 30e5c76563bd..e2ab62ab2e04 100644 --- a/packages/tracing/test/integrations/node/mongo.test.ts +++ b/packages/tracing/test/integrations/node/mongo.test.ts @@ -66,7 +66,7 @@ describe('patchOperation()', () => { childSpan = parentSpan.startChild(); jest.spyOn(scope, 'getSpan').mockReturnValueOnce(parentSpan); jest.spyOn(parentSpan, 'startChild').mockReturnValueOnce(childSpan); - jest.spyOn(childSpan, 'finish'); + jest.spyOn(childSpan, 'end'); }); it('should wrap method accepting callback as the last argument', done => { @@ -84,7 +84,7 @@ describe('patchOperation()', () => { origin: 'auto.db.mongo', description: 'insertOne', }); - expect(childSpan.finish).toBeCalled(); + expect(childSpan.end).toBeCalled(); done(); }) as void; }); @@ -104,7 +104,7 @@ describe('patchOperation()', () => { origin: 'auto.db.mongo', description: 'insertOne', }); - expect(childSpan.finish).toBeCalled(); + expect(childSpan.end).toBeCalled(); }); it('should wrap method accepting no callback as the last argument and not returning promise', () => { @@ -121,7 +121,7 @@ describe('patchOperation()', () => { origin: 'auto.db.mongo', description: 'initializeOrderedBulkOp', }); - expect(childSpan.finish).toBeCalled(); + expect(childSpan.end).toBeCalled(); }); it("doesn't attach when using otel instrumenter", () => { diff --git a/packages/tracing/test/integrations/node/postgres.test.ts b/packages/tracing/test/integrations/node/postgres.test.ts index 446d837d22d7..c94b9870907b 100644 --- a/packages/tracing/test/integrations/node/postgres.test.ts +++ b/packages/tracing/test/integrations/node/postgres.test.ts @@ -67,7 +67,7 @@ describe('setupOnce', () => { childSpan = parentSpan.startChild(); jest.spyOn(scope, 'getSpan').mockReturnValueOnce(parentSpan); jest.spyOn(parentSpan, 'startChild').mockReturnValueOnce(childSpan); - jest.spyOn(childSpan, 'finish'); + jest.spyOn(childSpan, 'end'); }); it(`should wrap ${pgApi}'s query method accepting callback as the last argument`, done => { @@ -81,7 +81,7 @@ describe('setupOnce', () => { 'db.system': 'postgresql', }, }); - expect(childSpan.finish).toBeCalled(); + expect(childSpan.end).toBeCalled(); done(); }) as void; }); @@ -97,7 +97,7 @@ describe('setupOnce', () => { 'db.system': 'postgresql', }, }); - expect(childSpan.finish).toBeCalled(); + expect(childSpan.end).toBeCalled(); done(); }) as void; }); @@ -113,7 +113,7 @@ describe('setupOnce', () => { 'db.system': 'postgresql', }, }); - expect(childSpan.finish).toBeCalled(); + expect(childSpan.end).toBeCalled(); }); }); diff --git a/packages/tracing/test/span.test.ts b/packages/tracing/test/span.test.ts index 3e6c267b1233..c5a3c602952e 100644 --- a/packages/tracing/test/span.test.ts +++ b/packages/tracing/test/span.test.ts @@ -56,7 +56,7 @@ describe('Span', () => { const transaction = new Transaction({ name: 'test', sampled: true }); const span2 = transaction.startChild(); const span3 = span2.startChild(); - span3.finish(); + span3.end(); expect(transaction.spanRecorder).toBe(span2.spanRecorder); expect(transaction.spanRecorder).toBe(span3.spanRecorder); }); @@ -173,7 +173,7 @@ describe('Span', () => { test('simple', () => { const span = new Span({}); expect(span.endTimestamp).toBeUndefined(); - span.finish(); + span.end(); expect(span.endTimestamp).toBeGreaterThan(1); }); @@ -181,7 +181,7 @@ describe('Span', () => { test('finish a transaction', () => { const spy = jest.spyOn(hub as any, 'captureEvent') as any; const transaction = hub.startTransaction({ name: 'test' }); - transaction.finish(); + transaction.end(); expect(spy).toHaveBeenCalled(); expect(spy.mock.calls[0][0].spans).toHaveLength(0); expect(spy.mock.calls[0][0].timestamp).toBeTruthy(); @@ -193,8 +193,8 @@ describe('Span', () => { const spy = jest.spyOn(hub as any, 'captureEvent') as any; const transaction = hub.startTransaction({ name: 'test' }); const childSpan = transaction.startChild(); - childSpan.finish(); - transaction.finish(); + childSpan.end(); + transaction.end(); expect(spy).toHaveBeenCalled(); expect(spy.mock.calls[0][0].spans).toHaveLength(1); expect(spy.mock.calls[0][0].contexts.trace).toEqual(transaction.getTraceContext()); @@ -205,8 +205,8 @@ describe('Span', () => { const spy = jest.spyOn(hub as any, 'captureEvent') as any; const transaction = hub.startTransaction({ name: 'test', op: 'parent', sampled: true }); const childSpan = transaction.startChild({ op: 'child' }); - childSpan.finish(); - transaction.finish(); + childSpan.end(); + transaction.end(); expect(spy).toHaveBeenCalled(); expect(spy.mock.calls[0][0].spans).toHaveLength(1); expect(spy.mock.calls[0][0].contexts.trace).toEqual(transaction.getTraceContext()); @@ -216,7 +216,7 @@ describe('Span', () => { const spy = jest.spyOn(hub as any, 'captureEvent') as any; const transaction = hub.startTransaction({ name: 'test' }); const childSpan = transaction.startChild(); - childSpan.finish(); + childSpan.end(); expect(spy).not.toHaveBeenCalled(); }); @@ -224,13 +224,13 @@ describe('Span', () => { const spy = jest.spyOn(hub as any, 'captureEvent') as any; const transaction = hub.startTransaction({ name: 'test' }); const childSpanOne = transaction.startChild(); - childSpanOne.finish(); + childSpanOne.end(); hub.getScope().setSpan(childSpanOne); const spanTwo = transaction.startChild(); - spanTwo.finish(); - transaction.finish(); + spanTwo.end(); + transaction.end(); expect(spy).toHaveBeenCalled(); expect(spy.mock.calls[0][0].spans).toHaveLength(2); @@ -247,9 +247,9 @@ describe('Span', () => { const transaction = _hub.startTransaction({ name: 'test' }); for (let i = 0; i < 10; i++) { const child = transaction.startChild(); - child.finish(); + child.end(); } - transaction.finish(); + transaction.end(); expect(spy.mock.calls[0][0].spans).toHaveLength(3); }); @@ -262,9 +262,9 @@ describe('Span', () => { const transaction = _hub.startTransaction({ name: 'test', sampled: false }); for (let i = 0; i < 10; i++) { const child = transaction.startChild(); - child.finish(); + child.end(); } - transaction.finish(); + transaction.end(); expect((transaction as any).spanRecorder).toBeUndefined(); expect(spy).not.toHaveBeenCalled(); }); @@ -276,15 +276,158 @@ describe('Span', () => { const childSpanOne = transaction.startChild(); const childSpanTwo = childSpanOne.startChild(); - childSpanTwo.finish(); + childSpanTwo.end(); - childSpanOne.finish(); + childSpanOne.end(); hub.getScope().setSpan(transaction); const spanTwo = transaction.startChild({}); - spanTwo.finish(); - transaction.finish(); + spanTwo.end(); + transaction.end(); + + expect(spy).toHaveBeenCalled(); + expect(spy.mock.calls[0][0].spans).toHaveLength(3); + expect(spy.mock.calls[0][0].contexts.trace).toEqual(transaction.getTraceContext()); + expect(childSpanOne.toJSON().parent_span_id).toEqual(transaction.toJSON().span_id); + expect(childSpanTwo.toJSON().parent_span_id).toEqual(childSpanOne.toJSON().span_id); + expect(spanTwo.toJSON().parent_span_id).toEqual(transaction.toJSON().span_id); + }); + }); + }); + + describe('end', () => { + test('simple', () => { + const span = new Span({}); + expect(span.endTimestamp).toBeUndefined(); + span.end(); + expect(span.endTimestamp).toBeGreaterThan(1); + }); + + test('with endTime in seconds', () => { + const span = new Span({}); + expect(span.endTimestamp).toBeUndefined(); + const endTime = Date.now() / 1000; + span.end(endTime); + expect(span.endTimestamp).toBe(endTime); + }); + + test('with endTime in milliseconds', () => { + const span = new Span({}); + expect(span.endTimestamp).toBeUndefined(); + const endTime = Date.now(); + span.end(endTime); + expect(span.endTimestamp).toBe(endTime / 1000); + }); + + describe('hub.startTransaction', () => { + test('finish a transaction', () => { + const spy = jest.spyOn(hub as any, 'captureEvent') as any; + const transaction = hub.startTransaction({ name: 'test' }); + transaction.end(); + expect(spy).toHaveBeenCalled(); + expect(spy.mock.calls[0][0].spans).toHaveLength(0); + expect(spy.mock.calls[0][0].timestamp).toBeTruthy(); + expect(spy.mock.calls[0][0].start_timestamp).toBeTruthy(); + expect(spy.mock.calls[0][0].contexts.trace).toEqual(transaction.getTraceContext()); + }); + + test('finish a transaction + child span', () => { + const spy = jest.spyOn(hub as any, 'captureEvent') as any; + const transaction = hub.startTransaction({ name: 'test' }); + const childSpan = transaction.startChild(); + childSpan.end(); + transaction.end(); + expect(spy).toHaveBeenCalled(); + expect(spy.mock.calls[0][0].spans).toHaveLength(1); + expect(spy.mock.calls[0][0].contexts.trace).toEqual(transaction.getTraceContext()); + }); + + // See https://github.com/getsentry/sentry-javascript/issues/3254 + test('finish a transaction + child span + sampled:true', () => { + const spy = jest.spyOn(hub as any, 'captureEvent') as any; + const transaction = hub.startTransaction({ name: 'test', op: 'parent', sampled: true }); + const childSpan = transaction.startChild({ op: 'child' }); + childSpan.end(); + transaction.end(); + expect(spy).toHaveBeenCalled(); + expect(spy.mock.calls[0][0].spans).toHaveLength(1); + expect(spy.mock.calls[0][0].contexts.trace).toEqual(transaction.getTraceContext()); + }); + + test("finish a child span shouldn't trigger captureEvent", () => { + const spy = jest.spyOn(hub as any, 'captureEvent') as any; + const transaction = hub.startTransaction({ name: 'test' }); + const childSpan = transaction.startChild(); + childSpan.end(); + expect(spy).not.toHaveBeenCalled(); + }); + + test("finish a span with another one on the scope shouldn't override contexts.trace", () => { + const spy = jest.spyOn(hub as any, 'captureEvent') as any; + const transaction = hub.startTransaction({ name: 'test' }); + const childSpanOne = transaction.startChild(); + childSpanOne.end(); + + hub.getScope().setSpan(childSpanOne); + + const spanTwo = transaction.startChild(); + spanTwo.end(); + transaction.end(); + + expect(spy).toHaveBeenCalled(); + expect(spy.mock.calls[0][0].spans).toHaveLength(2); + expect(spy.mock.calls[0][0].contexts.trace).toEqual(transaction.getTraceContext()); + }); + + test('maxSpans correctly limits number of spans', () => { + const options = getDefaultBrowserClientOptions({ + _experiments: { maxSpans: 3 }, + tracesSampleRate: 1, + }); + const _hub = new Hub(new BrowserClient(options)); + const spy = jest.spyOn(_hub as any, 'captureEvent') as any; + const transaction = _hub.startTransaction({ name: 'test' }); + for (let i = 0; i < 10; i++) { + const child = transaction.startChild(); + child.end(); + } + transaction.end(); + expect(spy.mock.calls[0][0].spans).toHaveLength(3); + }); + + test('no span recorder created if transaction.sampled is false', () => { + const options = getDefaultBrowserClientOptions({ + tracesSampleRate: 1, + }); + const _hub = new Hub(new BrowserClient(options)); + const spy = jest.spyOn(_hub as any, 'captureEvent') as any; + const transaction = _hub.startTransaction({ name: 'test', sampled: false }); + for (let i = 0; i < 10; i++) { + const child = transaction.startChild(); + child.end(); + } + transaction.end(); + expect((transaction as any).spanRecorder).toBeUndefined(); + expect(spy).not.toHaveBeenCalled(); + }); + + test('tree structure of spans should be correct when mixing it with span on scope', () => { + const spy = jest.spyOn(hub as any, 'captureEvent') as any; + + const transaction = hub.startTransaction({ name: 'test' }); + const childSpanOne = transaction.startChild(); + + const childSpanTwo = childSpanOne.startChild(); + childSpanTwo.end(); + + childSpanOne.end(); + + hub.getScope().setSpan(transaction); + + const spanTwo = transaction.startChild({}); + spanTwo.end(); + transaction.end(); expect(spy).toHaveBeenCalled(); expect(spy.mock.calls[0][0].spans).toHaveLength(3); @@ -508,7 +651,7 @@ describe('Span', () => { }); expect(spy).toHaveBeenCalledTimes(0); - transaction.finish(); + transaction.end(); expect(spy).toHaveBeenCalledTimes(1); expect(spy).toHaveBeenLastCalledWith( expect.objectContaining({ diff --git a/packages/tracing/test/transaction.test.ts b/packages/tracing/test/transaction.test.ts index 979dc119531e..64691385f533 100644 --- a/packages/tracing/test/transaction.test.ts +++ b/packages/tracing/test/transaction.test.ts @@ -141,7 +141,7 @@ describe('`Transaction` class', () => { const transaction = hub.startTransaction({ name: 'dogpark' }); transaction.setContext('foo', { key: 'val' }); - transaction.finish(); + transaction.end(); // eslint-disable-next-line @typescript-eslint/unbound-method expect(hub.captureEvent).toHaveBeenCalledTimes(1); @@ -169,7 +169,7 @@ describe('`Transaction` class', () => { const transaction = hub.startTransaction({ name: 'dogpark' }); transaction.setContext('trace', { key: 'val' }); - transaction.finish(); + transaction.end(); // eslint-disable-next-line @typescript-eslint/unbound-method expect(hub.captureEvent).toHaveBeenCalledTimes(1); diff --git a/packages/types/src/hub.ts b/packages/types/src/hub.ts index 8d4d47885d40..ca18e088340a 100644 --- a/packages/types/src/hub.ts +++ b/packages/types/src/hub.ts @@ -200,7 +200,7 @@ export interface Hub { * * Every child span must be finished before the transaction is finished, otherwise the unfinished spans are discarded. * - * The transaction must be finished with a call to its `.finish()` method, at which point the transaction with all its + * The transaction must be finished with a call to its `.end()` method, at which point the transaction with all its * finished child spans will be sent to Sentry. * * @param context Properties of the new `Transaction`. diff --git a/packages/types/src/span.ts b/packages/types/src/span.ts index c931b7e457c2..1877d7a25524 100644 --- a/packages/types/src/span.ts +++ b/packages/types/src/span.ts @@ -134,6 +134,11 @@ export interface Span extends SpanContext { */ finish(endTimestamp?: number): void; + /** + * End the current span. + */ + end(endTimestamp?: number): void; + /** * Sets the tag attribute on the current span. * diff --git a/packages/vue/src/tracing.ts b/packages/vue/src/tracing.ts index ef509dcdb406..810fa40b045d 100644 --- a/packages/vue/src/tracing.ts +++ b/packages/vue/src/tracing.ts @@ -45,7 +45,7 @@ function finishRootSpan(vm: VueSentry, timestamp: number, timeout: number): void vm.$_sentryRootSpanTimer = setTimeout(() => { if (vm.$root && vm.$root.$_sentryRootSpan) { - vm.$root.$_sentryRootSpan.finish(timestamp); + vm.$root.$_sentryRootSpan.end(timestamp); vm.$root.$_sentryRootSpan = undefined; } }, timeout); @@ -108,7 +108,7 @@ export const createTracingMixins = (options: TracingOptions): Mixins => { // finished so we finish the span before starting a new one, just to be sure. const oldSpan = this.$_sentrySpans[operation]; if (oldSpan && !oldSpan.endTimestamp) { - oldSpan.finish(); + oldSpan.end(); } this.$_sentrySpans[operation] = activeTransaction.startChild({ @@ -123,7 +123,7 @@ export const createTracingMixins = (options: TracingOptions): Mixins => { // The before hook did not start the tracking span, so the span was not added. // This is probably because it happened before there is an active transaction if (!span) return; - span.finish(); + span.end(); finishRootSpan(this, timestampInSeconds(), options.timeout); } From 213bb4af298e46c321f70de1e3a7a5214e42ef97 Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Thu, 21 Dec 2023 16:05:24 +0100 Subject: [PATCH 13/24] feat(core): Add `getIsolationScope()` method (#9957) Also ensure to actually export `getGlobalScope()` everywhere as well. The isolation scope, currently, lies on the hub, and is applied to all events. --- packages/astro/src/index.server.ts | 2 + packages/bun/src/index.ts | 2 + packages/core/src/baseclient.ts | 10 ++- packages/core/src/hub.ts | 31 +++++++++- packages/core/src/index.ts | 1 + packages/core/src/server-runtime-client.ts | 9 ++- packages/core/src/utils/prepareEvent.ts | 6 ++ packages/core/test/lib/prepareEvent.test.ts | 62 ++++++++++++++----- packages/deno/src/index.ts | 2 + .../feedback/src/util/prepareFeedbackEvent.ts | 2 + packages/hub/test/global.test.ts | 4 +- packages/node-experimental/src/sdk/client.ts | 13 +++- packages/node-experimental/src/sdk/hub.ts | 5 +- packages/node-experimental/src/sdk/scope.ts | 26 -------- .../node-experimental/test/sdk/scope.test.ts | 16 +---- .../scopes/initialScopes/scenario.ts | 23 +++++++ .../public-api/scopes/initialScopes/test.ts | 31 ++++++++++ .../scopes/isolationScope/scenario.ts | 30 +++++++++ .../public-api/scopes/isolationScope/test.ts | 47 ++++++++++++++ packages/node/src/index.ts | 2 + packages/opentelemetry/src/custom/client.ts | 7 ++- .../replay/src/util/prepareReplayEvent.ts | 2 + packages/serverless/src/index.ts | 2 + packages/sveltekit/src/server/index.ts | 2 + packages/types/src/hub.ts | 6 ++ packages/vercel-edge/src/index.ts | 2 + 26 files changed, 274 insertions(+), 71 deletions(-) create mode 100644 packages/node-integration-tests/suites/public-api/scopes/initialScopes/scenario.ts create mode 100644 packages/node-integration-tests/suites/public-api/scopes/initialScopes/test.ts create mode 100644 packages/node-integration-tests/suites/public-api/scopes/isolationScope/scenario.ts create mode 100644 packages/node-integration-tests/suites/public-api/scopes/isolationScope/test.ts diff --git a/packages/astro/src/index.server.ts b/packages/astro/src/index.server.ts index adcf95527364..73dce6f0fa7f 100644 --- a/packages/astro/src/index.server.ts +++ b/packages/astro/src/index.server.ts @@ -27,6 +27,8 @@ export { getCurrentHub, getClient, getCurrentScope, + getGlobalScope, + getIsolationScope, Hub, makeMain, Scope, diff --git a/packages/bun/src/index.ts b/packages/bun/src/index.ts index 499e969f3843..0166b43d137d 100644 --- a/packages/bun/src/index.ts +++ b/packages/bun/src/index.ts @@ -44,6 +44,8 @@ export { getCurrentHub, getClient, getCurrentScope, + getGlobalScope, + getIsolationScope, Hub, lastEventId, makeMain, diff --git a/packages/core/src/baseclient.ts b/packages/core/src/baseclient.ts index fc0258167530..865fdcbd14b5 100644 --- a/packages/core/src/baseclient.ts +++ b/packages/core/src/baseclient.ts @@ -49,6 +49,7 @@ import { getEnvelopeEndpointWithUrlEncodedAuth } from './api'; import { DEBUG_BUILD } from './debug-build'; import { createEventEnvelope, createSessionEnvelope } from './envelope'; import { getClient } from './exports'; +import { getIsolationScope } from './hub'; import type { IntegrationIndex } from './integration'; import { setupIntegration, setupIntegrations } from './integration'; import { createMetricEnvelope } from './metrics/envelope'; @@ -588,7 +589,12 @@ export abstract class BaseClient implements Client { * @param scope A scope containing event metadata. * @returns A new event with more information. */ - protected _prepareEvent(event: Event, hint: EventHint, scope?: Scope): PromiseLike { + protected _prepareEvent( + event: Event, + hint: EventHint, + scope?: Scope, + isolationScope = getIsolationScope(), + ): PromiseLike { const options = this.getOptions(); const integrations = Object.keys(this._integrations); if (!hint.integrations && integrations.length > 0) { @@ -597,7 +603,7 @@ export abstract class BaseClient implements Client { this.emit('preprocessEvent', event, hint); - return prepareEvent(options, event, hint, scope, this).then(evt => { + return prepareEvent(options, event, hint, scope, this, isolationScope).then(evt => { if (evt === null) { return evt; } diff --git a/packages/core/src/hub.ts b/packages/core/src/hub.ts index 75960550081a..29e9d5af3956 100644 --- a/packages/core/src/hub.ts +++ b/packages/core/src/hub.ts @@ -104,6 +104,8 @@ export class Hub implements HubInterface { /** Contains the last event id of a captured event. */ private _lastEventId?: string; + private _isolationScope: Scope; + /** * Creates a new instance of the hub, will push one {@link Layer} into the * internal stack on creation. @@ -112,11 +114,18 @@ export class Hub implements HubInterface { * @param scope bound to the hub. * @param version number, higher number means higher priority. */ - public constructor(client?: Client, scope: Scope = new Scope(), private readonly _version: number = API_VERSION) { + public constructor( + client?: Client, + scope: Scope = new Scope(), + isolationScope = new Scope(), + private readonly _version: number = API_VERSION, + ) { this._stack = [{ scope }]; if (client) { this.bindClient(client); } + + this._isolationScope = isolationScope; } /** @@ -188,6 +197,11 @@ export class Hub implements HubInterface { return this.getStackTop().scope; } + /** @inheritdoc */ + public getIsolationScope(): Scope { + return this._isolationScope; + } + /** Returns the scope stack for domains or the process. */ public getStack(): Layer[] { return this._stack; @@ -567,6 +581,15 @@ export function getCurrentHub(): Hub { return getGlobalHub(registry); } +/** + * Get the currently active isolation scope. + * The isolation scope is active for the current exection context, + * meaning that it will remain stable for the same Hub. + */ +export function getIsolationScope(): Scope { + return getCurrentHub().getIsolationScope(); +} + function getGlobalHub(registry: Carrier = getMainCarrier()): Hub { // If there's no hub, or its an old API, assign a new one if (!hasHubOnCarrier(registry) || getHubFromCarrier(registry).isOlderThan(API_VERSION)) { @@ -585,8 +608,10 @@ function getGlobalHub(registry: Carrier = getMainCarrier()): Hub { export function ensureHubOnCarrier(carrier: Carrier, parent: Hub = getGlobalHub()): void { // If there's no hub on current domain, or it's an old API, assign a new one if (!hasHubOnCarrier(carrier) || getHubFromCarrier(carrier).isOlderThan(API_VERSION)) { - const globalHubTopStack = parent.getStackTop(); - setHubOnCarrier(carrier, new Hub(globalHubTopStack.client, globalHubTopStack.scope.clone())); + const client = parent.getClient(); + const scope = parent.getScope(); + const isolationScope = parent.getIsolationScope(); + setHubOnCarrier(carrier, new Hub(client, scope.clone(), isolationScope.clone())); } } diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index dbf18135ef67..3954dc6a7005 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -31,6 +31,7 @@ export { } from './exports'; export { getCurrentHub, + getIsolationScope, getHubFromCarrier, Hub, makeMain, diff --git a/packages/core/src/server-runtime-client.ts b/packages/core/src/server-runtime-client.ts index 9cffaca15faf..f4abd134223f 100644 --- a/packages/core/src/server-runtime-client.ts +++ b/packages/core/src/server-runtime-client.ts @@ -216,7 +216,12 @@ export class ServerRuntimeClient< /** * @inheritDoc */ - protected _prepareEvent(event: Event, hint: EventHint, scope?: Scope): PromiseLike { + protected _prepareEvent( + event: Event, + hint: EventHint, + scope?: Scope, + isolationScope?: Scope, + ): PromiseLike { if (this._options.platform) { event.platform = event.platform || this._options.platform; } @@ -232,7 +237,7 @@ export class ServerRuntimeClient< event.server_name = event.server_name || this._options.serverName; } - return super._prepareEvent(event, hint, scope); + return super._prepareEvent(event, hint, scope, isolationScope); } /** Extract trace information from scope */ diff --git a/packages/core/src/utils/prepareEvent.ts b/packages/core/src/utils/prepareEvent.ts index a4584c34066a..6f448df8496d 100644 --- a/packages/core/src/utils/prepareEvent.ts +++ b/packages/core/src/utils/prepareEvent.ts @@ -48,6 +48,7 @@ export function prepareEvent( hint: EventHint, scope?: Scope, client?: Client, + isolationScope?: Scope, ): PromiseLike { const { normalizeDepth = 3, normalizeMaxBreadth = 1_000 } = options; const prepared: Event = { @@ -80,6 +81,11 @@ export function prepareEvent( // Merge scope data together const data = getGlobalScope().getScopeData(); + if (isolationScope) { + const isolationData = isolationScope.getScopeData(); + mergeScopeData(data, isolationData); + } + if (finalScope) { const finalScopeData = finalScope.getScopeData(); mergeScopeData(data, finalScopeData); diff --git a/packages/core/test/lib/prepareEvent.test.ts b/packages/core/test/lib/prepareEvent.test.ts index 09c48afffc35..98895ba31256 100644 --- a/packages/core/test/lib/prepareEvent.test.ts +++ b/packages/core/test/lib/prepareEvent.test.ts @@ -9,7 +9,7 @@ import type { ScopeContext, } from '@sentry/types'; import { GLOBAL_OBJ, createStackParser } from '@sentry/utils'; -import { setGlobalScope } from '../../src'; +import { getCurrentHub, getIsolationScope, setGlobalScope } from '../../src'; import { Scope, getGlobalScope } from '../../src/scope'; import { @@ -192,6 +192,7 @@ describe('parseEventHintOrCaptureContext', () => { describe('prepareEvent', () => { beforeEach(() => { setGlobalScope(undefined); + getCurrentHub().getIsolationScope().clear(); }); it('works without any scope data', async () => { @@ -240,12 +241,15 @@ describe('prepareEvent', () => { const breadcrumb1 = { message: '1', timestamp: 111 } as Breadcrumb; const breadcrumb2 = { message: '2', timestamp: 222 } as Breadcrumb; const breadcrumb3 = { message: '3', timestamp: 123 } as Breadcrumb; + const breadcrumb4 = { message: '4', timestamp: 123 } as Breadcrumb; const eventProcessor1 = jest.fn((a: unknown) => a) as EventProcessor; const eventProcessor2 = jest.fn((b: unknown) => b) as EventProcessor; + const eventProcessor3 = jest.fn((b: unknown) => b) as EventProcessor; const attachment1 = { filename: '1' } as Attachment; const attachment2 = { filename: '2' } as Attachment; + const attachment3 = { filename: '3' } as Attachment; const scope = new Scope(); scope.update({ @@ -261,13 +265,19 @@ describe('prepareEvent', () => { scope.addAttachment(attachment1); const globalScope = getGlobalScope(); + const isolationScope = getIsolationScope(); globalScope.addBreadcrumb(breadcrumb2); globalScope.addEventProcessor(eventProcessor2); globalScope.setSDKProcessingMetadata({ aa: 'aa' }); globalScope.addAttachment(attachment2); - const event = { message: 'foo', breadcrumbs: [breadcrumb3], fingerprint: ['dd'] }; + isolationScope.addBreadcrumb(breadcrumb3); + isolationScope.addEventProcessor(eventProcessor3); + isolationScope.setSDKProcessingMetadata({ bb: 'bb' }); + isolationScope.addAttachment(attachment3); + + const event = { message: 'foo', breadcrumbs: [breadcrumb4], fingerprint: ['dd'] }; const options = {} as ClientOptions; const processedEvent = await prepareEvent( @@ -277,15 +287,18 @@ describe('prepareEvent', () => { integrations: [], }, scope, + undefined, + isolationScope, ); expect(eventProcessor1).toHaveBeenCalledTimes(1); expect(eventProcessor2).toHaveBeenCalledTimes(1); + expect(eventProcessor3).toHaveBeenCalledTimes(1); // Test that attachments are correctly merged expect(eventProcessor1).toHaveBeenCalledWith(processedEvent, { integrations: [], - attachments: [attachment2, attachment1], + attachments: [attachment2, attachment3, attachment1], }); expect(processedEvent).toEqual({ @@ -298,9 +311,10 @@ describe('prepareEvent', () => { extra: { extra1: 'aa', extra2: 'aa' }, contexts: { os: { name: 'os1' }, culture: { display_name: 'name1' } }, fingerprint: ['dd', 'aa'], - breadcrumbs: [breadcrumb3, breadcrumb2, breadcrumb1], + breadcrumbs: [breadcrumb4, breadcrumb2, breadcrumb3, breadcrumb1], sdkProcessingMetadata: { aa: 'aa', + bb: 'bb', propagationContext: { spanId: '1', traceId: '1', @@ -312,33 +326,50 @@ describe('prepareEvent', () => { it('works without a scope', async () => { const breadcrumb1 = { message: '1', timestamp: 111 } as Breadcrumb; const breadcrumb2 = { message: '2', timestamp: 222 } as Breadcrumb; + const breadcrumb3 = { message: '3', timestamp: 333 } as Breadcrumb; const eventProcessor1 = jest.fn((a: unknown) => a) as EventProcessor; + const eventProcessor2 = jest.fn((a: unknown) => a) as EventProcessor; - const attachment1 = { filename: '1' } as Attachment; - const attachment2 = { filename: '2' } as Attachment; + const attachmentGlobal = { filename: 'global scope attachment' } as Attachment; + const attachmentIsolation = { filename: 'isolation scope attachment' } as Attachment; + const attachmentHint = { filename: 'hint attachment' } as Attachment; const globalScope = getGlobalScope(); + const isolationScope = getIsolationScope(); globalScope.addBreadcrumb(breadcrumb1); globalScope.addEventProcessor(eventProcessor1); globalScope.setSDKProcessingMetadata({ aa: 'aa' }); - globalScope.addAttachment(attachment1); + globalScope.addAttachment(attachmentGlobal); - const event = { message: 'foo', breadcrumbs: [breadcrumb2], fingerprint: ['dd'] }; + isolationScope.addBreadcrumb(breadcrumb2); + isolationScope.addEventProcessor(eventProcessor2); + isolationScope.setSDKProcessingMetadata({ bb: 'bb' }); + isolationScope.addAttachment(attachmentIsolation); + + const event = { message: 'foo', breadcrumbs: [breadcrumb3], fingerprint: ['dd'] }; const options = {} as ClientOptions; - const processedEvent = await prepareEvent(options, event, { - integrations: [], - attachments: [attachment2], - }); + const processedEvent = await prepareEvent( + options, + event, + { + integrations: [], + attachments: [attachmentHint], + }, + undefined, + undefined, + isolationScope, + ); expect(eventProcessor1).toHaveBeenCalledTimes(1); + expect(eventProcessor2).toHaveBeenCalledTimes(1); // Test that attachments are correctly merged expect(eventProcessor1).toHaveBeenCalledWith(processedEvent, { integrations: [], - attachments: [attachment2, attachment1], + attachments: [attachmentHint, attachmentGlobal, attachmentIsolation], }); expect(processedEvent).toEqual({ @@ -347,10 +378,11 @@ describe('prepareEvent', () => { environment: 'production', message: 'foo', fingerprint: ['dd'], - breadcrumbs: [breadcrumb2, breadcrumb1], + breadcrumbs: [breadcrumb3, breadcrumb1, breadcrumb2], sdkProcessingMetadata: { aa: 'aa', - propagationContext: globalScope.getPropagationContext(), + bb: 'bb', + propagationContext: isolationScope.getPropagationContext(), }, }); }); diff --git a/packages/deno/src/index.ts b/packages/deno/src/index.ts index bd2a7061019a..4a57bb6f2cfd 100644 --- a/packages/deno/src/index.ts +++ b/packages/deno/src/index.ts @@ -43,6 +43,8 @@ export { getCurrentHub, getClient, getCurrentScope, + getGlobalScope, + getIsolationScope, Hub, lastEventId, makeMain, diff --git a/packages/feedback/src/util/prepareFeedbackEvent.ts b/packages/feedback/src/util/prepareFeedbackEvent.ts index 9b1b0f9c6e8b..cb48efeaf89d 100644 --- a/packages/feedback/src/util/prepareFeedbackEvent.ts +++ b/packages/feedback/src/util/prepareFeedbackEvent.ts @@ -1,4 +1,5 @@ import type { Scope } from '@sentry/core'; +import { getIsolationScope } from '@sentry/core'; import { prepareEvent } from '@sentry/core'; import type { Client, FeedbackEvent } from '@sentry/types'; @@ -26,6 +27,7 @@ export async function prepareFeedbackEvent({ eventHint, scope, client, + getIsolationScope(), )) as FeedbackEvent | null; if (preparedEvent === null) { diff --git a/packages/hub/test/global.test.ts b/packages/hub/test/global.test.ts index dafe638a5a92..23bc51193a29 100644 --- a/packages/hub/test/global.test.ts +++ b/packages/hub/test/global.test.ts @@ -19,13 +19,13 @@ describe('global', () => { }); test('getGlobalHub', () => { - const newestHub = new Hub(undefined, undefined, 999999); + const newestHub = new Hub(undefined, undefined, undefined, 999999); GLOBAL_OBJ.__SENTRY__.hub = newestHub; expect(getCurrentHub()).toBe(newestHub); }); test('hub extension methods receive correct hub instance', () => { - const newestHub = new Hub(undefined, undefined, 999999); + const newestHub = new Hub(undefined, undefined, undefined, 999999); GLOBAL_OBJ.__SENTRY__.hub = newestHub; const fn = jest.fn().mockImplementation(function (...args: []) { // @ts-expect-error typescript complains that this can be `any` diff --git a/packages/node-experimental/src/sdk/client.ts b/packages/node-experimental/src/sdk/client.ts index 8a7626b4ff9c..6f1012bdb422 100644 --- a/packages/node-experimental/src/sdk/client.ts +++ b/packages/node-experimental/src/sdk/client.ts @@ -4,7 +4,7 @@ import type { Tracer } from '@opentelemetry/api'; import { trace } from '@opentelemetry/api'; import type { BasicTracerProvider } from '@opentelemetry/sdk-trace-base'; import type { CaptureContext, Event, EventHint } from '@sentry/types'; -import { Scope } from './scope'; +import { Scope, getIsolationScope } from './scope'; /** A client for using Sentry with Node & OpenTelemetry. */ export class NodeExperimentalClient extends NodeClient { @@ -59,7 +59,12 @@ export class NodeExperimentalClient extends NodeClient { * Extends the base `_prepareEvent` so that we can properly handle `captureContext`. * This uses `new Scope()`, which we need to replace with our own Scope for this client. */ - protected _prepareEvent(event: Event, hint: EventHint, scope?: Scope): PromiseLike { + protected _prepareEvent( + event: Event, + hint: EventHint, + scope?: Scope, + _isolationScope?: Scope, + ): PromiseLike { let actualScope = scope; // Remove `captureContext` hint and instead clone already here @@ -68,7 +73,9 @@ export class NodeExperimentalClient extends NodeClient { delete hint.captureContext; } - return super._prepareEvent(event, hint, actualScope); + const isolationScope = _isolationScope || (scope && scope.isolationScope) || getIsolationScope(); + + return super._prepareEvent(event, hint, actualScope, isolationScope); } } diff --git a/packages/node-experimental/src/sdk/hub.ts b/packages/node-experimental/src/sdk/hub.ts index 21e1c83a34bb..b58548acb326 100644 --- a/packages/node-experimental/src/sdk/hub.ts +++ b/packages/node-experimental/src/sdk/hub.ts @@ -5,7 +5,6 @@ import type { Hub, Integration, IntegrationClass, - Session, Severity, SeverityLevel, TransactionContext, @@ -14,8 +13,6 @@ import type { import { addBreadcrumb, captureEvent, - captureException, - captureMessage, configureScope, endSession, getClient, @@ -32,6 +29,7 @@ import { } from './api'; import { callExtensionMethod, getGlobalCarrier } from './globals'; import type { Scope } from './scope'; +import { getIsolationScope } from './scope'; import type { SentryCarrier } from './types'; /** Ensure the global hub is our proxied hub. */ @@ -67,6 +65,7 @@ export function getCurrentHub(): Hub { withScope, getClient, getScope: getCurrentScope, + getIsolationScope, captureException: (exception: unknown, hint?: EventHint) => { return getCurrentScope().captureException(exception, hint); }, diff --git a/packages/node-experimental/src/sdk/scope.ts b/packages/node-experimental/src/sdk/scope.ts index f169f95250e2..86bad56e932e 100644 --- a/packages/node-experimental/src/sdk/scope.ts +++ b/packages/node-experimental/src/sdk/scope.ts @@ -189,32 +189,6 @@ export class Scope extends OpenTelemetryScope implements ScopeInterface { public getOwnScopeData(): ScopeData { return super.getScopeData(); } - - /** @inheritdoc */ - public getScopeData(): ScopeData { - const globalScope = getGlobalScope(); - const isolationScope = this._getIsolationScope(); - - // Special case: If this is the global/isolation scope, no need to merge other data in here - if (this === globalScope || this === isolationScope) { - return this.getOwnScopeData(); - } - - // Global scope is applied anyhow in prepareEvent, - // but we need to merge the isolation scope in here - const data = isolationScope.getOwnScopeData(); - const scopeData = this.getOwnScopeData(); - - // Merge data together, in order - mergeScopeData(data, scopeData); - - return data; - } - - /** Get the isolation scope for this scope. */ - protected _getIsolationScope(): Scope { - return this.isolationScope || getIsolationScope(); - } } function getScopes(): CurrentScopes { diff --git a/packages/node-experimental/test/sdk/scope.test.ts b/packages/node-experimental/test/sdk/scope.test.ts index c38437121959..6e1245c35aed 100644 --- a/packages/node-experimental/test/sdk/scope.test.ts +++ b/packages/node-experimental/test/sdk/scope.test.ts @@ -94,20 +94,6 @@ describe('Unit | Scope', () => { expect(scope.getClient()).toBe(client); }); - it('gets the correct isolationScope in _getIsolationScope', () => { - resetGlobals(); - - const scope = new Scope(); - const globalIsolationScope = getIsolationScope(); - - expect(scope['_getIsolationScope']()).toBe(globalIsolationScope); - - const customIsolationScope = new Scope(); - scope.isolationScope = customIsolationScope; - - expect(scope['_getIsolationScope']()).toBe(customIsolationScope); - }); - describe('prepareEvent', () => { it('works without any scope data', async () => { mockSdkInit(); @@ -205,6 +191,8 @@ describe('Unit | Scope', () => { integrations: [], }, scope, + undefined, + isolationScope, ); expect(eventProcessor1).toHaveBeenCalledTimes(1); diff --git a/packages/node-integration-tests/suites/public-api/scopes/initialScopes/scenario.ts b/packages/node-integration-tests/suites/public-api/scopes/initialScopes/scenario.ts new file mode 100644 index 000000000000..759206f761fc --- /dev/null +++ b/packages/node-integration-tests/suites/public-api/scopes/initialScopes/scenario.ts @@ -0,0 +1,23 @@ +import * as Sentry from '@sentry/node'; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', +}); + +const globalScope = Sentry.getGlobalScope(); +const isolationScope = Sentry.getIsolationScope(); +const currentScope = Sentry.getCurrentScope(); + +globalScope.setExtra('aa', 'aa'); +isolationScope.setExtra('bb', 'bb'); +currentScope.setExtra('cc', 'cc'); + +Sentry.captureMessage('outer_before'); + +Sentry.withScope(scope => { + scope.setExtra('dd', 'dd'); + Sentry.captureMessage('inner'); +}); + +Sentry.captureMessage('outer_after'); diff --git a/packages/node-integration-tests/suites/public-api/scopes/initialScopes/test.ts b/packages/node-integration-tests/suites/public-api/scopes/initialScopes/test.ts new file mode 100644 index 000000000000..069285c452c7 --- /dev/null +++ b/packages/node-integration-tests/suites/public-api/scopes/initialScopes/test.ts @@ -0,0 +1,31 @@ +import { TestEnv, assertSentryEvent } from '../../../../utils'; + +test('should apply scopes correctly', async () => { + const env = await TestEnv.init(__dirname); + const events = await env.getMultipleEnvelopeRequest({ count: 3 }); + + assertSentryEvent(events[0][2], { + message: 'outer_before', + extra: { + aa: 'aa', + bb: 'bb', + }, + }); + + assertSentryEvent(events[1][2], { + message: 'inner', + extra: { + aa: 'aa', + bb: 'bb', + cc: 'cc', + }, + }); + + assertSentryEvent(events[2][2], { + message: 'outer_after', + extra: { + aa: 'aa', + bb: 'bb', + }, + }); +}); diff --git a/packages/node-integration-tests/suites/public-api/scopes/isolationScope/scenario.ts b/packages/node-integration-tests/suites/public-api/scopes/isolationScope/scenario.ts new file mode 100644 index 000000000000..7d78c276880b --- /dev/null +++ b/packages/node-integration-tests/suites/public-api/scopes/isolationScope/scenario.ts @@ -0,0 +1,30 @@ +import * as Sentry from '@sentry/node'; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', +}); + +const globalScope = Sentry.getGlobalScope(); +const isolationScope = Sentry.getIsolationScope(); +const currentScope = Sentry.getCurrentScope(); + +globalScope.setExtra('aa', 'aa'); +isolationScope.setExtra('bb', 'bb'); +currentScope.setExtra('cc', 'cc'); + +Sentry.captureMessage('outer_before'); + +Sentry.withScope(scope => { + Sentry.getIsolationScope().setExtra('dd', 'dd'); + scope.setExtra('ee', 'ee'); + Sentry.captureMessage('inner'); +}); + +Sentry.runWithAsyncContext(() => { + Sentry.getIsolationScope().setExtra('ff', 'ff'); + Sentry.getCurrentScope().setExtra('gg', 'gg'); + Sentry.captureMessage('inner_async_context'); +}); + +Sentry.captureMessage('outer_after'); diff --git a/packages/node-integration-tests/suites/public-api/scopes/isolationScope/test.ts b/packages/node-integration-tests/suites/public-api/scopes/isolationScope/test.ts new file mode 100644 index 000000000000..4288d59bb799 --- /dev/null +++ b/packages/node-integration-tests/suites/public-api/scopes/isolationScope/test.ts @@ -0,0 +1,47 @@ +import { TestEnv, assertSentryEvent } from '../../../../utils'; + +test('should apply scopes correctly', async () => { + const env = await TestEnv.init(__dirname); + const events = await env.getMultipleEnvelopeRequest({ count: 4 }); + + assertSentryEvent(events[0][2], { + message: 'outer_before', + extra: { + aa: 'aa', + bb: 'bb', + }, + }); + + assertSentryEvent(events[1][2], { + message: 'inner', + extra: { + aa: 'aa', + bb: 'bb', + cc: 'cc', + dd: 'dd', + ee: 'ee', + }, + }); + + assertSentryEvent(events[2][2], { + message: 'inner_async_context', + extra: { + aa: 'aa', + bb: 'bb', + cc: 'cc', + dd: 'dd', + ff: 'ff', + gg: 'gg', + }, + }); + + assertSentryEvent(events[3][2], { + message: 'outer_after', + extra: { + aa: 'aa', + bb: 'bb', + cc: 'cc', + dd: 'dd', + }, + }); +}); diff --git a/packages/node/src/index.ts b/packages/node/src/index.ts index af73f34df7bc..46b1d6d742d4 100644 --- a/packages/node/src/index.ts +++ b/packages/node/src/index.ts @@ -43,6 +43,8 @@ export { getCurrentHub, getClient, getCurrentScope, + getGlobalScope, + getIsolationScope, Hub, lastEventId, makeMain, diff --git a/packages/opentelemetry/src/custom/client.ts b/packages/opentelemetry/src/custom/client.ts index 44dd0cf80f4f..abb6ba30972d 100644 --- a/packages/opentelemetry/src/custom/client.ts +++ b/packages/opentelemetry/src/custom/client.ts @@ -67,7 +67,12 @@ export function wrapClientClass< * Extends the base `_prepareEvent` so that we can properly handle `captureContext`. * This uses `Scope.clone()`, which we need to replace with `OpenTelemetryScope.clone()` for this client. */ - protected _prepareEvent(event: Event, hint: EventHint, scope?: Scope): PromiseLike { + protected _prepareEvent( + event: Event, + hint: EventHint, + scope?: Scope, + isolationScope?: Scope, + ): PromiseLike { let actualScope = scope; // Remove `captureContext` hint and instead clone already here diff --git a/packages/replay/src/util/prepareReplayEvent.ts b/packages/replay/src/util/prepareReplayEvent.ts index 4505b5b86f41..6b104c87c573 100644 --- a/packages/replay/src/util/prepareReplayEvent.ts +++ b/packages/replay/src/util/prepareReplayEvent.ts @@ -1,4 +1,5 @@ import type { Scope } from '@sentry/core'; +import { getIsolationScope } from '@sentry/core'; import { prepareEvent } from '@sentry/core'; import type { IntegrationIndex } from '@sentry/core/build/types/integration'; import type { Client, EventHint, ReplayEvent } from '@sentry/types'; @@ -34,6 +35,7 @@ export async function prepareReplayEvent({ eventHint, scope, client, + getIsolationScope(), )) as ReplayEvent | null; // If e.g. a global event processor returned null diff --git a/packages/serverless/src/index.ts b/packages/serverless/src/index.ts index c8086fc5d69e..77f355e1e8af 100644 --- a/packages/serverless/src/index.ts +++ b/packages/serverless/src/index.ts @@ -31,6 +31,8 @@ export { getCurrentHub, getClient, getCurrentScope, + getGlobalScope, + getIsolationScope, getHubFromCarrier, makeMain, setContext, diff --git a/packages/sveltekit/src/server/index.ts b/packages/sveltekit/src/server/index.ts index 560f839c9fe3..b5d7d64a58a3 100644 --- a/packages/sveltekit/src/server/index.ts +++ b/packages/sveltekit/src/server/index.ts @@ -24,6 +24,8 @@ export { getCurrentHub, getClient, getCurrentScope, + getGlobalScope, + getIsolationScope, Hub, makeMain, Scope, diff --git a/packages/types/src/hub.ts b/packages/types/src/hub.ts index ca18e088340a..f5ec2ea4fbb2 100644 --- a/packages/types/src/hub.ts +++ b/packages/types/src/hub.ts @@ -77,6 +77,12 @@ export interface Hub { /** Returns the scope of the top stack */ getScope(): Scope; + /** + * Get the currently active isolation scope. + * The isolation scope is used to isolate data between different hubs. + */ + getIsolationScope(): Scope; + /** * Captures an exception event and sends it to Sentry. * diff --git a/packages/vercel-edge/src/index.ts b/packages/vercel-edge/src/index.ts index 76219f4faafa..cbc7f6a89d7c 100644 --- a/packages/vercel-edge/src/index.ts +++ b/packages/vercel-edge/src/index.ts @@ -43,6 +43,8 @@ export { getCurrentHub, getClient, getCurrentScope, + getGlobalScope, + getIsolationScope, Hub, lastEventId, makeMain, From 141356860440ca96ae380212ef534dea8ee3e797 Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Thu, 21 Dec 2023 16:09:29 +0100 Subject: [PATCH 14/24] ref(integrations): Rewrite pluggable integrations to use functional style (#9936) Slowly getting there... --- packages/core/src/integration.ts | 26 +- packages/core/test/lib/integration.test.ts | 17 + packages/deno/src/integrations/deno-cron.ts | 6 +- packages/integrations/src/captureconsole.ts | 71 +- packages/integrations/src/contextlines.ts | 98 ++- packages/integrations/src/debug.ts | 110 ++-- packages/integrations/src/dedupe.ts | 84 +-- packages/integrations/src/extraerrordata.ts | 64 +- packages/integrations/src/httpclient.ts | 620 +++++++++--------- .../integrations/src/reportingobserver.ts | 100 ++- packages/integrations/src/rewriteframes.ts | 147 ++--- packages/integrations/src/sessiontiming.ts | 76 +-- packages/integrations/src/transaction.ts | 72 +- packages/integrations/test/debug.test.ts | 8 +- packages/integrations/test/dedupe.test.ts | 1 - .../integrations/test/extraerrordata.test.ts | 20 +- .../test/reportingobserver.test.ts | 37 +- .../integrations/test/rewriteframes.test.ts | 44 +- .../integrations/test/sessiontiming.test.ts | 12 +- .../integrations/test/transaction.test.ts | 2 +- packages/sveltekit/test/server/utils.test.ts | 27 +- 21 files changed, 731 insertions(+), 911 deletions(-) diff --git a/packages/core/src/integration.ts b/packages/core/src/integration.ts index 01a55081c04f..f867ad1d032a 100644 --- a/packages/core/src/integration.ts +++ b/packages/core/src/integration.ts @@ -175,14 +175,16 @@ function findIndex(arr: T[], callback: (item: T) => boolean): number { export function convertIntegrationFnToClass( name: string, fn: Fn, -): IntegrationClass< - Integration & { - setupOnce: (addGlobalEventProcessor?: (callback: EventProcessor) => void, getCurrentHub?: () => Hub) => void; - } -> { +): { + id: string; + new (...args: Parameters): Integration & + ReturnType & { + setupOnce: (addGlobalEventProcessor?: (callback: EventProcessor) => void, getCurrentHub?: () => Hub) => void; + }; +} { return Object.assign( // eslint-disable-next-line @typescript-eslint/no-explicit-any - function ConvertedIntegration(...rest: any[]) { + function ConvertedIntegration(...rest: Parameters) { return { // eslint-disable-next-line @typescript-eslint/no-empty-function setupOnce: () => {}, @@ -190,9 +192,11 @@ export function convertIntegrationFnToClass( }; }, { id: name }, - ) as unknown as IntegrationClass< - Integration & { - setupOnce: (addGlobalEventProcessor?: (callback: EventProcessor) => void, getCurrentHub?: () => Hub) => void; - } - >; + ) as unknown as { + id: string; + new (...args: Parameters): Integration & + ReturnType & { + setupOnce: (addGlobalEventProcessor?: (callback: EventProcessor) => void, getCurrentHub?: () => Hub) => void; + }; + }; } diff --git a/packages/core/test/lib/integration.test.ts b/packages/core/test/lib/integration.test.ts index 65bf30483d86..137a7dce4df3 100644 --- a/packages/core/test/lib/integration.test.ts +++ b/packages/core/test/lib/integration.test.ts @@ -670,6 +670,23 @@ describe('convertIntegrationFnToClass', () => { }); }); + it('works with options', () => { + const integrationFn = (_options: { num: number }) => ({ name: 'testName' }); + + const IntegrationClass = convertIntegrationFnToClass('testName', integrationFn); + + expect(IntegrationClass.id).toBe('testName'); + + // @ts-expect-error This should fail TS without options + new IntegrationClass(); + + const integration = new IntegrationClass({ num: 3 }); + expect(integration).toEqual({ + name: 'testName', + setupOnce: expect.any(Function), + }); + }); + it('works with integration hooks', () => { const setup = jest.fn(); const setupOnce = jest.fn(); diff --git a/packages/deno/src/integrations/deno-cron.ts b/packages/deno/src/integrations/deno-cron.ts index a3bbcbaf59bd..73c1bc1954fb 100644 --- a/packages/deno/src/integrations/deno-cron.ts +++ b/packages/deno/src/integrations/deno-cron.ts @@ -9,7 +9,7 @@ type CronParams = [string, string | Deno.CronSchedule, CronFn | CronOptions, Cro const INTEGRATION_NAME = 'DenoCron'; -const SETUP_CLIENTS: Client[] = []; +const SETUP_CLIENTS = new WeakMap(); const denoCronIntegration = (() => { return { @@ -37,7 +37,7 @@ const denoCronIntegration = (() => { } async function cronCalled(): Promise { - if (SETUP_CLIENTS.includes(getClient() as Client)) { + if (SETUP_CLIENTS.has(getClient() as Client)) { return; } @@ -55,7 +55,7 @@ const denoCronIntegration = (() => { }); }, setup(client) { - SETUP_CLIENTS.push(client); + SETUP_CLIENTS.set(client, true); }, }; }) satisfies IntegrationFn; diff --git a/packages/integrations/src/captureconsole.ts b/packages/integrations/src/captureconsole.ts index a1792573c9b1..4f37ecb1011a 100644 --- a/packages/integrations/src/captureconsole.ts +++ b/packages/integrations/src/captureconsole.ts @@ -1,5 +1,5 @@ -import { captureException, captureMessage, getClient, withScope } from '@sentry/core'; -import type { CaptureContext, Client, EventProcessor, Hub, Integration } from '@sentry/types'; +import { captureException, captureMessage, convertIntegrationFnToClass, getClient, withScope } from '@sentry/core'; +import type { CaptureContext, IntegrationFn } from '@sentry/types'; import { CONSOLE_LEVELS, GLOBAL_OBJ, @@ -9,55 +9,36 @@ import { severityLevelFromString, } from '@sentry/utils'; -/** Send Console API calls as Sentry Events */ -export class CaptureConsole implements Integration { - /** - * @inheritDoc - */ - public static id: string = 'CaptureConsole'; - - /** - * @inheritDoc - */ - public name: string; - - /** - * @inheritDoc - */ - private readonly _levels: readonly string[]; - - /** - * @inheritDoc - */ - public constructor(options: { levels?: string[] } = {}) { - this.name = CaptureConsole.id; - this._levels = options.levels || CONSOLE_LEVELS; - } - - /** - * @inheritDoc - */ - public setupOnce(_: (callback: EventProcessor) => void, _getCurrentHub: () => Hub): void { - // noop - } +interface CaptureConsoleOptions { + levels?: string[]; +} - /** @inheritdoc */ - public setup(client: Client): void { - if (!('console' in GLOBAL_OBJ)) { - return; - } +const INTEGRATION_NAME = 'CaptureConsole'; - const levels = this._levels; +const captureConsoleIntegration = ((options: CaptureConsoleOptions = {}) => { + const levels = options.levels || CONSOLE_LEVELS; - addConsoleInstrumentationHandler(({ args, level }) => { - if (getClient() !== client || !levels.includes(level)) { + return { + name: INTEGRATION_NAME, + setup(client) { + if (!('console' in GLOBAL_OBJ)) { return; } - consoleHandler(args, level); - }); - } -} + addConsoleInstrumentationHandler(({ args, level }) => { + if (getClient() !== client || !levels.includes(level)) { + return; + } + + consoleHandler(args, level); + }); + }, + }; +}) satisfies IntegrationFn; + +/** Send Console API calls as Sentry Events */ +// eslint-disable-next-line deprecation/deprecation +export const CaptureConsole = convertIntegrationFnToClass(INTEGRATION_NAME, captureConsoleIntegration); function consoleHandler(args: unknown[], level: string): void { const captureContext: CaptureContext = { diff --git a/packages/integrations/src/contextlines.ts b/packages/integrations/src/contextlines.ts index d716ca2fbb5f..656080ec3182 100644 --- a/packages/integrations/src/contextlines.ts +++ b/packages/integrations/src/contextlines.ts @@ -1,10 +1,13 @@ -import type { Event, Integration, StackFrame } from '@sentry/types'; +import { convertIntegrationFnToClass } from '@sentry/core'; +import type { Event, IntegrationFn, StackFrame } from '@sentry/types'; import { GLOBAL_OBJ, addContextToFrame, stripUrlQueryAndFragment } from '@sentry/utils'; const WINDOW = GLOBAL_OBJ as typeof GLOBAL_OBJ & Window; const DEFAULT_LINES_OF_CONTEXT = 7; +const INTEGRATION_NAME = 'ContextLines'; + interface ContextLinesOptions { /** * Sets the number of context lines for each frame when loading a file. @@ -15,6 +18,17 @@ interface ContextLinesOptions { frameContextLines?: number; } +const contextLinesIntegration: IntegrationFn = (options: ContextLinesOptions = {}) => { + const contextLines = options.frameContextLines != null ? options.frameContextLines : DEFAULT_LINES_OF_CONTEXT; + + return { + name: INTEGRATION_NAME, + processEvent(event) { + return addSourceContext(event, contextLines); + }, + }; +}; + /** * Collects source context lines around the lines of stackframes pointing to JS embedded in * the current page's HTML. @@ -26,73 +40,41 @@ interface ContextLinesOptions { * Use this integration if you have inline JS code in HTML pages that can't be accessed * by our backend (e.g. due to a login-protected page). */ -export class ContextLines implements Integration { - /** - * @inheritDoc - */ - public static id: string = 'ContextLines'; - - /** - * @inheritDoc - */ - public name: string; +// eslint-disable-next-line deprecation/deprecation +export const ContextLines = convertIntegrationFnToClass(INTEGRATION_NAME, contextLinesIntegration); - public constructor(private readonly _options: ContextLinesOptions = {}) { - this.name = ContextLines.id; +/** + * Processes an event and adds context lines. + */ +function addSourceContext(event: Event, contextLines: number): Event { + const doc = WINDOW.document; + const htmlFilename = WINDOW.location && stripUrlQueryAndFragment(WINDOW.location.href); + if (!doc || !htmlFilename) { + return event; } - /** - * @inheritDoc - */ - public setupOnce(_addGlobalEventProcessor: unknown, _getCurrentHub: unknown): void { - // noop + const exceptions = event.exception && event.exception.values; + if (!exceptions || !exceptions.length) { + return event; } - /** @inheritDoc */ - public processEvent(event: Event): Event { - return this.addSourceContext(event); + const html = doc.documentElement.innerHTML; + if (!html) { + return event; } - /** - * Processes an event and adds context lines. - * - * TODO (v8): Make this internal/private - */ - public addSourceContext(event: Event): Event { - const doc = WINDOW.document; - const htmlFilename = WINDOW.location && stripUrlQueryAndFragment(WINDOW.location.href); - if (!doc || !htmlFilename) { - return event; - } - - const exceptions = event.exception && event.exception.values; - if (!exceptions || !exceptions.length) { - return event; - } + const htmlLines = ['', '', ...html.split('\n'), '']; - const html = doc.documentElement.innerHTML; - if (!html) { - return event; + exceptions.forEach(exception => { + const stacktrace = exception.stacktrace; + if (stacktrace && stacktrace.frames) { + stacktrace.frames = stacktrace.frames.map(frame => + applySourceContextToFrame(frame, htmlLines, htmlFilename, contextLines), + ); } + }); - const htmlLines = ['', '', ...html.split('\n'), '']; - - exceptions.forEach(exception => { - const stacktrace = exception.stacktrace; - if (stacktrace && stacktrace.frames) { - stacktrace.frames = stacktrace.frames.map(frame => - applySourceContextToFrame( - frame, - htmlLines, - htmlFilename, - this._options.frameContextLines != null ? this._options.frameContextLines : DEFAULT_LINES_OF_CONTEXT, - ), - ); - } - }); - - return event; - } + return event; } /** diff --git a/packages/integrations/src/debug.ts b/packages/integrations/src/debug.ts index bb8ed8924254..6edb9939269a 100644 --- a/packages/integrations/src/debug.ts +++ b/packages/integrations/src/debug.ts @@ -1,6 +1,9 @@ -import type { Client, Event, EventHint, EventProcessor, Hub, Integration } from '@sentry/types'; +import { convertIntegrationFnToClass } from '@sentry/core'; +import type { Event, EventHint, IntegrationFn } from '@sentry/types'; import { consoleSandbox } from '@sentry/utils'; +const INTEGRATION_NAME = 'Debug'; + interface DebugOptions { /** Controls whether console output created by this integration should be stringified. Default: `false` */ stringify?: boolean; @@ -8,70 +11,49 @@ interface DebugOptions { debugger?: boolean; } -/** - * Integration to debug sent Sentry events. - * This integration should not be used in production - */ -export class Debug implements Integration { - /** - * @inheritDoc - */ - public static id: string = 'Debug'; - - /** - * @inheritDoc - */ - public name: string; - - private readonly _options: DebugOptions; - - public constructor(options?: DebugOptions) { - this.name = Debug.id; - - this._options = { - debugger: false, - stringify: false, - ...options, - }; - } - - /** - * @inheritDoc - */ - public setupOnce( - _addGlobalEventProcessor: (eventProcessor: EventProcessor) => void, - _getCurrentHub: () => Hub, - ): void { - // noop - } - - /** @inheritdoc */ - public setup(client: Client): void { - if (!client.on) { - return; - } - - client.on('beforeSendEvent', (event: Event, hint?: EventHint) => { - if (this._options.debugger) { - // eslint-disable-next-line no-debugger - debugger; +const debugIntegration = ((options: DebugOptions = {}) => { + const _options = { + debugger: false, + stringify: false, + ...options, + }; + + return { + name: INTEGRATION_NAME, + setup(client) { + if (!client.on) { + return; } - /* eslint-disable no-console */ - consoleSandbox(() => { - if (this._options.stringify) { - console.log(JSON.stringify(event, null, 2)); - if (hint && Object.keys(hint).length) { - console.log(JSON.stringify(hint, null, 2)); - } - } else { - console.log(event); - if (hint && Object.keys(hint).length) { - console.log(hint); - } + client.on('beforeSendEvent', (event: Event, hint?: EventHint) => { + if (_options.debugger) { + // eslint-disable-next-line no-debugger + debugger; } + + /* eslint-disable no-console */ + consoleSandbox(() => { + if (_options.stringify) { + console.log(JSON.stringify(event, null, 2)); + if (hint && Object.keys(hint).length) { + console.log(JSON.stringify(hint, null, 2)); + } + } else { + console.log(event); + if (hint && Object.keys(hint).length) { + console.log(hint); + } + } + }); + /* eslint-enable no-console */ }); - /* eslint-enable no-console */ - }); - } -} + }, + }; +}) satisfies IntegrationFn; + +/** + * Integration to debug sent Sentry events. + * This integration should not be used in production + */ +// eslint-disable-next-line deprecation/deprecation +export const Debug = convertIntegrationFnToClass(INTEGRATION_NAME, debugIntegration); diff --git a/packages/integrations/src/dedupe.ts b/packages/integrations/src/dedupe.ts index 464758d20dfc..1e3ae1be7626 100644 --- a/packages/integrations/src/dedupe.ts +++ b/packages/integrations/src/dedupe.ts @@ -1,59 +1,41 @@ -import type { Event, Exception, Integration, StackFrame } from '@sentry/types'; +import { convertIntegrationFnToClass } from '@sentry/core'; +import type { Event, Exception, IntegrationFn, StackFrame } from '@sentry/types'; import { logger } from '@sentry/utils'; import { DEBUG_BUILD } from './debug-build'; -/** Deduplication filter */ -export class Dedupe implements Integration { - /** - * @inheritDoc - */ - public static id: string = 'Dedupe'; - - /** - * @inheritDoc - */ - public name: string; - - /** - * @inheritDoc - */ - private _previousEvent?: Event; - - public constructor() { - this.name = Dedupe.id; - } - - /** @inheritDoc */ - public setupOnce(_addGlobalEventProcessor: unknown, _getCurrentHub: unknown): void { - // noop - } - - /** - * @inheritDoc - */ - public processEvent(currentEvent: Event): Event | null { - // We want to ignore any non-error type events, e.g. transactions or replays - // These should never be deduped, and also not be compared against as _previousEvent. - if (currentEvent.type) { - return currentEvent; - } +const INTEGRATION_NAME = 'Dedupe'; - // Juuust in case something goes wrong - try { - if (_shouldDropEvent(currentEvent, this._previousEvent)) { - DEBUG_BUILD && logger.warn('Event dropped due to being a duplicate of previously captured event.'); - return null; +const dedupeIntegration = (() => { + let previousEvent: Event | undefined; + + return { + name: INTEGRATION_NAME, + processEvent(currentEvent) { + // We want to ignore any non-error type events, e.g. transactions or replays + // These should never be deduped, and also not be compared against as _previousEvent. + if (currentEvent.type) { + return currentEvent; } - } catch (_oO) { - return (this._previousEvent = currentEvent); - } - return (this._previousEvent = currentEvent); - } -} + // Juuust in case something goes wrong + try { + if (_shouldDropEvent(currentEvent, previousEvent)) { + DEBUG_BUILD && logger.warn('Event dropped due to being a duplicate of previously captured event.'); + return null; + } + } catch (_oO) {} // eslint-disable-line no-empty + + return (previousEvent = currentEvent); + }, + }; +}) satisfies IntegrationFn; + +/** Deduplication filter */ +// eslint-disable-next-line deprecation/deprecation +export const Dedupe = convertIntegrationFnToClass(INTEGRATION_NAME, dedupeIntegration); -/** JSDoc */ +/** only exported for tests. */ export function _shouldDropEvent(currentEvent: Event, previousEvent?: Event): boolean { if (!previousEvent) { return false; @@ -70,7 +52,6 @@ export function _shouldDropEvent(currentEvent: Event, previousEvent?: Event): bo return false; } -/** JSDoc */ function _isSameMessageEvent(currentEvent: Event, previousEvent: Event): boolean { const currentMessage = currentEvent.message; const previousMessage = previousEvent.message; @@ -100,7 +81,6 @@ function _isSameMessageEvent(currentEvent: Event, previousEvent: Event): boolean return true; } -/** JSDoc */ function _isSameExceptionEvent(currentEvent: Event, previousEvent: Event): boolean { const previousException = _getExceptionFromEvent(previousEvent); const currentException = _getExceptionFromEvent(currentEvent); @@ -124,7 +104,6 @@ function _isSameExceptionEvent(currentEvent: Event, previousEvent: Event): boole return true; } -/** JSDoc */ function _isSameStacktrace(currentEvent: Event, previousEvent: Event): boolean { let currentFrames = _getFramesFromEvent(currentEvent); let previousFrames = _getFramesFromEvent(previousEvent); @@ -165,7 +144,6 @@ function _isSameStacktrace(currentEvent: Event, previousEvent: Event): boolean { return true; } -/** JSDoc */ function _isSameFingerprint(currentEvent: Event, previousEvent: Event): boolean { let currentFingerprint = currentEvent.fingerprint; let previousFingerprint = previousEvent.fingerprint; @@ -191,12 +169,10 @@ function _isSameFingerprint(currentEvent: Event, previousEvent: Event): boolean } } -/** JSDoc */ function _getExceptionFromEvent(event: Event): Exception | undefined { return event.exception && event.exception.values && event.exception.values[0]; } -/** JSDoc */ function _getFramesFromEvent(event: Event): StackFrame[] | undefined { const exception = event.exception; diff --git a/packages/integrations/src/extraerrordata.ts b/packages/integrations/src/extraerrordata.ts index 1c1b46e58c22..9d8a00f976cf 100644 --- a/packages/integrations/src/extraerrordata.ts +++ b/packages/integrations/src/extraerrordata.ts @@ -1,61 +1,29 @@ -import type { Contexts, Event, EventHint, ExtendedError, Integration } from '@sentry/types'; +import { convertIntegrationFnToClass } from '@sentry/core'; +import type { Contexts, Event, EventHint, ExtendedError, IntegrationFn } from '@sentry/types'; import { addNonEnumerableProperty, isError, isPlainObject, logger, normalize } from '@sentry/utils'; import { DEBUG_BUILD } from './debug-build'; -/** JSDoc */ +const INTEGRATION_NAME = 'ExtraErrorData'; + interface ExtraErrorDataOptions { depth: number; } -/** Patch toString calls to return proper name for wrapped functions */ -export class ExtraErrorData implements Integration { - /** - * @inheritDoc - */ - public static id: string = 'ExtraErrorData'; - - /** - * @inheritDoc - */ - public name: string; - - /** JSDoc */ - private readonly _options: ExtraErrorDataOptions; - - /** - * @inheritDoc - */ - public constructor(options?: Partial) { - this.name = ExtraErrorData.id; - - this._options = { - depth: 3, - ...options, - }; - } - - /** - * @inheritDoc - */ - public setupOnce(_addGlobalEventProcessor: unknown, _getCurrentHub: unknown): void { - // noop - } +const extraErrorDataIntegration = ((options: Partial = {}) => { + const depth = options.depth || 3; - /** @inheritDoc */ - public processEvent(event: Event, hint: EventHint): Event { - return this.enhanceEventWithErrorData(event, hint); - } + return { + name: INTEGRATION_NAME, + processEvent(event, hint) { + return _enhanceEventWithErrorData(event, hint, depth); + }, + }; +}) satisfies IntegrationFn; - /** - * Attaches extracted information from the Error object to extra field in the Event. - * - * TODO (v8): Drop this public function. - */ - public enhanceEventWithErrorData(event: Event, hint: EventHint = {}): Event { - return _enhanceEventWithErrorData(event, hint, this._options.depth); - } -} +/** Extract additional data for from original exceptions. */ +// eslint-disable-next-line deprecation/deprecation +export const ExtraErrorData = convertIntegrationFnToClass(INTEGRATION_NAME, extraErrorDataIntegration); function _enhanceEventWithErrorData(event: Event, hint: EventHint = {}, depth: number): Event { if (!hint.originalException || !isError(hint.originalException)) { diff --git a/packages/integrations/src/httpclient.ts b/packages/integrations/src/httpclient.ts index bcb4429b7aeb..74142487473a 100644 --- a/packages/integrations/src/httpclient.ts +++ b/packages/integrations/src/httpclient.ts @@ -1,12 +1,5 @@ -import { captureEvent, getClient, isSentryRequestUrl } from '@sentry/core'; -import type { - Client, - Event as SentryEvent, - EventProcessor, - Hub, - Integration, - SentryWrappedXMLHttpRequest, -} from '@sentry/types'; +import { captureEvent, convertIntegrationFnToClass, getClient, isSentryRequestUrl } from '@sentry/core'; +import type { Client, Event as SentryEvent, IntegrationFn, SentryWrappedXMLHttpRequest } from '@sentry/types'; import { GLOBAL_OBJ, SENTRY_XHR_DATA_KEY, @@ -22,6 +15,8 @@ import { DEBUG_BUILD } from './debug-build'; export type HttpStatusCodeRange = [number, number] | number; export type HttpRequestTarget = string | RegExp; +const INTEGRATION_NAME = 'HttpClient'; + interface HttpClientOptions { /** * HTTP status codes that should be considered failed. @@ -31,7 +26,7 @@ interface HttpClientOptions { * Example: [[500, 505], 507] * Default: [[500, 599]] */ - failedRequestStatusCodes?: HttpStatusCodeRange[]; + failedRequestStatusCodes: HttpStatusCodeRange[]; /** * Targets to track for failed requests. @@ -40,375 +35,360 @@ interface HttpClientOptions { * Example: ['http://localhost', /api\/.*\/] * Default: [/.*\/] */ - failedRequestTargets?: HttpRequestTarget[]; + failedRequestTargets: HttpRequestTarget[]; } -/** HTTPClient integration creates events for failed client side HTTP requests. */ -export class HttpClient implements Integration { - /** - * @inheritDoc - */ - public static id: string = 'HttpClient'; - - /** - * @inheritDoc - */ - public name: string; - - private readonly _options: HttpClientOptions; - - /** - * @inheritDoc - * - * @param options - */ - public constructor(options?: Partial) { - this.name = HttpClient.id; - this._options = { - failedRequestStatusCodes: [[500, 599]], - failedRequestTargets: [/.*/], - ...options, - }; - } - - /** - * @inheritDoc - * - * @param options - */ - public setupOnce(_: (callback: EventProcessor) => void, _getCurrentHub: () => Hub): void { - // noop - } - - /** @inheritdoc */ - public setup(client: Client): void { - this._wrapFetch(client); - this._wrapXHR(client); - } - - /** - * Interceptor function for fetch requests - * - * @param requestInfo The Fetch API request info - * @param response The Fetch API response - * @param requestInit The request init object - */ - private _fetchResponseHandler(requestInfo: RequestInfo, response: Response, requestInit?: RequestInit): void { - if (this._shouldCaptureResponse(response.status, response.url)) { - const request = _getRequest(requestInfo, requestInit); - - let requestHeaders, responseHeaders, requestCookies, responseCookies; - - if (_shouldSendDefaultPii()) { - [{ headers: requestHeaders, cookies: requestCookies }, { headers: responseHeaders, cookies: responseCookies }] = - [ - { cookieHeader: 'Cookie', obj: request }, - { cookieHeader: 'Set-Cookie', obj: response }, - ].map(({ cookieHeader, obj }) => { - const headers = this._extractFetchHeaders(obj.headers); - let cookies; - - try { - const cookieString = headers[cookieHeader] || headers[cookieHeader.toLowerCase()] || undefined; - - if (cookieString) { - cookies = this._parseCookieString(cookieString); - } - } catch (e) { - DEBUG_BUILD && logger.log(`Could not extract cookies from header ${cookieHeader}`); - } - - return { - headers, - cookies, - }; - }); - } - - const event = this._createEvent({ - url: request.url, - method: request.method, - status: response.status, - requestHeaders, - responseHeaders, - requestCookies, - responseCookies, - }); - - captureEvent(event); - } - } +const httpClientIntegration = ((options: Partial = {}) => { + const _options: HttpClientOptions = { + failedRequestStatusCodes: [[500, 599]], + failedRequestTargets: [/.*/], + ...options, + }; + + return { + name: INTEGRATION_NAME, + setup(client): void { + _wrapFetch(client, _options); + _wrapXHR(client, _options); + }, + }; +}) satisfies IntegrationFn; - /** - * Interceptor function for XHR requests - * - * @param xhr The XHR request - * @param method The HTTP method - * @param headers The HTTP headers - */ - private _xhrResponseHandler(xhr: XMLHttpRequest, method: string, headers: Record): void { - if (this._shouldCaptureResponse(xhr.status, xhr.responseURL)) { - let requestHeaders, responseCookies, responseHeaders; +/** HTTPClient integration creates events for failed client side HTTP requests. */ +// eslint-disable-next-line deprecation/deprecation +export const HttpClient = convertIntegrationFnToClass(INTEGRATION_NAME, httpClientIntegration); + +/** + * Interceptor function for fetch requests + * + * @param requestInfo The Fetch API request info + * @param response The Fetch API response + * @param requestInit The request init object + */ +function _fetchResponseHandler( + options: HttpClientOptions, + requestInfo: RequestInfo, + response: Response, + requestInit?: RequestInit, +): void { + if (_shouldCaptureResponse(options, response.status, response.url)) { + const request = _getRequest(requestInfo, requestInit); + + let requestHeaders, responseHeaders, requestCookies, responseCookies; + + if (_shouldSendDefaultPii()) { + [{ headers: requestHeaders, cookies: requestCookies }, { headers: responseHeaders, cookies: responseCookies }] = [ + { cookieHeader: 'Cookie', obj: request }, + { cookieHeader: 'Set-Cookie', obj: response }, + ].map(({ cookieHeader, obj }) => { + const headers = _extractFetchHeaders(obj.headers); + let cookies; - if (_shouldSendDefaultPii()) { try { - const cookieString = xhr.getResponseHeader('Set-Cookie') || xhr.getResponseHeader('set-cookie') || undefined; + const cookieString = headers[cookieHeader] || headers[cookieHeader.toLowerCase()] || undefined; if (cookieString) { - responseCookies = this._parseCookieString(cookieString); + cookies = _parseCookieString(cookieString); } } catch (e) { - DEBUG_BUILD && logger.log('Could not extract cookies from response headers'); - } - - try { - responseHeaders = this._getXHRResponseHeaders(xhr); - } catch (e) { - DEBUG_BUILD && logger.log('Could not extract headers from response'); + DEBUG_BUILD && logger.log(`Could not extract cookies from header ${cookieHeader}`); } - requestHeaders = headers; - } - - const event = this._createEvent({ - url: xhr.responseURL, - method, - status: xhr.status, - requestHeaders, - // Can't access request cookies from XHR - responseHeaders, - responseCookies, + return { + headers, + cookies, + }; }); - - captureEvent(event); } + + const event = _createEvent({ + url: request.url, + method: request.method, + status: response.status, + requestHeaders, + responseHeaders, + requestCookies, + responseCookies, + }); + + captureEvent(event); } +} - /** - * Extracts response size from `Content-Length` header when possible - * - * @param headers - * @returns The response size in bytes or undefined - */ - private _getResponseSizeFromHeaders(headers?: Record): number | undefined { - if (headers) { - const contentLength = headers['Content-Length'] || headers['content-length']; +/** + * Interceptor function for XHR requests + * + * @param xhr The XHR request + * @param method The HTTP method + * @param headers The HTTP headers + */ +function _xhrResponseHandler( + options: HttpClientOptions, + xhr: XMLHttpRequest, + method: string, + headers: Record, +): void { + if (_shouldCaptureResponse(options, xhr.status, xhr.responseURL)) { + let requestHeaders, responseCookies, responseHeaders; + + if (_shouldSendDefaultPii()) { + try { + const cookieString = xhr.getResponseHeader('Set-Cookie') || xhr.getResponseHeader('set-cookie') || undefined; + + if (cookieString) { + responseCookies = _parseCookieString(cookieString); + } + } catch (e) { + DEBUG_BUILD && logger.log('Could not extract cookies from response headers'); + } - if (contentLength) { - return parseInt(contentLength, 10); + try { + responseHeaders = _getXHRResponseHeaders(xhr); + } catch (e) { + DEBUG_BUILD && logger.log('Could not extract headers from response'); } + + requestHeaders = headers; } - return undefined; + const event = _createEvent({ + url: xhr.responseURL, + method, + status: xhr.status, + requestHeaders, + // Can't access request cookies from XHR + responseHeaders, + responseCookies, + }); + + captureEvent(event); } +} - /** - * Creates an object containing cookies from the given cookie string - * - * @param cookieString The cookie string to parse - * @returns The parsed cookies - */ - private _parseCookieString(cookieString: string): Record { - return cookieString.split('; ').reduce((acc: Record, cookie: string) => { - const [key, value] = cookie.split('='); - acc[key] = value; - return acc; - }, {}); +/** + * Extracts response size from `Content-Length` header when possible + * + * @param headers + * @returns The response size in bytes or undefined + */ +function _getResponseSizeFromHeaders(headers?: Record): number | undefined { + if (headers) { + const contentLength = headers['Content-Length'] || headers['content-length']; + + if (contentLength) { + return parseInt(contentLength, 10); + } } - /** - * Extracts the headers as an object from the given Fetch API request or response object - * - * @param headers The headers to extract - * @returns The extracted headers as an object - */ - private _extractFetchHeaders(headers: Headers): Record { - const result: Record = {}; + return undefined; +} - headers.forEach((value, key) => { - result[key] = value; - }); +/** + * Creates an object containing cookies from the given cookie string + * + * @param cookieString The cookie string to parse + * @returns The parsed cookies + */ +function _parseCookieString(cookieString: string): Record { + return cookieString.split('; ').reduce((acc: Record, cookie: string) => { + const [key, value] = cookie.split('='); + acc[key] = value; + return acc; + }, {}); +} - return result; - } +/** + * Extracts the headers as an object from the given Fetch API request or response object + * + * @param headers The headers to extract + * @returns The extracted headers as an object + */ +function _extractFetchHeaders(headers: Headers): Record { + const result: Record = {}; - /** - * Extracts the response headers as an object from the given XHR object - * - * @param xhr The XHR object to extract the response headers from - * @returns The response headers as an object - */ - private _getXHRResponseHeaders(xhr: XMLHttpRequest): Record { - const headers = xhr.getAllResponseHeaders(); + headers.forEach((value, key) => { + result[key] = value; + }); - if (!headers) { - return {}; - } + return result; +} - return headers.split('\r\n').reduce((acc: Record, line: string) => { - const [key, value] = line.split(': '); - acc[key] = value; - return acc; - }, {}); +/** + * Extracts the response headers as an object from the given XHR object + * + * @param xhr The XHR object to extract the response headers from + * @returns The response headers as an object + */ +function _getXHRResponseHeaders(xhr: XMLHttpRequest): Record { + const headers = xhr.getAllResponseHeaders(); + + if (!headers) { + return {}; } - /** - * Checks if the given target url is in the given list of targets - * - * @param target The target url to check - * @returns true if the target url is in the given list of targets, false otherwise - */ - private _isInGivenRequestTargets(target: string): boolean { - if (!this._options.failedRequestTargets) { - return false; - } + return headers.split('\r\n').reduce((acc: Record, line: string) => { + const [key, value] = line.split(': '); + acc[key] = value; + return acc; + }, {}); +} - return this._options.failedRequestTargets.some((givenRequestTarget: HttpRequestTarget) => { - if (typeof givenRequestTarget === 'string') { - return target.includes(givenRequestTarget); - } +/** + * Checks if the given target url is in the given list of targets + * + * @param target The target url to check + * @returns true if the target url is in the given list of targets, false otherwise + */ +function _isInGivenRequestTargets( + failedRequestTargets: HttpClientOptions['failedRequestTargets'], + target: string, +): boolean { + return failedRequestTargets.some((givenRequestTarget: HttpRequestTarget) => { + if (typeof givenRequestTarget === 'string') { + return target.includes(givenRequestTarget); + } - return givenRequestTarget.test(target); - }); - } + return givenRequestTarget.test(target); + }); +} - /** - * Checks if the given status code is in the given range - * - * @param status The status code to check - * @returns true if the status code is in the given range, false otherwise - */ - private _isInGivenStatusRanges(status: number): boolean { - if (!this._options.failedRequestStatusCodes) { - return false; +/** + * Checks if the given status code is in the given range + * + * @param status The status code to check + * @returns true if the status code is in the given range, false otherwise + */ +function _isInGivenStatusRanges( + failedRequestStatusCodes: HttpClientOptions['failedRequestStatusCodes'], + status: number, +): boolean { + return failedRequestStatusCodes.some((range: HttpStatusCodeRange) => { + if (typeof range === 'number') { + return range === status; } - return this._options.failedRequestStatusCodes.some((range: HttpStatusCodeRange) => { - if (typeof range === 'number') { - return range === status; - } + return status >= range[0] && status <= range[1]; + }); +} - return status >= range[0] && status <= range[1]; - }); +/** + * Wraps `fetch` function to capture request and response data + */ +function _wrapFetch(client: Client, options: HttpClientOptions): void { + if (!supportsNativeFetch()) { + return; } - /** - * Wraps `fetch` function to capture request and response data - */ - private _wrapFetch(client: Client): void { - if (!supportsNativeFetch()) { + addFetchInstrumentationHandler(handlerData => { + if (getClient() !== client) { return; } - addFetchInstrumentationHandler(handlerData => { - if (getClient() !== client) { - return; - } + const { response, args } = handlerData; + const [requestInfo, requestInit] = args as [RequestInfo, RequestInit | undefined]; - const { response, args } = handlerData; - const [requestInfo, requestInit] = args as [RequestInfo, RequestInit | undefined]; + if (!response) { + return; + } - if (!response) { - return; - } + _fetchResponseHandler(options, requestInfo, response as Response, requestInit); + }); +} - this._fetchResponseHandler(requestInfo, response as Response, requestInit); - }); +/** + * Wraps XMLHttpRequest to capture request and response data + */ +function _wrapXHR(client: Client, options: HttpClientOptions): void { + if (!('XMLHttpRequest' in GLOBAL_OBJ)) { + return; } - /** - * Wraps XMLHttpRequest to capture request and response data - */ - private _wrapXHR(client: Client): void { - if (!('XMLHttpRequest' in GLOBAL_OBJ)) { + addXhrInstrumentationHandler(handlerData => { + if (getClient() !== client) { return; } - addXhrInstrumentationHandler(handlerData => { - if (getClient() !== client) { - return; - } - - const xhr = handlerData.xhr as SentryWrappedXMLHttpRequest & XMLHttpRequest; + const xhr = handlerData.xhr as SentryWrappedXMLHttpRequest & XMLHttpRequest; - const sentryXhrData = xhr[SENTRY_XHR_DATA_KEY]; + const sentryXhrData = xhr[SENTRY_XHR_DATA_KEY]; - if (!sentryXhrData) { - return; - } + if (!sentryXhrData) { + return; + } - const { method, request_headers: headers } = sentryXhrData; + const { method, request_headers: headers } = sentryXhrData; - try { - this._xhrResponseHandler(xhr, method, headers); - } catch (e) { - DEBUG_BUILD && logger.warn('Error while extracting response event form XHR response', e); - } - }); - } + try { + _xhrResponseHandler(options, xhr, method, headers); + } catch (e) { + DEBUG_BUILD && logger.warn('Error while extracting response event form XHR response', e); + } + }); +} - /** - * Checks whether to capture given response as an event - * - * @param status response status code - * @param url response url - */ - private _shouldCaptureResponse(status: number, url: string): boolean { - return ( - this._isInGivenStatusRanges(status) && this._isInGivenRequestTargets(url) && !isSentryRequestUrl(url, getClient()) - ); - } +/** + * Checks whether to capture given response as an event + * + * @param status response status code + * @param url response url + */ +function _shouldCaptureResponse(options: HttpClientOptions, status: number, url: string): boolean { + return ( + _isInGivenStatusRanges(options.failedRequestStatusCodes, status) && + _isInGivenRequestTargets(options.failedRequestTargets, url) && + !isSentryRequestUrl(url, getClient()) + ); +} - /** - * Creates a synthetic Sentry event from given response data - * - * @param data response data - * @returns event - */ - private _createEvent(data: { - url: string; - method: string; - status: number; - responseHeaders?: Record; - responseCookies?: Record; - requestHeaders?: Record; - requestCookies?: Record; - }): SentryEvent { - const message = `HTTP Client Error with status code: ${data.status}`; - - const event: SentryEvent = { - message, - exception: { - values: [ - { - type: 'Error', - value: message, - }, - ], - }, - request: { - url: data.url, - method: data.method, - headers: data.requestHeaders, - cookies: data.requestCookies, - }, - contexts: { - response: { - status_code: data.status, - headers: data.responseHeaders, - cookies: data.responseCookies, - body_size: this._getResponseSizeFromHeaders(data.responseHeaders), +/** + * Creates a synthetic Sentry event from given response data + * + * @param data response data + * @returns event + */ +function _createEvent(data: { + url: string; + method: string; + status: number; + responseHeaders?: Record; + responseCookies?: Record; + requestHeaders?: Record; + requestCookies?: Record; +}): SentryEvent { + const message = `HTTP Client Error with status code: ${data.status}`; + + const event: SentryEvent = { + message, + exception: { + values: [ + { + type: 'Error', + value: message, }, + ], + }, + request: { + url: data.url, + method: data.method, + headers: data.requestHeaders, + cookies: data.requestCookies, + }, + contexts: { + response: { + status_code: data.status, + headers: data.responseHeaders, + cookies: data.responseCookies, + body_size: _getResponseSizeFromHeaders(data.responseHeaders), }, - }; + }, + }; - addExceptionMechanism(event, { - type: 'http.client', - handled: false, - }); + addExceptionMechanism(event, { + type: 'http.client', + handled: false, + }); - return event; - } + return event; } function _getRequest(requestInfo: RequestInfo, requestInit?: RequestInit): Request { diff --git a/packages/integrations/src/reportingobserver.ts b/packages/integrations/src/reportingobserver.ts index dbcae7f014e2..cd8c28b54f8c 100644 --- a/packages/integrations/src/reportingobserver.ts +++ b/packages/integrations/src/reportingobserver.ts @@ -1,9 +1,11 @@ -import { captureMessage, getClient, withScope } from '@sentry/core'; -import type { Client, EventProcessor, Hub, Integration } from '@sentry/types'; +import { captureMessage, convertIntegrationFnToClass, getClient, withScope } from '@sentry/core'; +import type { Client, IntegrationFn } from '@sentry/types'; import { GLOBAL_OBJ, supportsReportingObserver } from '@sentry/utils'; const WINDOW = GLOBAL_OBJ as typeof GLOBAL_OBJ & Window; +const INTEGRATION_NAME = 'ReportingObserver'; + interface Report { [key: string]: unknown; type: ReportTypes; @@ -40,67 +42,17 @@ interface InterventionReportBody { columnNumber?: number; } -const SETUP_CLIENTS: Client[] = []; - -/** Reporting API integration - https://w3c.github.io/reporting/ */ -export class ReportingObserver implements Integration { - /** - * @inheritDoc - */ - public static id: string = 'ReportingObserver'; - - /** - * @inheritDoc - */ - public readonly name: string; - - private readonly _types: ReportTypes[]; - - /** - * @inheritDoc - */ - public constructor( - options: { - types?: ReportTypes[]; - } = {}, - ) { - this.name = ReportingObserver.id; - - this._types = options.types || ['crash', 'deprecation', 'intervention']; - } - - /** - * @inheritDoc - */ - public setupOnce(_: (callback: EventProcessor) => void, _getCurrentHub: () => Hub): void { - if (!supportsReportingObserver()) { - return; - } +interface ReportingObserverOptions { + types?: ReportTypes[]; +} - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-explicit-any - const observer = new (WINDOW as any).ReportingObserver(this.handler.bind(this), { - buffered: true, - types: this._types, - }); +const SETUP_CLIENTS = new WeakMap(); - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - observer.observe(); - } +const reportingObserverIntegration = ((options: ReportingObserverOptions = {}) => { + const types = options.types || ['crash', 'deprecation', 'intervention']; - /** @inheritdoc */ - public setup(client: Client): void { - if (!supportsReportingObserver()) { - return; - } - - SETUP_CLIENTS.push(client); - } - - /** - * @inheritDoc - */ - public handler(reports: Report[]): void { - if (!SETUP_CLIENTS.includes(getClient() as Client)) { + function handler(reports: Report[]): void { + if (!SETUP_CLIENTS.has(getClient() as Client)) { return; } @@ -138,4 +90,30 @@ export class ReportingObserver implements Integration { }); } } -} + + return { + name: INTEGRATION_NAME, + setupOnce() { + if (!supportsReportingObserver()) { + return; + } + + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-explicit-any + const observer = new (WINDOW as any).ReportingObserver(handler, { + buffered: true, + types, + }); + + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + observer.observe(); + }, + + setup(client): void { + SETUP_CLIENTS.set(client, true); + }, + }; +}) satisfies IntegrationFn; + +/** Reporting API integration - https://w3c.github.io/reporting/ */ +// eslint-disable-next-line deprecation/deprecation +export const ReportingObserver = convertIntegrationFnToClass(INTEGRATION_NAME, reportingObserverIntegration); diff --git a/packages/integrations/src/rewriteframes.ts b/packages/integrations/src/rewriteframes.ts index bcb6eeca56f2..d82bde5728c6 100644 --- a/packages/integrations/src/rewriteframes.ts +++ b/packages/integrations/src/rewriteframes.ts @@ -1,98 +1,47 @@ -import type { Event, Integration, StackFrame, Stacktrace } from '@sentry/types'; +import { convertIntegrationFnToClass } from '@sentry/core'; +import type { Event, IntegrationFn, StackFrame, Stacktrace } from '@sentry/types'; import { basename, relative } from '@sentry/utils'; type StackFrameIteratee = (frame: StackFrame) => StackFrame; -/** Rewrite event frames paths */ -export class RewriteFrames implements Integration { - /** - * @inheritDoc - */ - public static id: string = 'RewriteFrames'; - - /** - * @inheritDoc - */ - public name: string; - - /** - * @inheritDoc - */ - private readonly _root?: string; - - /** - * @inheritDoc - */ - private readonly _prefix: string; - - /** - * @inheritDoc - */ - public constructor(options: { root?: string; prefix?: string; iteratee?: StackFrameIteratee } = {}) { - this.name = RewriteFrames.id; - - if (options.root) { - this._root = options.root; - } - this._prefix = options.prefix || 'app:///'; - if (options.iteratee) { - this._iteratee = options.iteratee; - } - } - - /** - * @inheritDoc - */ - public setupOnce(_addGlobalEventProcessor: unknown, _getCurrentHub: unknown): void { - // noop - } - - /** @inheritDoc */ - public processEvent(event: Event): Event { - return this.process(event); - } - - /** - * TODO (v8): Make this private/internal - */ - public process(originalEvent: Event): Event { - let processedEvent = originalEvent; +const INTEGRATION_NAME = 'RewriteFrames'; - if (originalEvent.exception && Array.isArray(originalEvent.exception.values)) { - processedEvent = this._processExceptionsEvent(processedEvent); - } +interface RewriteFramesOptions { + root?: string; + prefix?: string; + iteratee?: StackFrameIteratee; +} - return processedEvent; - } +const rewriteFramesIntegration = ((options: RewriteFramesOptions = {}) => { + const root = options.root; + const prefix = options.prefix || 'app:///'; - /** - * @inheritDoc - */ - private readonly _iteratee: StackFrameIteratee = (frame: StackFrame) => { - if (!frame.filename) { + const iteratee: StackFrameIteratee = + options.iteratee || + ((frame: StackFrame) => { + if (!frame.filename) { + return frame; + } + // Determine if this is a Windows frame by checking for a Windows-style prefix such as `C:\` + const isWindowsFrame = + /^[a-zA-Z]:\\/.test(frame.filename) || + // or the presence of a backslash without a forward slash (which are not allowed on Windows) + (frame.filename.includes('\\') && !frame.filename.includes('/')); + // Check if the frame filename begins with `/` + const startsWithSlash = /^\//.test(frame.filename); + if (isWindowsFrame || startsWithSlash) { + const filename = isWindowsFrame + ? frame.filename + .replace(/^[a-zA-Z]:/, '') // remove Windows-style prefix + .replace(/\\/g, '/') // replace all `\\` instances with `/` + : frame.filename; + const base = root ? relative(root, filename) : basename(filename); + frame.filename = `${prefix}${base}`; + } return frame; - } - // Determine if this is a Windows frame by checking for a Windows-style prefix such as `C:\` - const isWindowsFrame = - /^[a-zA-Z]:\\/.test(frame.filename) || - // or the presence of a backslash without a forward slash (which are not allowed on Windows) - (frame.filename.includes('\\') && !frame.filename.includes('/')); - // Check if the frame filename begins with `/` - const startsWithSlash = /^\//.test(frame.filename); - if (isWindowsFrame || startsWithSlash) { - const filename = isWindowsFrame - ? frame.filename - .replace(/^[a-zA-Z]:/, '') // remove Windows-style prefix - .replace(/\\/g, '/') // replace all `\\` instances with `/` - : frame.filename; - const base = this._root ? relative(this._root, filename) : basename(filename); - frame.filename = `${this._prefix}${base}`; - } - return frame; - }; + }); - /** JSDoc */ - private _processExceptionsEvent(event: Event): Event { + function _processExceptionsEvent(event: Event): Event { try { return { ...event, @@ -102,7 +51,7 @@ export class RewriteFrames implements Integration { // eslint-disable-next-line @typescript-eslint/no-non-null-assertion values: event.exception!.values!.map(value => ({ ...value, - ...(value.stacktrace && { stacktrace: this._processStacktrace(value.stacktrace) }), + ...(value.stacktrace && { stacktrace: _processStacktrace(value.stacktrace) }), })), }, }; @@ -111,11 +60,27 @@ export class RewriteFrames implements Integration { } } - /** JSDoc */ - private _processStacktrace(stacktrace?: Stacktrace): Stacktrace { + function _processStacktrace(stacktrace?: Stacktrace): Stacktrace { return { ...stacktrace, - frames: stacktrace && stacktrace.frames && stacktrace.frames.map(f => this._iteratee(f)), + frames: stacktrace && stacktrace.frames && stacktrace.frames.map(f => iteratee(f)), }; } -} + + return { + name: INTEGRATION_NAME, + processEvent(originalEvent) { + let processedEvent = originalEvent; + + if (originalEvent.exception && Array.isArray(originalEvent.exception.values)) { + processedEvent = _processExceptionsEvent(processedEvent); + } + + return processedEvent; + }, + }; +}) satisfies IntegrationFn; + +/** Rewrite event frames paths */ +// eslint-disable-next-line deprecation/deprecation +export const RewriteFrames = convertIntegrationFnToClass(INTEGRATION_NAME, rewriteFramesIntegration); diff --git a/packages/integrations/src/sessiontiming.ts b/packages/integrations/src/sessiontiming.ts index 6b85b6ff9d56..4398d170a981 100644 --- a/packages/integrations/src/sessiontiming.ts +++ b/packages/integrations/src/sessiontiming.ts @@ -1,51 +1,29 @@ -import type { Event, Integration } from '@sentry/types'; +import { convertIntegrationFnToClass } from '@sentry/core'; +import type { IntegrationFn } from '@sentry/types'; + +const INTEGRATION_NAME = 'SessionTiming'; + +const sessionTimingIntegration = (() => { + const startTime = Date.now(); + + return { + name: INTEGRATION_NAME, + processEvent(event) { + const now = Date.now(); + + return { + ...event, + extra: { + ...event.extra, + ['session:start']: startTime, + ['session:duration']: now - startTime, + ['session:end']: now, + }, + }; + }, + }; +}) satisfies IntegrationFn; /** This function adds duration since Sentry was initialized till the time event was sent */ -export class SessionTiming implements Integration { - /** - * @inheritDoc - */ - public static id: string = 'SessionTiming'; - - /** - * @inheritDoc - */ - public name: string; - - /** Exact time Client was initialized expressed in milliseconds since Unix Epoch. */ - protected readonly _startTime: number; - - public constructor() { - this.name = SessionTiming.id; - this._startTime = Date.now(); - } - - /** - * @inheritDoc - */ - public setupOnce(_addGlobalEventProcessor: unknown, _getCurrentHub: unknown): void { - // noop - } - - /** @inheritDoc */ - public processEvent(event: Event): Event { - return this.process(event); - } - - /** - * TODO (v8): make this private/internal - */ - public process(event: Event): Event { - const now = Date.now(); - - return { - ...event, - extra: { - ...event.extra, - ['session:start']: this._startTime, - ['session:duration']: now - this._startTime, - ['session:end']: now, - }, - }; - } -} +// eslint-disable-next-line deprecation/deprecation +export const SessionTiming = convertIntegrationFnToClass(INTEGRATION_NAME, sessionTimingIntegration); diff --git a/packages/integrations/src/transaction.ts b/packages/integrations/src/transaction.ts index 28bb90b0f91b..c44c94c7fe06 100644 --- a/packages/integrations/src/transaction.ts +++ b/packages/integrations/src/transaction.ts @@ -1,52 +1,32 @@ -import type { Event, Integration, StackFrame } from '@sentry/types'; +import { convertIntegrationFnToClass } from '@sentry/core'; +import type { Event, IntegrationFn, StackFrame } from '@sentry/types'; -/** Add node transaction to the event */ -export class Transaction implements Integration { - /** - * @inheritDoc - */ - public static id: string = 'Transaction'; - - /** - * @inheritDoc - */ - public name: string; - - public constructor() { - this.name = Transaction.id; - } - - /** - * @inheritDoc - */ - public setupOnce(_addGlobalEventProcessor: unknown, _getCurrentHub: unknown): void { - // noop - } - - /** @inheritDoc */ - public processEvent(event: Event): Event { - return this.process(event); - } - - /** - * TODO (v8): Make this private/internal - */ - public process(event: Event): Event { - const frames = _getFramesFromEvent(event); - - // use for loop so we don't have to reverse whole frames array - for (let i = frames.length - 1; i >= 0; i--) { - const frame = frames[i]; - - if (frame.in_app === true) { - event.transaction = _getTransaction(frame); - break; +const INTEGRATION_NAME = 'Transaction'; + +const transactionIntegration = (() => { + return { + name: INTEGRATION_NAME, + processEvent(event) { + const frames = _getFramesFromEvent(event); + + // use for loop so we don't have to reverse whole frames array + for (let i = frames.length - 1; i >= 0; i--) { + const frame = frames[i]; + + if (frame.in_app === true) { + event.transaction = _getTransaction(frame); + break; + } } - } - return event; - } -} + return event; + }, + }; +}) satisfies IntegrationFn; + +/** Add node transaction to the event */ +// eslint-disable-next-line deprecation/deprecation +export const Transaction = convertIntegrationFnToClass(INTEGRATION_NAME, transactionIntegration); function _getFramesFromEvent(event: Event): StackFrame[] { const exception = event.exception && event.exception.values && event.exception.values[0]; diff --git a/packages/integrations/test/debug.test.ts b/packages/integrations/test/debug.test.ts index eefd9c8b9240..1cb952f26a5a 100644 --- a/packages/integrations/test/debug.test.ts +++ b/packages/integrations/test/debug.test.ts @@ -1,8 +1,12 @@ -import type { Client, Event, EventHint, Hub, Integration } from '@sentry/types'; +import type { Client, Event, EventHint, Integration } from '@sentry/types'; import { Debug } from '../src/debug'; -function testEventLogged(integration: Debug, testEvent?: Event, testEventHint?: EventHint) { +interface IntegrationWithSetup extends Integration { + setup: (client: Client) => void; +} + +function testEventLogged(integration: IntegrationWithSetup, testEvent?: Event, testEventHint?: EventHint) { const callbacks: ((event: Event, hint?: EventHint) => void)[] = []; const client: Client = { diff --git a/packages/integrations/test/dedupe.test.ts b/packages/integrations/test/dedupe.test.ts index 545aa1a83c1c..bb996fa45960 100644 --- a/packages/integrations/test/dedupe.test.ts +++ b/packages/integrations/test/dedupe.test.ts @@ -10,7 +10,6 @@ type EventWithException = SentryEvent & { type ExceptionWithStacktrace = Exception & { stacktrace: StacktraceWithFrames }; type StacktraceWithFrames = Stacktrace & { frames: StackFrame[] }; -/** JSDoc */ function clone(data: T): T { return JSON.parse(JSON.stringify(data)); } diff --git a/packages/integrations/test/extraerrordata.test.ts b/packages/integrations/test/extraerrordata.test.ts index bc7a6312a65a..166c8e66fe37 100644 --- a/packages/integrations/test/extraerrordata.test.ts +++ b/packages/integrations/test/extraerrordata.test.ts @@ -15,7 +15,7 @@ describe('ExtraErrorData()', () => { error.baz = 42; error.foo = 'bar'; - const enhancedEvent = extraErrorData.enhanceEventWithErrorData(event, { + const enhancedEvent = extraErrorData.processEvent(event, { originalException: error, }); @@ -31,7 +31,7 @@ describe('ExtraErrorData()', () => { const error = new TypeError('foo') as ExtendedError; error.cause = new SyntaxError('bar'); - const enhancedEvent = extraErrorData.enhanceEventWithErrorData(event, { + const enhancedEvent = extraErrorData.processEvent(event, { originalException: error, }); @@ -52,7 +52,7 @@ describe('ExtraErrorData()', () => { }, }; - const enhancedEvent = extraErrorData.enhanceEventWithErrorData(event, { + const enhancedEvent = extraErrorData.processEvent(event, { originalException: error, }); @@ -76,7 +76,7 @@ describe('ExtraErrorData()', () => { const error = new TypeError('foo') as ExtendedError; error.baz = 42; - const enhancedEvent = extraErrorData.enhanceEventWithErrorData(event, { + const enhancedEvent = extraErrorData.processEvent(event, { originalException: error, }); @@ -91,7 +91,7 @@ describe('ExtraErrorData()', () => { it('should return event if originalException is not an Error object', () => { const error = 'error message, not object'; - const enhancedEvent = extraErrorData.enhanceEventWithErrorData(event, { + const enhancedEvent = extraErrorData.processEvent(event, { originalException: error, }); @@ -99,13 +99,13 @@ describe('ExtraErrorData()', () => { }); it('should return event if there is no SentryEventHint', () => { - const enhancedEvent = extraErrorData.enhanceEventWithErrorData(event); + const enhancedEvent = extraErrorData.processEvent(event, {}); expect(enhancedEvent).toEqual(event); }); it('should return event if there is no originalException', () => { - const enhancedEvent = extraErrorData.enhanceEventWithErrorData(event, { + const enhancedEvent = extraErrorData.processEvent(event, { // @ts-expect-error Allow event to have extra properties notOriginalException: 'fooled you', }); @@ -124,7 +124,7 @@ describe('ExtraErrorData()', () => { }; }; - const enhancedEvent = extraErrorData.enhanceEventWithErrorData(event, { + const enhancedEvent = extraErrorData.processEvent(event, { originalException: error, }); @@ -147,7 +147,7 @@ describe('ExtraErrorData()', () => { }; }; - const enhancedEvent = extraErrorData.enhanceEventWithErrorData(event, { + const enhancedEvent = extraErrorData.processEvent(event, { originalException: error, }); @@ -167,7 +167,7 @@ describe('ExtraErrorData()', () => { }; }; - const enhancedEvent = extraErrorData.enhanceEventWithErrorData(event, { + const enhancedEvent = extraErrorData.processEvent(event, { originalException: error, }); diff --git a/packages/integrations/test/reportingobserver.test.ts b/packages/integrations/test/reportingobserver.test.ts index 6378a456c854..275e63c82ea9 100644 --- a/packages/integrations/test/reportingobserver.test.ts +++ b/packages/integrations/test/reportingobserver.test.ts @@ -128,8 +128,10 @@ describe('ReportingObserver', () => { ); // without calling setup, the integration is not registered + const handler = mockReportingObserverConstructor.mock.calls[0][0]; + expect(() => { - reportingObserverIntegration.handler([{ type: 'crash', url: 'some url' }]); + handler([{ type: 'crash', url: 'some url' }]); }).not.toThrow(); expect(captureMessage).not.toHaveBeenCalled(); @@ -142,8 +144,9 @@ describe('ReportingObserver', () => { () => mockHub, ); reportingObserverIntegration.setup(mockClient); + const handler = mockReportingObserverConstructor.mock.calls[0][0]; - reportingObserverIntegration.handler([ + handler([ { type: 'crash', url: 'some url' }, { type: 'deprecation', url: 'some url' }, ]); @@ -158,8 +161,9 @@ describe('ReportingObserver', () => { () => mockHub, ); reportingObserverIntegration.setup(mockClient); + const handler = mockReportingObserverConstructor.mock.calls[0][0]; - reportingObserverIntegration.handler([ + handler([ { type: 'crash', url: 'some url 1' }, { type: 'deprecation', url: 'some url 2' }, ]); @@ -175,11 +179,12 @@ describe('ReportingObserver', () => { () => mockHub, ); reportingObserverIntegration.setup(mockClient); + const handler = mockReportingObserverConstructor.mock.calls[0][0]; const report1 = { type: 'crash', url: 'some url 1', body: { crashId: 'id1' } } as const; const report2 = { type: 'deprecation', url: 'some url 2', body: { id: 'id2', message: 'message' } } as const; - reportingObserverIntegration.handler([report1, report2]); + handler([report1, report2]); expect(mockScope.setExtra).toHaveBeenCalledWith('body', report1.body); expect(mockScope.setExtra).toHaveBeenCalledWith('body', report2.body); @@ -192,8 +197,9 @@ describe('ReportingObserver', () => { () => mockHub, ); reportingObserverIntegration.setup(mockClient); + const handler = mockReportingObserverConstructor.mock.calls[0][0]; - reportingObserverIntegration.handler([{ type: 'crash', url: 'some url' }]); + handler([{ type: 'crash', url: 'some url' }]); expect(mockScope.setExtra).not.toHaveBeenCalledWith('body', expect.anything()); }); @@ -205,13 +211,14 @@ describe('ReportingObserver', () => { () => mockHub, ); reportingObserverIntegration.setup(mockClient); + const handler = mockReportingObserverConstructor.mock.calls[0][0]; const report = { type: 'crash', url: 'some url', body: { crashId: 'some id', reason: 'some reason' }, } as const; - reportingObserverIntegration.handler([report]); + handler([report]); expect(captureMessage).toHaveBeenCalledWith(expect.stringContaining(report.type)); expect(captureMessage).toHaveBeenCalledWith(expect.stringContaining(report.body.crashId)); @@ -225,13 +232,14 @@ describe('ReportingObserver', () => { () => mockHub, ); reportingObserverIntegration.setup(mockClient); + const handler = mockReportingObserverConstructor.mock.calls[0][0]; const report = { type: 'deprecation', url: 'some url', body: { id: 'some id', message: 'some message' }, } as const; - reportingObserverIntegration.handler([report]); + handler([report]); expect(captureMessage).toHaveBeenCalledWith(expect.stringContaining(report.type)); expect(captureMessage).toHaveBeenCalledWith(expect.stringContaining(report.body.message)); @@ -244,13 +252,14 @@ describe('ReportingObserver', () => { () => mockHub, ); reportingObserverIntegration.setup(mockClient); + const handler = mockReportingObserverConstructor.mock.calls[0][0]; const report = { type: 'intervention', url: 'some url', body: { id: 'some id', message: 'some message' }, } as const; - reportingObserverIntegration.handler([report]); + handler([report]); expect(captureMessage).toHaveBeenCalledWith(expect.stringContaining(report.type)); expect(captureMessage).toHaveBeenCalledWith(expect.stringContaining(report.body.message)); @@ -263,12 +272,13 @@ describe('ReportingObserver', () => { () => mockHub, ); reportingObserverIntegration.setup(mockClient); + const handler = mockReportingObserverConstructor.mock.calls[0][0]; const report = { type: 'intervention', url: 'some url', } as const; - reportingObserverIntegration.handler([report]); + handler([report]); expect(captureMessage).toHaveBeenCalledWith(expect.stringContaining(report.type)); expect(captureMessage).toHaveBeenCalledWith(expect.stringContaining('No details available')); @@ -281,9 +291,10 @@ describe('ReportingObserver', () => { () => mockHub, ); reportingObserverIntegration.setup(mockClient); + const handler = mockReportingObserverConstructor.mock.calls[0][0]; const report = { type: 'crash', url: 'some url', body: { crashId: '', reason: '' } } as const; - reportingObserverIntegration.handler([report]); + handler([report]); expect(captureMessage).toHaveBeenCalledWith(expect.stringContaining(report.type)); expect(captureMessage).toHaveBeenCalledWith(expect.stringContaining('No details available')); @@ -296,13 +307,14 @@ describe('ReportingObserver', () => { () => mockHub, ); reportingObserverIntegration.setup(mockClient); + const handler = mockReportingObserverConstructor.mock.calls[0][0]; const report = { type: 'deprecation', url: 'some url', body: { id: 'some id', message: '' }, } as const; - reportingObserverIntegration.handler([report]); + handler([report]); expect(captureMessage).toHaveBeenCalledWith(expect.stringContaining(report.type)); expect(captureMessage).toHaveBeenCalledWith(expect.stringContaining('No details available')); @@ -315,13 +327,14 @@ describe('ReportingObserver', () => { () => mockHub, ); reportingObserverIntegration.setup(mockClient); + const handler = mockReportingObserverConstructor.mock.calls[0][0]; const report = { type: 'intervention', url: 'some url', body: { id: 'some id', message: '' }, } as const; - reportingObserverIntegration.handler([report]); + handler([report]); expect(captureMessage).toHaveBeenCalledWith(expect.stringContaining(report.type)); expect(captureMessage).toHaveBeenCalledWith(expect.stringContaining('No details available')); diff --git a/packages/integrations/test/rewriteframes.test.ts b/packages/integrations/test/rewriteframes.test.ts index 749df9a862e1..7a65ff129aca 100644 --- a/packages/integrations/test/rewriteframes.test.ts +++ b/packages/integrations/test/rewriteframes.test.ts @@ -1,8 +1,12 @@ -import type { Event, StackFrame } from '@sentry/types'; +import type { Event, Integration, StackFrame } from '@sentry/types'; import { RewriteFrames } from '../src/rewriteframes'; -let rewriteFrames: RewriteFrames; +interface IntegrationWithProcessEvent extends Integration { + processEvent(event: Event): Event; +} + +let rewriteFrames: IntegrationWithProcessEvent; let exceptionEvent: Event; let exceptionWithoutStackTrace: Event; let windowsExceptionEvent: Event; @@ -102,7 +106,7 @@ describe('RewriteFrames', () => { }); it('transforms exceptionEvent frames', () => { - const event = rewriteFrames.process(exceptionEvent); + const event = rewriteFrames.processEvent(exceptionEvent); expect(event.exception!.values![0].stacktrace!.frames![0].filename).toEqual('app:///file1.js'); expect(event.exception!.values![0].stacktrace!.frames![1].filename).toEqual('app:///file2.js'); }); @@ -110,7 +114,7 @@ describe('RewriteFrames', () => { it('ignore exception without StackTrace', () => { // @ts-expect-error Validates that the Stacktrace does not exist before validating the test. expect(exceptionWithoutStackTrace.exception?.values[0].stacktrace).toEqual(undefined); - const event = rewriteFrames.process(exceptionWithoutStackTrace); + const event = rewriteFrames.processEvent(exceptionWithoutStackTrace); expect(event.exception!.values![0].stacktrace).toEqual(undefined); }); }); @@ -123,7 +127,7 @@ describe('RewriteFrames', () => { }); it('transforms exceptionEvent frames', () => { - const event = rewriteFrames.process(exceptionEvent); + const event = rewriteFrames.processEvent(exceptionEvent); expect(event.exception!.values![0].stacktrace!.frames![0].filename).toEqual('foobar/file1.js'); expect(event.exception!.values![0].stacktrace!.frames![1].filename).toEqual('foobar/file2.js'); }); @@ -135,25 +139,25 @@ describe('RewriteFrames', () => { }); it('transforms windowsExceptionEvent frames (C:\\)', () => { - const event = rewriteFrames.process(windowsExceptionEvent); + const event = rewriteFrames.processEvent(windowsExceptionEvent); expect(event.exception!.values![0].stacktrace!.frames![0].filename).toEqual('app:///file1.js'); expect(event.exception!.values![0].stacktrace!.frames![1].filename).toEqual('app:///file2.js'); }); it('transforms windowsExceptionEvent frames with lower-case prefix (c:\\)', () => { - const event = rewriteFrames.process(windowsLowerCaseExceptionEvent); + const event = rewriteFrames.processEvent(windowsLowerCaseExceptionEvent); expect(event.exception!.values![0].stacktrace!.frames![0].filename).toEqual('app:///file1.js'); expect(event.exception!.values![0].stacktrace!.frames![1].filename).toEqual('app:///file2.js'); }); it('transforms windowsExceptionEvent frames with no prefix', () => { - const event = rewriteFrames.process(windowsExceptionEventWithoutPrefix); + const event = rewriteFrames.processEvent(windowsExceptionEventWithoutPrefix); expect(event.exception!.values![0].stacktrace!.frames![0].filename).toEqual('app:///file1.js'); expect(event.exception!.values![0].stacktrace!.frames![1].filename).toEqual('app:///file2.js'); }); it('transforms windowsExceptionEvent frames with backslash prefix', () => { - const event = rewriteFrames.process(windowsExceptionEventWithBackslashPrefix); + const event = rewriteFrames.processEvent(windowsExceptionEventWithBackslashPrefix); expect(event.exception!.values![0].stacktrace!.frames![0].filename).toEqual('app:///file1.js'); expect(event.exception!.values![0].stacktrace!.frames![1].filename).toEqual('app:///file2.js'); }); @@ -167,31 +171,31 @@ describe('RewriteFrames', () => { }); it('transforms exceptionEvent frames', () => { - const event = rewriteFrames.process(exceptionEvent); + const event = rewriteFrames.processEvent(exceptionEvent); expect(event.exception!.values![0].stacktrace!.frames![0].filename).toEqual('app:///src/app/file1.js'); expect(event.exception!.values![0].stacktrace!.frames![1].filename).toEqual('app:///src/app/mo\\dule/file2.js'); }); it('transforms windowsExceptionEvent frames', () => { - const event = rewriteFrames.process(windowsExceptionEvent); + const event = rewriteFrames.processEvent(windowsExceptionEvent); expect(event.exception!.values![0].stacktrace!.frames![0].filename).toEqual('app:///src/app/file1.js'); expect(event.exception!.values![0].stacktrace!.frames![1].filename).toEqual('app:///src/app/file2.js'); }); it('transforms windowsExceptionEvent lower-case prefix frames', () => { - const event = rewriteFrames.process(windowsLowerCaseExceptionEvent); + const event = rewriteFrames.processEvent(windowsLowerCaseExceptionEvent); expect(event.exception!.values![0].stacktrace!.frames![0].filename).toEqual('app:///src/app/file1.js'); expect(event.exception!.values![0].stacktrace!.frames![1].filename).toEqual('app:///src/app/file2.js'); }); it('transforms windowsExceptionEvent frames with no prefix', () => { - const event = rewriteFrames.process(windowsExceptionEventWithoutPrefix); + const event = rewriteFrames.processEvent(windowsExceptionEventWithoutPrefix); expect(event.exception!.values![0].stacktrace!.frames![0].filename).toEqual('app:///src/app/file1.js'); expect(event.exception!.values![0].stacktrace!.frames![1].filename).toEqual('app:///src/app/file2.js'); }); it('transforms windowsExceptionEvent frames with backslash prefix', () => { - const event = rewriteFrames.process(windowsExceptionEventWithBackslashPrefix); + const event = rewriteFrames.processEvent(windowsExceptionEventWithBackslashPrefix); expect(event.exception!.values![0].stacktrace!.frames![0].filename).toEqual('app:///src/app/file1.js'); expect(event.exception!.values![0].stacktrace!.frames![1].filename).toEqual('app:///src/app/file2.js'); }); @@ -208,7 +212,7 @@ describe('RewriteFrames', () => { }); it('transforms exceptionEvent frames', () => { - const event = rewriteFrames.process(exceptionEvent); + const event = rewriteFrames.processEvent(exceptionEvent); expect(event.exception!.values![0].stacktrace!.frames![0].filename).toEqual('/www/src/app/file1.js'); expect(event.exception!.values![0].stacktrace!.frames![0].function).toEqual('whoops'); expect(event.exception!.values![0].stacktrace!.frames![1].filename).toEqual('/www/src/app/mo\\dule/file2.js'); @@ -219,7 +223,7 @@ describe('RewriteFrames', () => { describe('can process events that contain multiple stacktraces', () => { it('with defaults', () => { rewriteFrames = new RewriteFrames(); - const event = rewriteFrames.process(multipleStacktracesEvent); + const event = rewriteFrames.processEvent(multipleStacktracesEvent); // first stacktrace expect(event.exception!.values![0].stacktrace!.frames![0].filename).toEqual('app:///file1.js'); expect(event.exception!.values![0].stacktrace!.frames![1].filename).toEqual('app:///file2.js'); @@ -235,7 +239,7 @@ describe('RewriteFrames', () => { rewriteFrames = new RewriteFrames({ root: '/www', }); - const event = rewriteFrames.process(multipleStacktracesEvent); + const event = rewriteFrames.processEvent(multipleStacktracesEvent); // first stacktrace expect(event.exception!.values![0].stacktrace!.frames![0].filename).toEqual('app:///src/app/file1.js'); expect(event.exception!.values![0].stacktrace!.frames![1].filename).toEqual('app:///src/app/mo\\dule/file2.js'); @@ -254,7 +258,7 @@ describe('RewriteFrames', () => { function: 'whoops', }), }); - const event = rewriteFrames.process(multipleStacktracesEvent); + const event = rewriteFrames.processEvent(multipleStacktracesEvent); // first stacktrace expect(event.exception!.values![0].stacktrace!.frames![0].filename).toEqual('/www/src/app/file1.js'); expect(event.exception!.values![0].stacktrace!.frames![0].function).toEqual('whoops'); @@ -281,7 +285,7 @@ describe('RewriteFrames', () => { values: undefined, }, }; - expect(rewriteFrames.process(brokenEvent)).toEqual(brokenEvent); + expect(rewriteFrames.processEvent(brokenEvent)).toEqual(brokenEvent); }); it('no frames', () => { @@ -295,7 +299,7 @@ describe('RewriteFrames', () => { ], }, }; - expect(rewriteFrames.process(brokenEvent)).toEqual(brokenEvent); + expect(rewriteFrames.processEvent(brokenEvent)).toEqual(brokenEvent); }); }); }); diff --git a/packages/integrations/test/sessiontiming.test.ts b/packages/integrations/test/sessiontiming.test.ts index 033c9ea8a441..d1569db52095 100644 --- a/packages/integrations/test/sessiontiming.test.ts +++ b/packages/integrations/test/sessiontiming.test.ts @@ -1,18 +1,18 @@ import { SessionTiming } from '../src/sessiontiming'; -const sessionTiming: SessionTiming = new SessionTiming(); +const sessionTiming = new SessionTiming(); describe('SessionTiming', () => { it('should work as expected', () => { - const event = sessionTiming.process({ + const event = sessionTiming.processEvent({ extra: { some: 'value', }, }); - expect(typeof event.extra!['session:start']).toBe('number'); - expect(typeof event.extra!['session:duration']).toBe('number'); - expect(typeof event.extra!['session:end']).toBe('number'); - expect(event.extra!.some).toEqual('value'); + expect(typeof event.extra['session:start']).toBe('number'); + expect(typeof event.extra['session:duration']).toBe('number'); + expect(typeof event.extra['session:end']).toBe('number'); + expect((event.extra as any).some).toEqual('value'); }); }); diff --git a/packages/integrations/test/transaction.test.ts b/packages/integrations/test/transaction.test.ts index 9a87369fb234..bfc6096a519e 100644 --- a/packages/integrations/test/transaction.test.ts +++ b/packages/integrations/test/transaction.test.ts @@ -1,6 +1,6 @@ import { Transaction } from '../src/transaction'; -const transaction: Transaction = new Transaction(); +const transaction = new Transaction(); describe('Transaction', () => { describe('extracts info from module/function of the first `in_app` frame', () => { diff --git a/packages/sveltekit/test/server/utils.test.ts b/packages/sveltekit/test/server/utils.test.ts index 272dba8330ce..cad2051c2c14 100644 --- a/packages/sveltekit/test/server/utils.test.ts +++ b/packages/sveltekit/test/server/utils.test.ts @@ -1,5 +1,5 @@ import { RewriteFrames } from '@sentry/integrations'; -import type { StackFrame } from '@sentry/types'; +import type { Event, StackFrame } from '@sentry/types'; import { basename } from '@sentry/utils'; import type { GlobalWithSentryValues } from '../../src/server/utils'; @@ -80,21 +80,30 @@ describe('rewriteFramesIteratee', () => { }; const originalRewriteFrames = new RewriteFrames(); - // @ts-expect-error this property exists - const defaultIteratee = originalRewriteFrames._iteratee; - - const defaultResult = defaultIteratee({ ...frame }); - delete defaultResult.module; + const rewriteFrames = new RewriteFrames({ iteratee: rewriteFramesIteratee }); + + const event: Event = { + exception: { + values: [ + { + stacktrace: { + frames: [frame], + }, + }, + ], + }, + }; - const result = rewriteFramesIteratee({ ...frame }); + const originalResult = originalRewriteFrames.processEvent(event); + const result = rewriteFrames.processEvent(event); - expect(result).toEqual({ + expect(result.exception?.values?.[0]?.stacktrace?.frames?.[0]).toEqual({ filename: 'app:///3-ab34d22f.js', lineno: 1, colno: 1, }); - expect(result).toStrictEqual(defaultResult); + expect(result).toStrictEqual(originalResult); }); it.each([ From b6a7cef8a65adaeee07dcb70ebefbe79cb7f5d82 Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Thu, 21 Dec 2023 16:31:46 +0100 Subject: [PATCH 15/24] feat(core): Ensure `startSpan` & `startSpanManual` fork scope (#9955) @lforst noticed that currently `startSpan()` and `startSpanManual` do not fork the scope, which is also a slightly different behavior than it is in OTEL. This adjusts this to better align, so that we always fork a scope. We also always leave the span on the forked scope, even after it was finished. In a follow up, we can thus also get rid of the `finish` callback arg for `startSpanManual`, as users can/should simply call `span.end()` themselves then. --- packages/core/src/tracing/trace.ts | 112 +++++++++---------- packages/core/test/lib/tracing/trace.test.ts | 83 +++++++++++++- 2 files changed, 137 insertions(+), 58 deletions(-) diff --git a/packages/core/src/tracing/trace.ts b/packages/core/src/tracing/trace.ts index 52c915c30189..db6e34aa487e 100644 --- a/packages/core/src/tracing/trace.ts +++ b/packages/core/src/tracing/trace.ts @@ -2,7 +2,7 @@ import type { TransactionContext } from '@sentry/types'; import { dropUndefinedKeys, isThenable, logger, tracingContextFromHeaders } from '@sentry/utils'; import { DEBUG_BUILD } from '../debug-build'; -import { getCurrentScope } from '../exports'; +import { getCurrentScope, withScope } from '../exports'; import type { Hub } from '../hub'; import { getCurrentHub } from '../hub'; import { hasTracingEnabled } from '../utils/hasTracingEnabled'; @@ -89,42 +89,42 @@ export function trace( export function startSpan(context: TransactionContext, callback: (span: Span | undefined) => T): T { const ctx = normalizeContext(context); - const hub = getCurrentHub(); - const scope = getCurrentScope(); - const parentSpan = scope.getSpan(); + return withScope(scope => { + const hub = getCurrentHub(); + const parentSpan = scope.getSpan(); - const activeSpan = createChildSpanOrTransaction(hub, parentSpan, ctx); - scope.setSpan(activeSpan); - - function finishAndSetSpan(): void { - activeSpan && activeSpan.end(); - scope.setSpan(parentSpan); - } + const activeSpan = createChildSpanOrTransaction(hub, parentSpan, ctx); + scope.setSpan(activeSpan); - let maybePromiseResult: T; - try { - maybePromiseResult = callback(activeSpan); - } catch (e) { - activeSpan && activeSpan.setStatus('internal_error'); - finishAndSetSpan(); - throw e; - } + function finishAndSetSpan(): void { + activeSpan && activeSpan.end(); + } - if (isThenable(maybePromiseResult)) { - Promise.resolve(maybePromiseResult).then( - () => { - finishAndSetSpan(); - }, - () => { - activeSpan && activeSpan.setStatus('internal_error'); - finishAndSetSpan(); - }, - ); - } else { - finishAndSetSpan(); - } - - return maybePromiseResult; + let maybePromiseResult: T; + try { + maybePromiseResult = callback(activeSpan); + } catch (e) { + activeSpan && activeSpan.setStatus('internal_error'); + finishAndSetSpan(); + throw e; + } + + if (isThenable(maybePromiseResult)) { + Promise.resolve(maybePromiseResult).then( + () => { + finishAndSetSpan(); + }, + () => { + activeSpan && activeSpan.setStatus('internal_error'); + finishAndSetSpan(); + }, + ); + } else { + finishAndSetSpan(); + } + + return maybePromiseResult; + }); } /** @@ -149,33 +149,33 @@ export function startSpanManual( ): T { const ctx = normalizeContext(context); - const hub = getCurrentHub(); - const scope = getCurrentScope(); - const parentSpan = scope.getSpan(); + return withScope(scope => { + const hub = getCurrentHub(); + const parentSpan = scope.getSpan(); - const activeSpan = createChildSpanOrTransaction(hub, parentSpan, ctx); - scope.setSpan(activeSpan); + const activeSpan = createChildSpanOrTransaction(hub, parentSpan, ctx); + scope.setSpan(activeSpan); - function finishAndSetSpan(): void { - activeSpan && activeSpan.end(); - scope.setSpan(parentSpan); - } + function finishAndSetSpan(): void { + activeSpan && activeSpan.end(); + } - let maybePromiseResult: T; - try { - maybePromiseResult = callback(activeSpan, finishAndSetSpan); - } catch (e) { - activeSpan && activeSpan.setStatus('internal_error'); - throw e; - } - - if (isThenable(maybePromiseResult)) { - Promise.resolve(maybePromiseResult).then(undefined, () => { + let maybePromiseResult: T; + try { + maybePromiseResult = callback(activeSpan, finishAndSetSpan); + } catch (e) { activeSpan && activeSpan.setStatus('internal_error'); - }); - } + throw e; + } - return maybePromiseResult; + if (isThenable(maybePromiseResult)) { + Promise.resolve(maybePromiseResult).then(undefined, () => { + activeSpan && activeSpan.setStatus('internal_error'); + }); + } + + return maybePromiseResult; + }); } /** diff --git a/packages/core/test/lib/tracing/trace.test.ts b/packages/core/test/lib/tracing/trace.test.ts index a9ea12ef89c0..30eac02c881f 100644 --- a/packages/core/test/lib/tracing/trace.test.ts +++ b/packages/core/test/lib/tracing/trace.test.ts @@ -1,5 +1,6 @@ -import { Hub, addTracingExtensions, makeMain } from '../../../src'; -import { continueTrace, startSpan } from '../../../src/tracing'; +import type { Span } from '@sentry/types'; +import { Hub, addTracingExtensions, getCurrentScope, makeMain } from '../../../src'; +import { continueTrace, startInactiveSpan, startSpan, startSpanManual } from '../../../src/tracing'; import { TestClient, getDefaultTestClientOptions } from '../../mocks/client'; beforeAll(() => { @@ -80,6 +81,18 @@ describe('startSpan', () => { expect(ref.status).toEqual(isError ? 'internal_error' : undefined); }); + it('creates & finishes span', async () => { + let _span: Span | undefined; + startSpan({ name: 'GET users/[id]' }, span => { + expect(span).toBeDefined(); + expect(span?.endTimestamp).toBeUndefined(); + _span = span; + }); + + expect(_span).toBeDefined(); + expect(_span?.endTimestamp).toBeDefined(); + }); + it('allows traceparent information to be overriden', async () => { let ref: any = undefined; client.on('finishTransaction', transaction => { @@ -168,6 +181,72 @@ describe('startSpan', () => { expect(ref.spanRecorder.spans).toHaveLength(2); expect(ref.spanRecorder.spans[1].op).toEqual('db.query'); }); + + it('forks the scope', () => { + const initialScope = getCurrentScope(); + + startSpan({ name: 'GET users/[id]' }, span => { + expect(getCurrentScope()).not.toBe(initialScope); + expect(getCurrentScope().getSpan()).toBe(span); + }); + + expect(getCurrentScope()).toBe(initialScope); + expect(initialScope.getSpan()).toBe(undefined); + }); + }); +}); + +describe('startSpanManual', () => { + it('creates & finishes span', async () => { + startSpanManual({ name: 'GET users/[id]' }, (span, finish) => { + expect(span).toBeDefined(); + expect(span?.endTimestamp).toBeUndefined(); + finish(); + expect(span?.endTimestamp).toBeDefined(); + }); + }); + + it('forks the scope automatically', () => { + const initialScope = getCurrentScope(); + + startSpanManual({ name: 'GET users/[id]' }, (span, finish) => { + expect(getCurrentScope()).not.toBe(initialScope); + expect(getCurrentScope().getSpan()).toBe(span); + + finish(); + + // Is still the active span + expect(getCurrentScope().getSpan()).toBe(span); + }); + + expect(getCurrentScope()).toBe(initialScope); + expect(initialScope.getSpan()).toBe(undefined); + }); +}); + +describe('startInactiveSpan', () => { + it('creates & finishes span', async () => { + const span = startInactiveSpan({ name: 'GET users/[id]' }); + + expect(span).toBeDefined(); + expect(span?.endTimestamp).toBeUndefined(); + + span?.end(); + + expect(span?.endTimestamp).toBeDefined(); + }); + + it('does not set span on scope', () => { + const initialScope = getCurrentScope(); + + const span = startInactiveSpan({ name: 'GET users/[id]' }); + + expect(span).toBeDefined(); + expect(initialScope.getSpan()).toBeUndefined(); + + span?.end(); + + expect(initialScope.getSpan()).toBeUndefined(); }); }); From f433a554e1b024b8855cb56e72e3f095c555fba3 Mon Sep 17 00:00:00 2001 From: Ash <0Calories@users.noreply.github.com> Date: Thu, 21 Dec 2023 11:40:04 -0500 Subject: [PATCH 16/24] feat(tracing): Send component name on interaction spans (#9948) One of the PRs scoped from https://github.com/getsentry/sentry-javascript/pull/9855 Sends component names on the databag of interaction spans --- .../interactions/assets/script.js | 1 + .../browsertracing/interactions/template.html | 1 + .../browsertracing/interactions/test.ts | 32 +++++++++++++++++++ .../src/browser/metrics/index.ts | 15 ++++++--- 4 files changed, 45 insertions(+), 4 deletions(-) diff --git a/packages/browser-integration-tests/suites/tracing/browsertracing/interactions/assets/script.js b/packages/browser-integration-tests/suites/tracing/browsertracing/interactions/assets/script.js index 89d814bd397d..a37a2c70ad27 100644 --- a/packages/browser-integration-tests/suites/tracing/browsertracing/interactions/assets/script.js +++ b/packages/browser-integration-tests/suites/tracing/browsertracing/interactions/assets/script.js @@ -14,3 +14,4 @@ const delay = e => { }; document.querySelector('[data-test-id=interaction-button]').addEventListener('click', delay); +document.querySelector('[data-test-id=annotated-button]').addEventListener('click', delay); diff --git a/packages/browser-integration-tests/suites/tracing/browsertracing/interactions/template.html b/packages/browser-integration-tests/suites/tracing/browsertracing/interactions/template.html index e16deb9ee519..3357fb20a94e 100644 --- a/packages/browser-integration-tests/suites/tracing/browsertracing/interactions/template.html +++ b/packages/browser-integration-tests/suites/tracing/browsertracing/interactions/template.html @@ -6,6 +6,7 @@
Rendered Before Long Task
+ diff --git a/packages/browser-integration-tests/suites/tracing/browsertracing/interactions/test.ts b/packages/browser-integration-tests/suites/tracing/browsertracing/interactions/test.ts index e79b724ec91a..131403756251 100644 --- a/packages/browser-integration-tests/suites/tracing/browsertracing/interactions/test.ts +++ b/packages/browser-integration-tests/suites/tracing/browsertracing/interactions/test.ts @@ -80,3 +80,35 @@ sentryTest( } }, ); + +sentryTest( + 'should use the component name for a clicked element when it is available', + async ({ browserName, getLocalTestPath, page }) => { + const supportedBrowsers = ['chromium', 'firefox']; + + if (shouldSkipTracingTest() || !supportedBrowsers.includes(browserName)) { + sentryTest.skip(); + } + + await page.route('**/path/to/script.js', (route: Route) => + route.fulfill({ path: `${__dirname}/assets/script.js` }), + ); + + const url = await getLocalTestPath({ testDir: __dirname }); + + await page.goto(url); + await getFirstSentryEnvelopeRequest(page); + + await page.locator('[data-test-id=annotated-button]').click(); + + const envelopes = await getMultipleSentryEnvelopeRequests(page, 1); + expect(envelopes).toHaveLength(1); + const eventData = envelopes[0]; + + expect(eventData.spans).toHaveLength(1); + + const interactionSpan = eventData.spans![0]; + expect(interactionSpan.op).toBe('ui.interaction.click'); + expect(interactionSpan.description).toBe('body > AnnotatedButton'); + }, +); diff --git a/packages/tracing-internal/src/browser/metrics/index.ts b/packages/tracing-internal/src/browser/metrics/index.ts index 5182897cb0b9..651246dfb688 100644 --- a/packages/tracing-internal/src/browser/metrics/index.ts +++ b/packages/tracing-internal/src/browser/metrics/index.ts @@ -1,8 +1,8 @@ /* eslint-disable max-lines */ import type { IdleTransaction, Transaction } from '@sentry/core'; import { getActiveTransaction } from '@sentry/core'; -import type { Measurements } from '@sentry/types'; -import { browserPerformanceTimeOrigin, htmlTreeAsString, logger } from '@sentry/utils'; +import type { Measurements, SpanContext } from '@sentry/types'; +import { browserPerformanceTimeOrigin, getComponentName, htmlTreeAsString, logger } from '@sentry/utils'; import { DEBUG_BUILD } from '../../common/debug-build'; import { @@ -102,13 +102,20 @@ export function startTrackingInteractions(): void { const startTime = msToSec((browserPerformanceTimeOrigin as number) + entry.startTime); const duration = msToSec(entry.duration); - transaction.startChild({ + const span: SpanContext = { description: htmlTreeAsString(entry.target), op: `ui.interaction.${entry.name}`, origin: 'auto.ui.browser.metrics', startTimestamp: startTime, endTimestamp: startTime + duration, - }); + }; + + const componentName = getComponentName(entry.target); + if (componentName) { + span.data = { 'ui.component_name': componentName }; + } + + transaction.startChild(span); } } }); From 9a2570bcb266a6b1afd9094c7b13ab95553a583d Mon Sep 17 00:00:00 2001 From: Ash <0Calories@users.noreply.github.com> Date: Thu, 21 Dec 2023 11:47:50 -0500 Subject: [PATCH 17/24] feat(replay): Send component names in replay breadcrumbs (#9947) One of the PRs scoped from https://github.com/getsentry/sentry-javascript/pull/9855 Sends component names on Replay UI breadcrumbs so they can be ingested and indexed. This will allow for searching for Replays by component name in the future. --------- Co-authored-by: Yagiz Nizipli --- .../replay/captureComponentName/init.js | 16 ++++ .../replay/captureComponentName/template.html | 10 +++ .../replay/captureComponentName/test.ts | 83 +++++++++++++++++++ .../util/getAttributesToRecord.ts | 1 + 4 files changed, 110 insertions(+) create mode 100644 packages/browser-integration-tests/suites/replay/captureComponentName/init.js create mode 100644 packages/browser-integration-tests/suites/replay/captureComponentName/template.html create mode 100644 packages/browser-integration-tests/suites/replay/captureComponentName/test.ts diff --git a/packages/browser-integration-tests/suites/replay/captureComponentName/init.js b/packages/browser-integration-tests/suites/replay/captureComponentName/init.js new file mode 100644 index 000000000000..040fb88ab2bd --- /dev/null +++ b/packages/browser-integration-tests/suites/replay/captureComponentName/init.js @@ -0,0 +1,16 @@ +import * as Sentry from '@sentry/browser'; + +window.Sentry = Sentry; +window.Replay = new Sentry.Replay({ + flushMinDelay: 200, + flushMaxDelay: 200, + minReplayDuration: 0, +}); + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + sampleRate: 0, + replaysSessionSampleRate: 1.0, + replaysOnErrorSampleRate: 0.0, + integrations: [window.Replay], +}); diff --git a/packages/browser-integration-tests/suites/replay/captureComponentName/template.html b/packages/browser-integration-tests/suites/replay/captureComponentName/template.html new file mode 100644 index 000000000000..1cb45daa349a --- /dev/null +++ b/packages/browser-integration-tests/suites/replay/captureComponentName/template.html @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/packages/browser-integration-tests/suites/replay/captureComponentName/test.ts b/packages/browser-integration-tests/suites/replay/captureComponentName/test.ts new file mode 100644 index 000000000000..99b7a71273e3 --- /dev/null +++ b/packages/browser-integration-tests/suites/replay/captureComponentName/test.ts @@ -0,0 +1,83 @@ +import { expect } from '@playwright/test'; + +import { sentryTest } from '../../../utils/fixtures'; +import { getCustomRecordingEvents, shouldSkipReplayTest, waitForReplayRequest } from '../../../utils/replayHelpers'; + +sentryTest('captures component name attribute when available', async ({ forceFlushReplay, getLocalTestPath, page }) => { + if (shouldSkipReplayTest()) { + sentryTest.skip(); + } + + const reqPromise0 = waitForReplayRequest(page, 0); + + await page.route('https://dsn.ingest.sentry.io/**/*', route => { + return route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ id: 'test-id' }), + }); + }); + + const url = await getLocalTestPath({ testDir: __dirname }); + + await page.goto(url); + await reqPromise0; + await forceFlushReplay(); + + const reqPromise1 = waitForReplayRequest(page, (event, res) => { + return getCustomRecordingEvents(res).breadcrumbs.some(breadcrumb => breadcrumb.category === 'ui.click'); + }); + const reqPromise2 = waitForReplayRequest(page, (event, res) => { + return getCustomRecordingEvents(res).breadcrumbs.some(breadcrumb => breadcrumb.category === 'ui.input'); + }); + + await page.locator('#button').click(); + + await page.locator('#input').focus(); + await page.keyboard.press('Control+A'); + await page.keyboard.type('Hello', { delay: 10 }); + + await forceFlushReplay(); + const { breadcrumbs } = getCustomRecordingEvents(await reqPromise1); + const { breadcrumbs: breadcrumbs2 } = getCustomRecordingEvents(await reqPromise2); + + // Combine the two together + breadcrumbs2.forEach(breadcrumb => { + if (!breadcrumbs.some(b => b.category === breadcrumb.category && b.timestamp === breadcrumb.timestamp)) { + breadcrumbs.push(breadcrumb); + } + }); + + expect(breadcrumbs).toEqual([ + { + timestamp: expect.any(Number), + type: 'default', + category: 'ui.click', + message: 'body > MyCoolButton', + data: { + nodeId: expect.any(Number), + node: { + attributes: { id: 'button', 'data-sentry-component': 'MyCoolButton' }, + id: expect.any(Number), + tagName: 'button', + textContent: '**', + }, + }, + }, + { + timestamp: expect.any(Number), + type: 'default', + category: 'ui.input', + message: 'body > MyCoolInput', + data: { + nodeId: expect.any(Number), + node: { + attributes: { id: 'input', 'data-sentry-component': 'MyCoolInput' }, + id: expect.any(Number), + tagName: 'input', + textContent: '', + }, + }, + }, + ]); +}); diff --git a/packages/replay/src/coreHandlers/util/getAttributesToRecord.ts b/packages/replay/src/coreHandlers/util/getAttributesToRecord.ts index 13c756901028..f50c2b9f9088 100644 --- a/packages/replay/src/coreHandlers/util/getAttributesToRecord.ts +++ b/packages/replay/src/coreHandlers/util/getAttributesToRecord.ts @@ -12,6 +12,7 @@ const ATTRIBUTES_TO_RECORD = new Set([ 'data-testid', 'disabled', 'aria-disabled', + 'data-sentry-component', ]); /** From f819d81bade81c1e7e9fe918583f10a73a525ad9 Mon Sep 17 00:00:00 2001 From: Ash <0Calories@users.noreply.github.com> Date: Thu, 21 Dec 2023 13:52:43 -0500 Subject: [PATCH 18/24] feat(breadcrumbs): Send component names on UI breadcrumbs (#9946) One of the PRs scoped from https://github.com/getsentry/sentry-javascript/pull/9855 Sends component names on UI event breadcrumbs --------- Co-authored-by: Abhijeet Prasad --- .../Breadcrumbs/dom/click/template.html | 1 + .../Breadcrumbs/dom/click/test.ts | 36 +++++++++++++++ .../Breadcrumbs/dom/textInput/template.html | 1 + .../Breadcrumbs/dom/textInput/test.ts | 45 +++++++++++++++++++ .../browser/src/integrations/breadcrumbs.ts | 35 +++++++++------ 5 files changed, 104 insertions(+), 14 deletions(-) diff --git a/packages/browser-integration-tests/suites/integrations/Breadcrumbs/dom/click/template.html b/packages/browser-integration-tests/suites/integrations/Breadcrumbs/dom/click/template.html index 5048dfd754f2..e54da47ff09d 100644 --- a/packages/browser-integration-tests/suites/integrations/Breadcrumbs/dom/click/template.html +++ b/packages/browser-integration-tests/suites/integrations/Breadcrumbs/dom/click/template.html @@ -7,5 +7,6 @@ + diff --git a/packages/browser-integration-tests/suites/integrations/Breadcrumbs/dom/click/test.ts b/packages/browser-integration-tests/suites/integrations/Breadcrumbs/dom/click/test.ts index 88a1c89fba0d..93ceb1e70001 100644 --- a/packages/browser-integration-tests/suites/integrations/Breadcrumbs/dom/click/test.ts +++ b/packages/browser-integration-tests/suites/integrations/Breadcrumbs/dom/click/test.ts @@ -55,3 +55,39 @@ sentryTest('captures Breadcrumb for clicks & debounces them for a second', async }, ]); }); + +sentryTest( + 'uses the annotated component name in the breadcrumb messages and adds it to the data object', + async ({ getLocalTestUrl, page }) => { + const url = await getLocalTestUrl({ testDir: __dirname }); + + await page.route('**/foo', route => { + return route.fulfill({ + status: 200, + body: JSON.stringify({ + userNames: ['John', 'Jane'], + }), + headers: { + 'Content-Type': 'application/json', + }, + }); + }); + + const promise = getFirstSentryEnvelopeRequest(page); + + await page.goto(url); + await page.click('#annotated-button'); + await page.evaluate('Sentry.captureException("test exception")'); + + const eventData = await promise; + + expect(eventData.breadcrumbs).toEqual([ + { + timestamp: expect.any(Number), + category: 'ui.click', + message: 'body > AnnotatedButton', + data: { 'ui.component_name': 'AnnotatedButton' }, + }, + ]); + }, +); diff --git a/packages/browser-integration-tests/suites/integrations/Breadcrumbs/dom/textInput/template.html b/packages/browser-integration-tests/suites/integrations/Breadcrumbs/dom/textInput/template.html index b3d53fbf9a3e..a16ca41e45da 100644 --- a/packages/browser-integration-tests/suites/integrations/Breadcrumbs/dom/textInput/template.html +++ b/packages/browser-integration-tests/suites/integrations/Breadcrumbs/dom/textInput/template.html @@ -7,5 +7,6 @@ + diff --git a/packages/browser-integration-tests/suites/integrations/Breadcrumbs/dom/textInput/test.ts b/packages/browser-integration-tests/suites/integrations/Breadcrumbs/dom/textInput/test.ts index b3393561f331..3a25abb1f9fe 100644 --- a/packages/browser-integration-tests/suites/integrations/Breadcrumbs/dom/textInput/test.ts +++ b/packages/browser-integration-tests/suites/integrations/Breadcrumbs/dom/textInput/test.ts @@ -64,3 +64,48 @@ sentryTest('captures Breadcrumb for events on inputs & debounced them', async ({ }, ]); }); + +sentryTest( + 'includes the annotated component name within the breadcrumb message and data', + async ({ getLocalTestUrl, page }) => { + const url = await getLocalTestUrl({ testDir: __dirname }); + + await page.route('**/foo', route => { + return route.fulfill({ + status: 200, + body: JSON.stringify({ + userNames: ['John', 'Jane'], + }), + headers: { + 'Content-Type': 'application/json', + }, + }); + }); + + const promise = getFirstSentryEnvelopeRequest(page); + + await page.goto(url); + + await page.click('#annotated-input'); + await page.type('#annotated-input', 'John', { delay: 1 }); + + await page.evaluate('Sentry.captureException("test exception")'); + const eventData = await promise; + expect(eventData.exception?.values).toHaveLength(1); + + expect(eventData.breadcrumbs).toEqual([ + { + timestamp: expect.any(Number), + category: 'ui.click', + message: 'body > AnnotatedInput', + data: { 'ui.component_name': 'AnnotatedInput' }, + }, + { + timestamp: expect.any(Number), + category: 'ui.input', + message: 'body > AnnotatedInput', + data: { 'ui.component_name': 'AnnotatedInput' }, + }, + ]); + }, +); diff --git a/packages/browser/src/integrations/breadcrumbs.ts b/packages/browser/src/integrations/breadcrumbs.ts index 102d1d7e500d..c14465b3448b 100644 --- a/packages/browser/src/integrations/breadcrumbs.ts +++ b/packages/browser/src/integrations/breadcrumbs.ts @@ -12,6 +12,7 @@ import type { IntegrationFn, } from '@sentry/types'; import type { + Breadcrumb, FetchBreadcrumbData, FetchBreadcrumbHint, XhrBreadcrumbData, @@ -24,6 +25,7 @@ import { addFetchInstrumentationHandler, addHistoryInstrumentationHandler, addXhrInstrumentationHandler, + getComponentName, getEventDescription, htmlTreeAsString, logger, @@ -133,6 +135,7 @@ function _getDomBreadcrumbHandler( } let target; + let componentName; let keyAttrs = typeof dom === 'object' ? dom.serializeAttribute : undefined; let maxStringLength = @@ -152,9 +155,10 @@ function _getDomBreadcrumbHandler( // Accessing event.target can throw (see getsentry/raven-js#838, #768) try { const event = handlerData.event as Event | Node; - target = _isEvent(event) - ? htmlTreeAsString(event.target, { keyAttrs, maxStringLength }) - : htmlTreeAsString(event, { keyAttrs, maxStringLength }); + const element = _isEvent(event) ? event.target : event; + + target = htmlTreeAsString(element, { keyAttrs, maxStringLength }); + componentName = getComponentName(element); } catch (e) { target = ''; } @@ -163,17 +167,20 @@ function _getDomBreadcrumbHandler( return; } - addBreadcrumb( - { - category: `ui.${handlerData.name}`, - message: target, - }, - { - event: handlerData.event, - name: handlerData.name, - global: handlerData.global, - }, - ); + const breadcrumb: Breadcrumb = { + category: `ui.${handlerData.name}`, + message: target, + }; + + if (componentName) { + breadcrumb.data = { 'ui.component_name': componentName }; + } + + addBreadcrumb(breadcrumb, { + event: handlerData.event, + name: handlerData.name, + global: handlerData.global, + }); }; } From 5d16aae1721086318696746f99caf9965073edbb Mon Sep 17 00:00:00 2001 From: Abhijeet Prasad Date: Thu, 21 Dec 2023 14:09:00 -0500 Subject: [PATCH 19/24] chore(biome): enable noUnusedImports rule (#9895) Adds https://biomejs.dev/linter/rules/no-unused-imports/ --- biome.json | 3 ++ package.json | 2 +- packages/angular/test/tracing.test.ts | 1 - packages/astro/test/server/middleware.test.ts | 2 +- .../public-api/startTransaction/init.js | 3 +- .../browser/src/integrations/breadcrumbs.ts | 1 - packages/browser/src/integrations/trycatch.ts | 2 +- packages/core/src/integration.ts | 12 +----- packages/core/src/integrations/metadata.ts | 2 +- packages/core/src/metrics/utils.ts | 2 +- .../create-remix-app-v2/app/entry.server.tsx | 2 +- .../create-remix-app/app/entry.server.tsx | 2 +- .../test-applications/generic-ts3.8/index.ts | 13 +++++- .../tests/async-context-edge.test.ts | 2 +- .../node-hapi-app/tests/server.test.ts | 2 +- .../src/pages/Index.tsx | 1 + .../src/pages/User.tsx | 1 + .../src/pages/Index.tsx | 1 + .../src/pages/User.tsx | 1 + .../src/pages/Index.tsx | 1 + .../src/pages/User.tsx | 1 + .../src/pages/Index.tsx | 1 + .../src/pages/User.tsx | 1 + packages/gatsby/test/integration.test.tsx | 2 +- packages/nextjs/src/common/_error.ts | 2 +- .../templates/routeHandlerWrapperTemplate.ts | 40 +++++++++---------- .../serverComponentWrapperTemplate.ts | 2 + .../src/edge/wrapApiHandlerWithSentry.ts | 2 +- .../test/integration/components/Layout.tsx | 2 +- .../test/integration/components/List.tsx | 1 + .../integration/components/ListDetail.tsx | 1 + .../test/integration/components/ListItem.tsx | 1 + .../src/integrations/http.ts | 3 +- .../src/otel/contextManager.ts | 1 - packages/node-experimental/src/sdk/api.ts | 1 - packages/node-experimental/src/sdk/scope.ts | 2 +- .../test/integration/transactions.test.ts | 1 - .../node-experimental/test/sdk/scope.test.ts | 2 +- .../test/integrations/localvariables.test.ts | 1 - packages/opentelemetry/src/spanExporter.ts | 2 - packages/react/test/profiler.test.tsx | 1 + packages/react/test/reactrouterv4.test.tsx | 1 + packages/react/test/reactrouterv5.test.tsx | 1 + packages/react/test/redux.test.ts | 1 - .../test/integration/app_v2/entry.server.tsx | 2 +- .../server-side-unexpected-errors.$id.tsx | 2 +- .../replay/test/integration/rrweb.test.ts | 1 - packages/sveltekit/src/client/handleError.ts | 2 +- packages/sveltekit/test/common/utils.test.ts | 2 - packages/vue/src/tracing.ts | 2 +- 50 files changed, 73 insertions(+), 67 deletions(-) diff --git a/biome.json b/biome.json index 8e8155f8d6d8..52f6d6728bf4 100644 --- a/biome.json +++ b/biome.json @@ -20,6 +20,9 @@ "suspicious": { "all": false, "noControlCharactersInRegex": "error" + }, + "nursery": { + "noUnusedImports": "error" } }, "ignore": [".vscode/*", "**/*.json"] diff --git a/package.json b/package.json index 080235bde9a9..4fa76ae58198 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,7 @@ "codecov": "codecov", "fix": "run-s fix:lerna fix:biome", "fix:lerna": "lerna run fix", - "fix:biome": "biome check --apply-unsafe .", + "fix:biome": "biome check --apply .", "changelog": "ts-node ./scripts/get-commit-list.ts", "link:yarn": "lerna exec yarn link", "lint": "run-s lint:lerna lint:biome", diff --git a/packages/angular/test/tracing.test.ts b/packages/angular/test/tracing.test.ts index a407539026c5..37ef01348699 100644 --- a/packages/angular/test/tracing.test.ts +++ b/packages/angular/test/tracing.test.ts @@ -1,6 +1,5 @@ import { Component } from '@angular/core'; import type { ActivatedRouteSnapshot, CanActivate, RouterStateSnapshot } from '@angular/router'; -import type { Hub } from '@sentry/types'; import { TraceClassDecorator, TraceDirective, TraceMethodDecorator, instrumentAngularRouting } from '../src'; import { getParameterizedRouteFromSnapshot } from '../src/tracing'; diff --git a/packages/astro/test/server/middleware.test.ts b/packages/astro/test/server/middleware.test.ts index 5e56c6bd70ed..13508cebf057 100644 --- a/packages/astro/test/server/middleware.test.ts +++ b/packages/astro/test/server/middleware.test.ts @@ -1,6 +1,6 @@ import * as SentryNode from '@sentry/node'; import type { Client } from '@sentry/types'; -import { SpyInstance, vi } from 'vitest'; +import { vi } from 'vitest'; import { handleRequest, interpolateRouteFromUrlAndParams } from '../../src/server/middleware'; diff --git a/packages/browser-integration-tests/suites/public-api/startTransaction/init.js b/packages/browser-integration-tests/suites/public-api/startTransaction/init.js index 0aadc7c39b84..e1903e2cc268 100644 --- a/packages/browser-integration-tests/suites/public-api/startTransaction/init.js +++ b/packages/browser-integration-tests/suites/public-api/startTransaction/init.js @@ -1,5 +1,6 @@ +/* eslint-disable no-unused-vars */ import * as Sentry from '@sentry/browser'; -// eslint-disable-next-line no-unused-vars +// biome-ignore lint/nursery/noUnusedImports: Need to import tracing for side effect import * as _ from '@sentry/tracing'; window.Sentry = Sentry; diff --git a/packages/browser/src/integrations/breadcrumbs.ts b/packages/browser/src/integrations/breadcrumbs.ts index c14465b3448b..831059e97756 100644 --- a/packages/browser/src/integrations/breadcrumbs.ts +++ b/packages/browser/src/integrations/breadcrumbs.ts @@ -1,4 +1,3 @@ -import { get } from 'http'; /* eslint-disable max-lines */ import { addBreadcrumb, convertIntegrationFnToClass, getClient } from '@sentry/core'; import type { diff --git a/packages/browser/src/integrations/trycatch.ts b/packages/browser/src/integrations/trycatch.ts index e65190d02b3f..f555acfcdc9c 100644 --- a/packages/browser/src/integrations/trycatch.ts +++ b/packages/browser/src/integrations/trycatch.ts @@ -1,5 +1,5 @@ import { convertIntegrationFnToClass } from '@sentry/core'; -import type { Client, IntegrationFn, WrappedFunction } from '@sentry/types'; +import type { IntegrationFn, WrappedFunction } from '@sentry/types'; import { fill, getFunctionName, getOriginalFunction } from '@sentry/utils'; import { WINDOW, wrap } from '../helpers'; diff --git a/packages/core/src/integration.ts b/packages/core/src/integration.ts index f867ad1d032a..d587ddb55ce8 100644 --- a/packages/core/src/integration.ts +++ b/packages/core/src/integration.ts @@ -1,14 +1,4 @@ -import type { - Client, - Event, - EventHint, - EventProcessor, - Hub, - Integration, - IntegrationClass, - IntegrationFn, - Options, -} from '@sentry/types'; +import type { Client, Event, EventHint, EventProcessor, Hub, Integration, IntegrationFn, Options } from '@sentry/types'; import { arrayify, logger } from '@sentry/utils'; import { DEBUG_BUILD } from './debug-build'; diff --git a/packages/core/src/integrations/metadata.ts b/packages/core/src/integrations/metadata.ts index b94f252b5ce0..e89cffbc8a0a 100644 --- a/packages/core/src/integrations/metadata.ts +++ b/packages/core/src/integrations/metadata.ts @@ -1,4 +1,4 @@ -import type { Event, EventItem, IntegrationFn } from '@sentry/types'; +import type { EventItem, IntegrationFn } from '@sentry/types'; import { forEachEnvelopeItem } from '@sentry/utils'; import { convertIntegrationFnToClass } from '../integration'; diff --git a/packages/core/src/metrics/utils.ts b/packages/core/src/metrics/utils.ts index 27c49d144523..6e4d75fee5f6 100644 --- a/packages/core/src/metrics/utils.ts +++ b/packages/core/src/metrics/utils.ts @@ -1,6 +1,6 @@ import type { MeasurementUnit, MetricBucketItem } from '@sentry/types'; import { dropUndefinedKeys } from '@sentry/utils'; -import type { MetricType, SimpleMetricBucket } from './types'; +import type { MetricType } from './types'; /** * Generate bucket key from metric properties. diff --git a/packages/e2e-tests/test-applications/create-remix-app-v2/app/entry.server.tsx b/packages/e2e-tests/test-applications/create-remix-app-v2/app/entry.server.tsx index 78e0e8940eb5..c2e0a480acab 100644 --- a/packages/e2e-tests/test-applications/create-remix-app-v2/app/entry.server.tsx +++ b/packages/e2e-tests/test-applications/create-remix-app-v2/app/entry.server.tsx @@ -6,7 +6,7 @@ import { PassThrough } from 'node:stream'; -import type { AppLoadContext, DataFunctionArgs, EntryContext } from '@remix-run/node'; +import type { AppLoadContext, EntryContext } from '@remix-run/node'; import { createReadableStreamFromReadable } from '@remix-run/node'; import { installGlobals } from '@remix-run/node'; import { RemixServer } from '@remix-run/react'; diff --git a/packages/e2e-tests/test-applications/create-remix-app/app/entry.server.tsx b/packages/e2e-tests/test-applications/create-remix-app/app/entry.server.tsx index f0c451dcaf55..6d137ec92d53 100644 --- a/packages/e2e-tests/test-applications/create-remix-app/app/entry.server.tsx +++ b/packages/e2e-tests/test-applications/create-remix-app/app/entry.server.tsx @@ -6,7 +6,7 @@ import { PassThrough } from 'node:stream'; -import type { AppLoadContext, DataFunctionArgs, EntryContext } from '@remix-run/node'; +import type { AppLoadContext, EntryContext } from '@remix-run/node'; import { Response } from '@remix-run/node'; import { RemixServer } from '@remix-run/react'; import * as Sentry from '@sentry/remix'; diff --git a/packages/e2e-tests/test-applications/generic-ts3.8/index.ts b/packages/e2e-tests/test-applications/generic-ts3.8/index.ts index 823bd62fe09c..46c0608d641d 100644 --- a/packages/e2e-tests/test-applications/generic-ts3.8/index.ts +++ b/packages/e2e-tests/test-applications/generic-ts3.8/index.ts @@ -1,13 +1,22 @@ -/* eslint-disable @typescript-eslint/no-unused-vars */ -// we need to import the SDK to ensure tsc check the types +// biome-ignore lint/nursery/noUnusedImports: we need to import the SDK to ensure tsc check the types import * as _SentryBrowser from '@sentry/browser'; +// biome-ignore lint/nursery/noUnusedImports: import * as _SentryCore from '@sentry/core'; +// biome-ignore lint/nursery/noUnusedImports: import * as _SentryHub from '@sentry/hub'; +// biome-ignore lint/nursery/noUnusedImports: import * as _SentryIntegrations from '@sentry/integrations'; +// biome-ignore lint/nursery/noUnusedImports: import * as _SentryNode from '@sentry/node'; +// biome-ignore lint/nursery/noUnusedImports: import * as _SentryOpentelemetry from '@sentry/opentelemetry-node'; +// biome-ignore lint/nursery/noUnusedImports: import * as _SentryReplay from '@sentry/replay'; +// biome-ignore lint/nursery/noUnusedImports: import * as _SentryTracing from '@sentry/tracing'; +// biome-ignore lint/nursery/noUnusedImports: import * as _SentryTypes from '@sentry/types'; +// biome-ignore lint/nursery/noUnusedImports: import * as _SentryUtils from '@sentry/utils'; +// biome-ignore lint/nursery/noUnusedImports: import * as _SentryWasm from '@sentry/wasm'; diff --git a/packages/e2e-tests/test-applications/nextjs-app-dir/tests/async-context-edge.test.ts b/packages/e2e-tests/test-applications/nextjs-app-dir/tests/async-context-edge.test.ts index 7ff7116af5d9..1465c560a36c 100644 --- a/packages/e2e-tests/test-applications/nextjs-app-dir/tests/async-context-edge.test.ts +++ b/packages/e2e-tests/test-applications/nextjs-app-dir/tests/async-context-edge.test.ts @@ -1,5 +1,5 @@ import { expect, test } from '@playwright/test'; -import { waitForError, waitForTransaction } from '../event-proxy-server'; +import { waitForTransaction } from '../event-proxy-server'; test('Should allow for async context isolation in the edge SDK', async ({ request }) => { // test.skip(process.env.TEST_ENV === 'development', "Doesn't work in dev mode."); diff --git a/packages/e2e-tests/test-applications/node-hapi-app/tests/server.test.ts b/packages/e2e-tests/test-applications/node-hapi-app/tests/server.test.ts index cbcd99e756d7..061278dded50 100644 --- a/packages/e2e-tests/test-applications/node-hapi-app/tests/server.test.ts +++ b/packages/e2e-tests/test-applications/node-hapi-app/tests/server.test.ts @@ -1,5 +1,5 @@ import { expect, test } from '@playwright/test'; -import axios, { AxiosError, AxiosResponse } from 'axios'; +import axios, { AxiosError } from 'axios'; import { waitForError, waitForTransaction } from '../event-proxy-server'; const authToken = process.env.E2E_TEST_AUTH_TOKEN; diff --git a/packages/e2e-tests/test-applications/react-create-hash-router/src/pages/Index.tsx b/packages/e2e-tests/test-applications/react-create-hash-router/src/pages/Index.tsx index f339eb867d6c..7789a2773224 100644 --- a/packages/e2e-tests/test-applications/react-create-hash-router/src/pages/Index.tsx +++ b/packages/e2e-tests/test-applications/react-create-hash-router/src/pages/Index.tsx @@ -1,4 +1,5 @@ import * as Sentry from '@sentry/react'; +// biome-ignore lint/nursery/noUnusedImports: Need React import for JSX import * as React from 'react'; import { Link } from 'react-router-dom'; diff --git a/packages/e2e-tests/test-applications/react-create-hash-router/src/pages/User.tsx b/packages/e2e-tests/test-applications/react-create-hash-router/src/pages/User.tsx index 671455a92fff..62f0c2d17533 100644 --- a/packages/e2e-tests/test-applications/react-create-hash-router/src/pages/User.tsx +++ b/packages/e2e-tests/test-applications/react-create-hash-router/src/pages/User.tsx @@ -1,3 +1,4 @@ +// biome-ignore lint/nursery/noUnusedImports: Need React import for JSX import * as React from 'react'; const User = () => { diff --git a/packages/e2e-tests/test-applications/react-router-6-use-routes/src/pages/Index.tsx b/packages/e2e-tests/test-applications/react-router-6-use-routes/src/pages/Index.tsx index f339eb867d6c..7789a2773224 100644 --- a/packages/e2e-tests/test-applications/react-router-6-use-routes/src/pages/Index.tsx +++ b/packages/e2e-tests/test-applications/react-router-6-use-routes/src/pages/Index.tsx @@ -1,4 +1,5 @@ import * as Sentry from '@sentry/react'; +// biome-ignore lint/nursery/noUnusedImports: Need React import for JSX import * as React from 'react'; import { Link } from 'react-router-dom'; diff --git a/packages/e2e-tests/test-applications/react-router-6-use-routes/src/pages/User.tsx b/packages/e2e-tests/test-applications/react-router-6-use-routes/src/pages/User.tsx index 671455a92fff..62f0c2d17533 100644 --- a/packages/e2e-tests/test-applications/react-router-6-use-routes/src/pages/User.tsx +++ b/packages/e2e-tests/test-applications/react-router-6-use-routes/src/pages/User.tsx @@ -1,3 +1,4 @@ +// biome-ignore lint/nursery/noUnusedImports: Need React import for JSX import * as React from 'react'; const User = () => { diff --git a/packages/e2e-tests/test-applications/standard-frontend-react-tracing-import/src/pages/Index.tsx b/packages/e2e-tests/test-applications/standard-frontend-react-tracing-import/src/pages/Index.tsx index f339eb867d6c..7789a2773224 100644 --- a/packages/e2e-tests/test-applications/standard-frontend-react-tracing-import/src/pages/Index.tsx +++ b/packages/e2e-tests/test-applications/standard-frontend-react-tracing-import/src/pages/Index.tsx @@ -1,4 +1,5 @@ import * as Sentry from '@sentry/react'; +// biome-ignore lint/nursery/noUnusedImports: Need React import for JSX import * as React from 'react'; import { Link } from 'react-router-dom'; diff --git a/packages/e2e-tests/test-applications/standard-frontend-react-tracing-import/src/pages/User.tsx b/packages/e2e-tests/test-applications/standard-frontend-react-tracing-import/src/pages/User.tsx index 671455a92fff..62f0c2d17533 100644 --- a/packages/e2e-tests/test-applications/standard-frontend-react-tracing-import/src/pages/User.tsx +++ b/packages/e2e-tests/test-applications/standard-frontend-react-tracing-import/src/pages/User.tsx @@ -1,3 +1,4 @@ +// biome-ignore lint/nursery/noUnusedImports: Need React import for JSX import * as React from 'react'; const User = () => { diff --git a/packages/e2e-tests/test-applications/standard-frontend-react/src/pages/Index.tsx b/packages/e2e-tests/test-applications/standard-frontend-react/src/pages/Index.tsx index f339eb867d6c..7789a2773224 100644 --- a/packages/e2e-tests/test-applications/standard-frontend-react/src/pages/Index.tsx +++ b/packages/e2e-tests/test-applications/standard-frontend-react/src/pages/Index.tsx @@ -1,4 +1,5 @@ import * as Sentry from '@sentry/react'; +// biome-ignore lint/nursery/noUnusedImports: Need React import for JSX import * as React from 'react'; import { Link } from 'react-router-dom'; diff --git a/packages/e2e-tests/test-applications/standard-frontend-react/src/pages/User.tsx b/packages/e2e-tests/test-applications/standard-frontend-react/src/pages/User.tsx index 671455a92fff..62f0c2d17533 100644 --- a/packages/e2e-tests/test-applications/standard-frontend-react/src/pages/User.tsx +++ b/packages/e2e-tests/test-applications/standard-frontend-react/src/pages/User.tsx @@ -1,3 +1,4 @@ +// biome-ignore lint/nursery/noUnusedImports: Need React import for JSX import * as React from 'react'; const User = () => { diff --git a/packages/gatsby/test/integration.test.tsx b/packages/gatsby/test/integration.test.tsx index 88a736ceee75..dbe10d9a3fbb 100644 --- a/packages/gatsby/test/integration.test.tsx +++ b/packages/gatsby/test/integration.test.tsx @@ -2,7 +2,7 @@ import { TextDecoder, TextEncoder } from 'util'; /* eslint-disable @typescript-eslint/no-explicit-any */ import { render } from '@testing-library/react'; import { useEffect } from 'react'; -// eslint-disable-next-line @typescript-eslint/no-unused-vars +// biome-ignore lint/nursery/noUnusedImports: Need React import for JSX import * as React from 'react'; import { onClientEntry } from '../gatsby-browser'; diff --git a/packages/nextjs/src/common/_error.ts b/packages/nextjs/src/common/_error.ts index 1f114494567b..f79c844adba7 100644 --- a/packages/nextjs/src/common/_error.ts +++ b/packages/nextjs/src/common/_error.ts @@ -1,4 +1,4 @@ -import { captureException, getClient, withScope } from '@sentry/core'; +import { captureException, withScope } from '@sentry/core'; import type { NextPageContext } from 'next'; import { flushQueue } from './utils/responseEnd'; diff --git a/packages/nextjs/src/config/templates/routeHandlerWrapperTemplate.ts b/packages/nextjs/src/config/templates/routeHandlerWrapperTemplate.ts index e8fafc18139d..346b2c29a784 100644 --- a/packages/nextjs/src/config/templates/routeHandlerWrapperTemplate.ts +++ b/packages/nextjs/src/config/templates/routeHandlerWrapperTemplate.ts @@ -8,18 +8,6 @@ import * as routeModule from '__SENTRY_WRAPPING_TARGET_FILE__'; import type { RequestAsyncStorage } from './requestAsyncStorageShim'; -declare const requestAsyncStorage: RequestAsyncStorage; - -declare const routeModule: { - GET?: (...args: unknown[]) => unknown; - POST?: (...args: unknown[]) => unknown; - PUT?: (...args: unknown[]) => unknown; - PATCH?: (...args: unknown[]) => unknown; - DELETE?: (...args: unknown[]) => unknown; - HEAD?: (...args: unknown[]) => unknown; - OPTIONS?: (...args: unknown[]) => unknown; -}; - function wrapHandler(handler: T, method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'HEAD' | 'OPTIONS'): T { // Running the instrumentation code during the build phase will mark any function as "dynamic" because we're accessing // the Request object. We do not want to turn handlers dynamic so we skip instrumentation in the build phase. @@ -39,7 +27,8 @@ function wrapHandler(handler: T, method: 'GET' | 'POST' | 'PUT' | 'PATCH' | ' // We try-catch here just in case the API around `requestAsyncStorage` changes unexpectedly since it is not public API try { - const requestAsyncStore = requestAsyncStorage.getStore(); + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + const requestAsyncStore = requestAsyncStorage.getStore() as ReturnType; sentryTraceHeader = requestAsyncStore?.headers.get('sentry-trace') ?? undefined; baggageHeader = requestAsyncStore?.headers.get('baggage') ?? undefined; headers = requestAsyncStore?.headers; @@ -65,10 +54,21 @@ export * from '__SENTRY_WRAPPING_TARGET_FILE__'; // @ts-expect-error This is the file we're wrapping export { default } from '__SENTRY_WRAPPING_TARGET_FILE__'; -export const GET = wrapHandler(routeModule.GET, 'GET'); -export const POST = wrapHandler(routeModule.POST, 'POST'); -export const PUT = wrapHandler(routeModule.PUT, 'PUT'); -export const PATCH = wrapHandler(routeModule.PATCH, 'PATCH'); -export const DELETE = wrapHandler(routeModule.DELETE, 'DELETE'); -export const HEAD = wrapHandler(routeModule.HEAD, 'HEAD'); -export const OPTIONS = wrapHandler(routeModule.OPTIONS, 'OPTIONS'); +declare const requestAsyncStorage: RequestAsyncStorage; + +type RouteHandler = (...args: unknown[]) => unknown; + +// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access +export const GET = wrapHandler(routeModule.GET as RouteHandler, 'GET'); +// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access +export const POST = wrapHandler(routeModule.POST as RouteHandler, 'POST'); +// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access +export const PUT = wrapHandler(routeModule.PUT as RouteHandler, 'PUT'); +// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access +export const PATCH = wrapHandler(routeModule.PATCH as RouteHandler, 'PATCH'); +// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access +export const DELETE = wrapHandler(routeModule.DELETE as RouteHandler, 'DELETE'); +// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access +export const HEAD = wrapHandler(routeModule.HEAD as RouteHandler, 'HEAD'); +// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access +export const OPTIONS = wrapHandler(routeModule.OPTIONS as RouteHandler, 'OPTIONS'); diff --git a/packages/nextjs/src/config/templates/serverComponentWrapperTemplate.ts b/packages/nextjs/src/config/templates/serverComponentWrapperTemplate.ts index 56b9853fa1af..717826e3a081 100644 --- a/packages/nextjs/src/config/templates/serverComponentWrapperTemplate.ts +++ b/packages/nextjs/src/config/templates/serverComponentWrapperTemplate.ts @@ -2,8 +2,10 @@ import * as Sentry from '@sentry/nextjs'; import type { WebFetchHeaders } from '@sentry/types'; // @ts-expect-error Because we cannot be sure if the RequestAsyncStorage module exists (it is not part of the Next.js public // API) we use a shim if it doesn't exist. The logic for this is in the wrapping loader. +// biome-ignore lint/nursery/noUnusedImports: Biome doesn't understand the shim with variable import path import { requestAsyncStorage } from '__SENTRY_NEXTJS_REQUEST_ASYNC_STORAGE_SHIM__'; // @ts-expect-error We use `__SENTRY_WRAPPING_TARGET_FILE__` as a placeholder for the path to the file being wrapped. +// biome-ignore lint/nursery/noUnusedImports: Biome doesn't understand the shim with variable import path import * as serverComponentModule from '__SENTRY_WRAPPING_TARGET_FILE__'; import type { RequestAsyncStorage } from './requestAsyncStorageShim'; diff --git a/packages/nextjs/src/edge/wrapApiHandlerWithSentry.ts b/packages/nextjs/src/edge/wrapApiHandlerWithSentry.ts index 5a9398319ae2..dabba7741e01 100644 --- a/packages/nextjs/src/edge/wrapApiHandlerWithSentry.ts +++ b/packages/nextjs/src/edge/wrapApiHandlerWithSentry.ts @@ -1,4 +1,4 @@ -import { getCurrentHub, getCurrentScope } from '@sentry/core'; +import { getCurrentScope } from '@sentry/core'; import { withEdgeWrapping } from '../common/utils/edgeWrapperUtils'; import type { EdgeRouteHandler } from './types'; diff --git a/packages/nextjs/test/integration/components/Layout.tsx b/packages/nextjs/test/integration/components/Layout.tsx index 25db194506fa..36b99f04720f 100644 --- a/packages/nextjs/test/integration/components/Layout.tsx +++ b/packages/nextjs/test/integration/components/Layout.tsx @@ -1,6 +1,6 @@ import Head from 'next/head'; import Link from 'next/link'; -import React, { ReactNode } from 'react'; +import { ReactNode } from 'react'; type Props = { children?: ReactNode; diff --git a/packages/nextjs/test/integration/components/List.tsx b/packages/nextjs/test/integration/components/List.tsx index e5c88d3e4b42..fbcdfeb504c9 100644 --- a/packages/nextjs/test/integration/components/List.tsx +++ b/packages/nextjs/test/integration/components/List.tsx @@ -1,3 +1,4 @@ +// biome-ignore lint/nursery/noUnusedImports: Need React import for JSX import * as React from 'react'; import { User } from '../interfaces'; import ListItem from './ListItem'; diff --git a/packages/nextjs/test/integration/components/ListDetail.tsx b/packages/nextjs/test/integration/components/ListDetail.tsx index 0443bb33d317..9b1417333c9f 100644 --- a/packages/nextjs/test/integration/components/ListDetail.tsx +++ b/packages/nextjs/test/integration/components/ListDetail.tsx @@ -1,3 +1,4 @@ +// biome-ignore lint/nursery/noUnusedImports: Need React import for JSX import * as React from 'react'; import { User } from '../interfaces'; diff --git a/packages/nextjs/test/integration/components/ListItem.tsx b/packages/nextjs/test/integration/components/ListItem.tsx index 2f5d131ae3ad..9de97d32c7fb 100644 --- a/packages/nextjs/test/integration/components/ListItem.tsx +++ b/packages/nextjs/test/integration/components/ListItem.tsx @@ -1,4 +1,5 @@ import Link from 'next/link'; +// biome-ignore lint/nursery/noUnusedImports: Need React import for JSX import React from 'react'; import { User } from '../interfaces'; diff --git a/packages/node-experimental/src/integrations/http.ts b/packages/node-experimental/src/integrations/http.ts index 4588d1b36b15..66606bbf8258 100644 --- a/packages/node-experimental/src/integrations/http.ts +++ b/packages/node-experimental/src/integrations/http.ts @@ -4,12 +4,11 @@ import { SpanKind } from '@opentelemetry/api'; import { registerInstrumentations } from '@opentelemetry/instrumentation'; import { HttpInstrumentation } from '@opentelemetry/instrumentation-http'; import { addBreadcrumb, hasTracingEnabled, isSentryRequestUrl } from '@sentry/core'; -import { _INTERNAL, getClient, getCurrentHub, getSpanKind, setSpanMetadata } from '@sentry/opentelemetry'; +import { _INTERNAL, getClient, getSpanKind, setSpanMetadata } from '@sentry/opentelemetry'; import type { EventProcessor, Hub, Integration } from '@sentry/types'; import { stringMatchesSomePattern } from '@sentry/utils'; import { getIsolationScope, setIsolationScope } from '../sdk/api'; -import { Scope } from '../sdk/scope'; import type { NodeExperimentalClient } from '../types'; import { addOriginToSpan } from '../utils/addOriginToSpan'; import { getRequestUrl } from '../utils/getRequestUrl'; diff --git a/packages/node-experimental/src/otel/contextManager.ts b/packages/node-experimental/src/otel/contextManager.ts index 4ba4f0642b16..a7154fb96390 100644 --- a/packages/node-experimental/src/otel/contextManager.ts +++ b/packages/node-experimental/src/otel/contextManager.ts @@ -4,7 +4,6 @@ import { setHubOnContext } from '@sentry/opentelemetry'; import { getCurrentHub } from '../sdk/hub'; import { getCurrentScope, getIsolationScope } from './../sdk/api'; -import { Scope } from './../sdk/scope'; import type { CurrentScopes } from './../sdk/types'; import { getScopesFromContext, setScopesOnContext } from './../utils/contextData'; diff --git a/packages/node-experimental/src/sdk/api.ts b/packages/node-experimental/src/sdk/api.ts index 1a7ddfd52ad5..9ef8862aa88b 100644 --- a/packages/node-experimental/src/sdk/api.ts +++ b/packages/node-experimental/src/sdk/api.ts @@ -6,7 +6,6 @@ import type { Breadcrumb, BreadcrumbHint, CaptureContext, - Client, Event, EventHint, EventProcessor, diff --git a/packages/node-experimental/src/sdk/scope.ts b/packages/node-experimental/src/sdk/scope.ts index 86bad56e932e..5b9d56dc4b84 100644 --- a/packages/node-experimental/src/sdk/scope.ts +++ b/packages/node-experimental/src/sdk/scope.ts @@ -1,4 +1,4 @@ -import { getGlobalScope as _getGlobalScope, mergeScopeData, setGlobalScope } from '@sentry/core'; +import { getGlobalScope as _getGlobalScope, setGlobalScope } from '@sentry/core'; import { OpenTelemetryScope } from '@sentry/opentelemetry'; import type { Breadcrumb, Client, Event, EventHint, Severity, SeverityLevel } from '@sentry/types'; import { uuid4 } from '@sentry/utils'; diff --git a/packages/node-experimental/test/integration/transactions.test.ts b/packages/node-experimental/test/integration/transactions.test.ts index 1a09b3234d92..be48f5f9e6b5 100644 --- a/packages/node-experimental/test/integration/transactions.test.ts +++ b/packages/node-experimental/test/integration/transactions.test.ts @@ -8,7 +8,6 @@ import { logger } from '@sentry/utils'; import * as Sentry from '../../src'; import { startSpan } from '../../src'; import type { Http, NodeFetch } from '../../src/integrations'; -import { getIsolationScope } from '../../src/sdk/api'; import type { NodeExperimentalClient } from '../../src/types'; import { cleanupOtel, getProvider, mockSdkInit } from '../helpers/mockSdkInit'; diff --git a/packages/node-experimental/test/sdk/scope.test.ts b/packages/node-experimental/test/sdk/scope.test.ts index 6e1245c35aed..e3919654e920 100644 --- a/packages/node-experimental/test/sdk/scope.test.ts +++ b/packages/node-experimental/test/sdk/scope.test.ts @@ -2,7 +2,7 @@ import { prepareEvent } from '@sentry/core'; import type { Attachment, Breadcrumb, Client, ClientOptions, EventProcessor } from '@sentry/types'; import { Scope, getIsolationScope } from '../../src'; import { getGlobalScope } from '../../src/sdk/scope'; -import { mockSdkInit, resetGlobals } from '../helpers/mockSdkInit'; +import { mockSdkInit } from '../helpers/mockSdkInit'; describe('Unit | Scope', () => { it('allows to create & update a scope', () => { diff --git a/packages/node/test/integrations/localvariables.test.ts b/packages/node/test/integrations/localvariables.test.ts index 6a09111370a8..027ccc391c6d 100644 --- a/packages/node/test/integrations/localvariables.test.ts +++ b/packages/node/test/integrations/localvariables.test.ts @@ -1,4 +1,3 @@ -import type { ClientOptions, EventProcessor } from '@sentry/types'; import type { LRUMap } from '@sentry/utils'; import type { Debugger, InspectorNotification } from 'inspector'; diff --git a/packages/opentelemetry/src/spanExporter.ts b/packages/opentelemetry/src/spanExporter.ts index fdb63636452f..8c515fc0afc9 100644 --- a/packages/opentelemetry/src/spanExporter.ts +++ b/packages/opentelemetry/src/spanExporter.ts @@ -1,4 +1,3 @@ -import type { Span } from '@opentelemetry/api'; import { SpanKind } from '@opentelemetry/api'; import type { ExportResult } from '@opentelemetry/core'; import { ExportResultCode } from '@opentelemetry/core'; @@ -9,7 +8,6 @@ import type { DynamicSamplingContext, Span as SentrySpan, SpanOrigin, Transactio import { logger } from '@sentry/utils'; import { getCurrentHub } from './custom/hub'; -import { OpenTelemetryScope } from './custom/scope'; import type { OpenTelemetryTransaction } from './custom/transaction'; import { startTransaction } from './custom/transaction'; import { DEBUG_BUILD } from './debug-build'; diff --git a/packages/react/test/profiler.test.tsx b/packages/react/test/profiler.test.tsx index 02a27836beec..03a8c7ed20dd 100644 --- a/packages/react/test/profiler.test.tsx +++ b/packages/react/test/profiler.test.tsx @@ -1,6 +1,7 @@ import type { SpanContext } from '@sentry/types'; import { render } from '@testing-library/react'; import { renderHook } from '@testing-library/react-hooks'; +// biome-ignore lint/nursery/noUnusedImports: Need React import for JSX import * as React from 'react'; import { REACT_MOUNT_OP, REACT_RENDER_OP, REACT_UPDATE_OP } from '../src/constants'; diff --git a/packages/react/test/reactrouterv4.test.tsx b/packages/react/test/reactrouterv4.test.tsx index 1d3d4df4f170..a17c885b5edf 100644 --- a/packages/react/test/reactrouterv4.test.tsx +++ b/packages/react/test/reactrouterv4.test.tsx @@ -1,5 +1,6 @@ import { act, render } from '@testing-library/react'; import { createMemoryHistory } from 'history-4'; +// biome-ignore lint/nursery/noUnusedImports: Need React import for JSX import * as React from 'react'; import { Route, Router, Switch, matchPath } from 'react-router-4'; diff --git a/packages/react/test/reactrouterv5.test.tsx b/packages/react/test/reactrouterv5.test.tsx index e61306bc8b40..104374201722 100644 --- a/packages/react/test/reactrouterv5.test.tsx +++ b/packages/react/test/reactrouterv5.test.tsx @@ -1,5 +1,6 @@ import { act, render } from '@testing-library/react'; import { createMemoryHistory } from 'history-4'; +// biome-ignore lint/nursery/noUnusedImports: Need React import for JSX import * as React from 'react'; import { Route, Router, Switch, matchPath } from 'react-router-5'; diff --git a/packages/react/test/redux.test.ts b/packages/react/test/redux.test.ts index 60cf59abd74e..0ce064365eeb 100644 --- a/packages/react/test/redux.test.ts +++ b/packages/react/test/redux.test.ts @@ -1,5 +1,4 @@ import * as Sentry from '@sentry/browser'; -import type { Scope } from '@sentry/types'; import * as Redux from 'redux'; import { createReduxEnhancer } from '../src/redux'; diff --git a/packages/remix/test/integration/app_v2/entry.server.tsx b/packages/remix/test/integration/app_v2/entry.server.tsx index f9205ecf89b2..bba366801092 100644 --- a/packages/remix/test/integration/app_v2/entry.server.tsx +++ b/packages/remix/test/integration/app_v2/entry.server.tsx @@ -1,4 +1,4 @@ -import type { DataFunctionArgs, EntryContext } from '@remix-run/node'; +import type { EntryContext } from '@remix-run/node'; import { RemixServer } from '@remix-run/react'; import * as Sentry from '@sentry/remix'; import { renderToString } from 'react-dom/server'; diff --git a/packages/remix/test/integration/common/routes/server-side-unexpected-errors.$id.tsx b/packages/remix/test/integration/common/routes/server-side-unexpected-errors.$id.tsx index a7d73e29a4ff..a6dbf6cfb0f0 100644 --- a/packages/remix/test/integration/common/routes/server-side-unexpected-errors.$id.tsx +++ b/packages/remix/test/integration/common/routes/server-side-unexpected-errors.$id.tsx @@ -1,4 +1,4 @@ -import { ActionFunction, LoaderFunction, json, redirect } from '@remix-run/node'; +import { ActionFunction } from '@remix-run/node'; import { useActionData } from '@remix-run/react'; export const action: ActionFunction = async ({ params: { id } }) => { diff --git a/packages/replay/test/integration/rrweb.test.ts b/packages/replay/test/integration/rrweb.test.ts index 3543a6771ce4..2e648358ffe3 100644 --- a/packages/replay/test/integration/rrweb.test.ts +++ b/packages/replay/test/integration/rrweb.test.ts @@ -1,4 +1,3 @@ -import type { CanvasManagerInterface } from '../../src/types'; import { resetSdkMock } from '../mocks/resetSdkMock'; import { useFakeTimers } from '../utils/use-fake-timers'; diff --git a/packages/sveltekit/src/client/handleError.ts b/packages/sveltekit/src/client/handleError.ts index 799e6e36db72..0880d93a59ef 100644 --- a/packages/sveltekit/src/client/handleError.ts +++ b/packages/sveltekit/src/client/handleError.ts @@ -1,6 +1,6 @@ import { captureException } from '@sentry/svelte'; import { consoleSandbox } from '@sentry/utils'; -import type { HandleClientError, NavigationEvent } from '@sveltejs/kit'; +import type { HandleClientError } from '@sveltejs/kit'; // The SvelteKit default error handler just logs the error to the console // see: https://github.com/sveltejs/kit/blob/369e7d6851f543a40c947e033bfc4a9506fdc0a8/packages/kit/src/core/sync/write_client_manifest.js#LL127C2-L127C2 diff --git a/packages/sveltekit/test/common/utils.test.ts b/packages/sveltekit/test/common/utils.test.ts index f048494881ec..0b0352042164 100644 --- a/packages/sveltekit/test/common/utils.test.ts +++ b/packages/sveltekit/test/common/utils.test.ts @@ -1,5 +1,3 @@ -import { redirect } from '@sveltejs/kit'; - import { isHttpError, isRedirect } from '../../src/common/utils'; describe('isRedirect', () => { diff --git a/packages/vue/src/tracing.ts b/packages/vue/src/tracing.ts index 810fa40b045d..af93701e74a7 100644 --- a/packages/vue/src/tracing.ts +++ b/packages/vue/src/tracing.ts @@ -1,4 +1,4 @@ -import { getCurrentHub, getCurrentScope } from '@sentry/browser'; +import { getCurrentScope } from '@sentry/browser'; import type { Span, Transaction } from '@sentry/types'; import { logger, timestampInSeconds } from '@sentry/utils'; From 7f8eca7aa7695b32a5d5cd4b12ad3243c8e1f656 Mon Sep 17 00:00:00 2001 From: Yagiz Nizipli Date: Thu, 21 Dec 2023 15:07:55 -0500 Subject: [PATCH 20/24] feat: Add server runtime metrics aggregator (#9894) ## Implements - [x] 10 Second Bucketing: SDKs are required to bucket into 10 second intervals (rollup in seconds) which is the current lower bound of metric accuracy. - [x] Flush Shift: SDKs are required to shift the flush interval by random() * rollup_in_seconds. That shift is determined once per startup to create jittering. - [ ] Force flush: an SDK is required to perform force flushing ahead of scheduled time if the memory pressure is too high. There is no rule for this other than that SDKs should be tracking abstract aggregation complexity (eg: a counter only carries a single float, whereas a distribution is a float per emission). ## Caveats - Force flush requires Node.js 14+ support (FinalizationRegistry). I recommend leaving it after v8 release to make the implementation a lot easier. --------- Co-authored-by: Abhijeet Prasad --- package.json | 4 +- packages/bun/src/index.ts | 1 + packages/core/src/baseclient.ts | 1 + packages/core/src/metrics/aggregator.ts | 163 ++++++++++++++++++ .../core/src/metrics/browser-aggregator.ts | 92 ++++++++++ packages/core/src/metrics/constants.ts | 13 +- packages/core/src/metrics/envelope.ts | 2 +- packages/core/src/metrics/exports.ts | 10 +- packages/core/src/metrics/instance.ts | 26 ++- packages/core/src/metrics/integration.ts | 4 +- packages/core/src/metrics/simpleaggregator.ts | 91 ---------- packages/core/src/metrics/types.ts | 4 +- packages/core/src/metrics/utils.ts | 30 +++- packages/core/src/server-runtime-client.ts | 5 + .../core/test/lib/metrics/aggregator.test.ts | 157 +++++++++++++++++ ...tor.test.ts => browser-aggregator.test.ts} | 33 ++-- packages/deno/src/index.ts | 1 + packages/node/src/index.ts | 1 + packages/types/src/metrics.ts | 38 ++-- packages/vercel-edge/src/index.ts | 1 + 20 files changed, 540 insertions(+), 137 deletions(-) create mode 100644 packages/core/src/metrics/aggregator.ts create mode 100644 packages/core/src/metrics/browser-aggregator.ts delete mode 100644 packages/core/src/metrics/simpleaggregator.ts create mode 100644 packages/core/test/lib/metrics/aggregator.test.ts rename packages/core/test/lib/metrics/{simpleaggregator.test.ts => browser-aggregator.test.ts} (71%) diff --git a/package.json b/package.json index 4fa76ae58198..e1d8309637c6 100644 --- a/package.json +++ b/package.json @@ -18,12 +18,12 @@ "clean:deps": "lerna clean --yes && rm -rf node_modules && yarn", "clean:all": "run-s clean:build clean:caches clean:deps", "codecov": "codecov", - "fix": "run-s fix:lerna fix:biome", + "fix": "run-p fix:lerna fix:biome", "fix:lerna": "lerna run fix", "fix:biome": "biome check --apply .", "changelog": "ts-node ./scripts/get-commit-list.ts", "link:yarn": "lerna exec yarn link", - "lint": "run-s lint:lerna lint:biome", + "lint": "run-p lint:lerna lint:biome", "lint:lerna": "lerna run lint", "lint:biome": "biome check .", "validate:es5": "lerna run validate:es5", diff --git a/packages/bun/src/index.ts b/packages/bun/src/index.ts index 0166b43d137d..bc29dcd908b5 100644 --- a/packages/bun/src/index.ts +++ b/packages/bun/src/index.ts @@ -70,6 +70,7 @@ export { startInactiveSpan, startSpanManual, continueTrace, + metrics, } from '@sentry/core'; export type { SpanStatusType } from '@sentry/core'; export { autoDiscoverNodePerformanceMonitoringIntegrations } from '@sentry/node'; diff --git a/packages/core/src/baseclient.ts b/packages/core/src/baseclient.ts index 865fdcbd14b5..75b736bbf803 100644 --- a/packages/core/src/baseclient.ts +++ b/packages/core/src/baseclient.ts @@ -407,6 +407,7 @@ export abstract class BaseClient implements Client { * @inheritDoc */ public captureAggregateMetrics(metricBucketItems: Array): void { + DEBUG_BUILD && logger.log(`Flushing aggregated metrics, number of metrics: ${metricBucketItems.length}`); const metricsEnvelope = createMetricEnvelope( metricBucketItems, this._dsn, diff --git a/packages/core/src/metrics/aggregator.ts b/packages/core/src/metrics/aggregator.ts new file mode 100644 index 000000000000..6a49fda5918b --- /dev/null +++ b/packages/core/src/metrics/aggregator.ts @@ -0,0 +1,163 @@ +import type { + Client, + ClientOptions, + MeasurementUnit, + MetricsAggregator as MetricsAggregatorBase, + Primitive, +} from '@sentry/types'; +import { timestampInSeconds } from '@sentry/utils'; +import { DEFAULT_FLUSH_INTERVAL, MAX_WEIGHT, NAME_AND_TAG_KEY_NORMALIZATION_REGEX } from './constants'; +import { METRIC_MAP } from './instance'; +import type { MetricBucket, MetricType } from './types'; +import { getBucketKey, sanitizeTags } from './utils'; + +/** + * A metrics aggregator that aggregates metrics in memory and flushes them periodically. + */ +export class MetricsAggregator implements MetricsAggregatorBase { + // TODO(@anonrig): Use FinalizationRegistry to have a proper way of flushing the buckets + // when the aggregator is garbage collected. + // Ref: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/FinalizationRegistry + private _buckets: MetricBucket; + + // Different metrics have different weights. We use this to limit the number of metrics + // that we store in memory. + private _bucketsTotalWeight; + + private readonly _interval: ReturnType; + + // SDKs are required to shift the flush interval by random() * rollup_in_seconds. + // That shift is determined once per startup to create jittering. + private readonly _flushShift: number; + + // An SDK is required to perform force flushing ahead of scheduled time if the memory + // pressure is too high. There is no rule for this other than that SDKs should be tracking + // abstract aggregation complexity (eg: a counter only carries a single float, whereas a + // distribution is a float per emission). + // + // Force flush is used on either shutdown, flush() or when we exceed the max weight. + private _forceFlush: boolean; + + public constructor(private readonly _client: Client) { + this._buckets = new Map(); + this._bucketsTotalWeight = 0; + this._interval = setInterval(() => this._flush(), DEFAULT_FLUSH_INTERVAL); + this._flushShift = Math.floor((Math.random() * DEFAULT_FLUSH_INTERVAL) / 1000); + this._forceFlush = false; + } + + /** + * @inheritDoc + */ + public add( + metricType: MetricType, + unsanitizedName: string, + value: number | string, + unit: MeasurementUnit = 'none', + unsanitizedTags: Record = {}, + maybeFloatTimestamp = timestampInSeconds(), + ): void { + const timestamp = Math.floor(maybeFloatTimestamp); + const name = unsanitizedName.replace(NAME_AND_TAG_KEY_NORMALIZATION_REGEX, '_'); + const tags = sanitizeTags(unsanitizedTags); + + const bucketKey = getBucketKey(metricType, name, unit, tags); + let bucketItem = this._buckets.get(bucketKey); + if (bucketItem) { + bucketItem.metric.add(value); + // TODO(abhi): Do we need this check? + if (bucketItem.timestamp < timestamp) { + bucketItem.timestamp = timestamp; + } + } else { + bucketItem = { + // @ts-expect-error we don't need to narrow down the type of value here, saves bundle size. + metric: new METRIC_MAP[metricType](value), + timestamp, + metricType, + name, + unit, + tags, + }; + this._buckets.set(bucketKey, bucketItem); + } + + // We need to keep track of the total weight of the buckets so that we can + // flush them when we exceed the max weight. + this._bucketsTotalWeight += bucketItem.metric.weight; + + if (this._bucketsTotalWeight >= MAX_WEIGHT) { + this.flush(); + } + } + + /** + * Flushes the current metrics to the transport via the transport. + */ + public flush(): void { + this._forceFlush = true; + this._flush(); + } + + /** + * Shuts down metrics aggregator and clears all metrics. + */ + public close(): void { + this._forceFlush = true; + clearInterval(this._interval); + this._flush(); + } + + /** + * Flushes the buckets according to the internal state of the aggregator. + * If it is a force flush, which happens on shutdown, it will flush all buckets. + * Otherwise, it will only flush buckets that are older than the flush interval, + * and according to the flush shift. + * + * This function mutates `_forceFlush` and `_bucketsTotalWeight` properties. + */ + private _flush(): void { + // TODO(@anonrig): Add Atomics for locking to avoid having force flush and regular flush + // running at the same time. + // Ref: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Atomics + + // This path eliminates the need for checking for timestamps since we're forcing a flush. + // Remember to reset the flag, or it will always flush all metrics. + if (this._forceFlush) { + this._forceFlush = false; + this._bucketsTotalWeight = 0; + this._captureMetrics(this._buckets); + this._buckets.clear(); + return; + } + const cutoffSeconds = Math.floor(timestampInSeconds()) - DEFAULT_FLUSH_INTERVAL / 1000 - this._flushShift; + // TODO(@anonrig): Optimization opportunity. + // Convert this map to an array and store key in the bucketItem. + const flushedBuckets: MetricBucket = new Map(); + for (const [key, bucket] of this._buckets) { + if (bucket.timestamp <= cutoffSeconds) { + flushedBuckets.set(key, bucket); + this._bucketsTotalWeight -= bucket.metric.weight; + } + } + + for (const [key] of flushedBuckets) { + this._buckets.delete(key); + } + + this._captureMetrics(flushedBuckets); + } + + /** + * Only captures a subset of the buckets passed to this function. + * @param flushedBuckets + */ + private _captureMetrics(flushedBuckets: MetricBucket): void { + if (flushedBuckets.size > 0 && this._client.captureAggregateMetrics) { + // TODO(@anonrig): Optimization opportunity. + // This copy operation can be avoided if we store the key in the bucketItem. + const buckets = Array.from(flushedBuckets).map(([, bucketItem]) => bucketItem); + this._client.captureAggregateMetrics(buckets); + } + } +} diff --git a/packages/core/src/metrics/browser-aggregator.ts b/packages/core/src/metrics/browser-aggregator.ts new file mode 100644 index 000000000000..5b5c81353024 --- /dev/null +++ b/packages/core/src/metrics/browser-aggregator.ts @@ -0,0 +1,92 @@ +import type { + Client, + ClientOptions, + MeasurementUnit, + MetricBucketItem, + MetricsAggregator, + Primitive, +} from '@sentry/types'; +import { timestampInSeconds } from '@sentry/utils'; +import { DEFAULT_BROWSER_FLUSH_INTERVAL, NAME_AND_TAG_KEY_NORMALIZATION_REGEX } from './constants'; +import { METRIC_MAP } from './instance'; +import type { MetricBucket, MetricType } from './types'; +import { getBucketKey, sanitizeTags } from './utils'; + +/** + * A simple metrics aggregator that aggregates metrics in memory and flushes them periodically. + * Default flush interval is 5 seconds. + * + * @experimental This API is experimental and might change in the future. + */ +export class BrowserMetricsAggregator implements MetricsAggregator { + // TODO(@anonrig): Use FinalizationRegistry to have a proper way of flushing the buckets + // when the aggregator is garbage collected. + // Ref: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/FinalizationRegistry + private _buckets: MetricBucket; + private readonly _interval: ReturnType; + + public constructor(private readonly _client: Client) { + this._buckets = new Map(); + this._interval = setInterval(() => this.flush(), DEFAULT_BROWSER_FLUSH_INTERVAL); + } + + /** + * @inheritDoc + */ + public add( + metricType: MetricType, + unsanitizedName: string, + value: number | string, + unit: MeasurementUnit | undefined = 'none', + unsanitizedTags: Record | undefined = {}, + maybeFloatTimestamp: number | undefined = timestampInSeconds(), + ): void { + const timestamp = Math.floor(maybeFloatTimestamp); + const name = unsanitizedName.replace(NAME_AND_TAG_KEY_NORMALIZATION_REGEX, '_'); + const tags = sanitizeTags(unsanitizedTags); + + const bucketKey = getBucketKey(metricType, name, unit, tags); + const bucketItem: MetricBucketItem | undefined = this._buckets.get(bucketKey); + if (bucketItem) { + bucketItem.metric.add(value); + // TODO(abhi): Do we need this check? + if (bucketItem.timestamp < timestamp) { + bucketItem.timestamp = timestamp; + } + } else { + this._buckets.set(bucketKey, { + // @ts-expect-error we don't need to narrow down the type of value here, saves bundle size. + metric: new METRIC_MAP[metricType](value), + timestamp, + metricType, + name, + unit, + tags, + }); + } + } + + /** + * @inheritDoc + */ + public flush(): void { + // short circuit if buckets are empty. + if (this._buckets.size === 0) { + return; + } + if (this._client.captureAggregateMetrics) { + // TODO(@anonrig): Use Object.values() when we support ES6+ + const metricBuckets = Array.from(this._buckets).map(([, bucketItem]) => bucketItem); + this._client.captureAggregateMetrics(metricBuckets); + } + this._buckets.clear(); + } + + /** + * @inheritDoc + */ + public close(): void { + clearInterval(this._interval); + this.flush(); + } +} diff --git a/packages/core/src/metrics/constants.ts b/packages/core/src/metrics/constants.ts index f29ac323c2ee..e89e0fd1562b 100644 --- a/packages/core/src/metrics/constants.ts +++ b/packages/core/src/metrics/constants.ts @@ -27,4 +27,15 @@ export const TAG_VALUE_NORMALIZATION_REGEX = /[^\w\d_:/@.{}[\]$-]+/g; * This does not match spec in https://develop.sentry.dev/sdk/metrics * but was chosen to optimize for the most common case in browser environments. */ -export const DEFAULT_FLUSH_INTERVAL = 5000; +export const DEFAULT_BROWSER_FLUSH_INTERVAL = 5000; + +/** + * SDKs are required to bucket into 10 second intervals (rollup in seconds) + * which is the current lower bound of metric accuracy. + */ +export const DEFAULT_FLUSH_INTERVAL = 10000; + +/** + * The maximum number of metrics that should be stored in memory. + */ +export const MAX_WEIGHT = 10000; diff --git a/packages/core/src/metrics/envelope.ts b/packages/core/src/metrics/envelope.ts index c7c65674b736..95622e109740 100644 --- a/packages/core/src/metrics/envelope.ts +++ b/packages/core/src/metrics/envelope.ts @@ -30,7 +30,7 @@ export function createMetricEnvelope( return createEnvelope(headers, [item]); } -function createMetricEnvelopeItem(metricBucketItems: Array): StatsdItem { +function createMetricEnvelopeItem(metricBucketItems: MetricBucketItem[]): StatsdItem { const payload = serializeMetricBuckets(metricBucketItems); const metricHeaders: StatsdItem[0] = { type: 'statsd', diff --git a/packages/core/src/metrics/exports.ts b/packages/core/src/metrics/exports.ts index 22a5e83ffb3d..66074a7e846c 100644 --- a/packages/core/src/metrics/exports.ts +++ b/packages/core/src/metrics/exports.ts @@ -17,7 +17,7 @@ function addToMetricsAggregator( metricType: MetricType, name: string, value: number | string, - data: MetricData = {}, + data: MetricData | undefined = {}, ): void { const client = getClient>(); const scope = getCurrentScope(); @@ -49,7 +49,7 @@ function addToMetricsAggregator( /** * Adds a value to a counter metric * - * @experimental This API is experimental and might having breaking changes in the future. + * @experimental This API is experimental and might have breaking changes in the future. */ export function increment(name: string, value: number = 1, data?: MetricData): void { addToMetricsAggregator(COUNTER_METRIC_TYPE, name, value, data); @@ -58,7 +58,7 @@ export function increment(name: string, value: number = 1, data?: MetricData): v /** * Adds a value to a distribution metric * - * @experimental This API is experimental and might having breaking changes in the future. + * @experimental This API is experimental and might have breaking changes in the future. */ export function distribution(name: string, value: number, data?: MetricData): void { addToMetricsAggregator(DISTRIBUTION_METRIC_TYPE, name, value, data); @@ -67,7 +67,7 @@ export function distribution(name: string, value: number, data?: MetricData): vo /** * Adds a value to a set metric. Value must be a string or integer. * - * @experimental This API is experimental and might having breaking changes in the future. + * @experimental This API is experimental and might have breaking changes in the future. */ export function set(name: string, value: number | string, data?: MetricData): void { addToMetricsAggregator(SET_METRIC_TYPE, name, value, data); @@ -76,7 +76,7 @@ export function set(name: string, value: number | string, data?: MetricData): vo /** * Adds a value to a gauge metric * - * @experimental This API is experimental and might having breaking changes in the future. + * @experimental This API is experimental and might have breaking changes in the future. */ export function gauge(name: string, value: number, data?: MetricData): void { addToMetricsAggregator(GAUGE_METRIC_TYPE, name, value, data); diff --git a/packages/core/src/metrics/instance.ts b/packages/core/src/metrics/instance.ts index f071006c96ca..f7d37d8118ed 100644 --- a/packages/core/src/metrics/instance.ts +++ b/packages/core/src/metrics/instance.ts @@ -8,6 +8,11 @@ import { simpleHash } from './utils'; export class CounterMetric implements MetricInstance { public constructor(private _value: number) {} + /** @inheritDoc */ + public get weight(): number { + return 1; + } + /** @inheritdoc */ public add(value: number): void { this._value += value; @@ -37,6 +42,11 @@ export class GaugeMetric implements MetricInstance { this._count = 1; } + /** @inheritDoc */ + public get weight(): number { + return 5; + } + /** @inheritdoc */ public add(value: number): void { this._last = value; @@ -66,6 +76,11 @@ export class DistributionMetric implements MetricInstance { this._value = [first]; } + /** @inheritDoc */ + public get weight(): number { + return this._value.length; + } + /** @inheritdoc */ public add(value: number): void { this._value.push(value); @@ -87,6 +102,11 @@ export class SetMetric implements MetricInstance { this._value = new Set([first]); } + /** @inheritDoc */ + public get weight(): number { + return this._value.size; + } + /** @inheritdoc */ public add(value: number | string): void { this._value.add(value); @@ -94,14 +114,12 @@ export class SetMetric implements MetricInstance { /** @inheritdoc */ public toString(): string { - return `${Array.from(this._value) + return Array.from(this._value) .map(val => (typeof val === 'string' ? simpleHash(val) : val)) - .join(':')}`; + .join(':'); } } -export type Metric = CounterMetric | GaugeMetric | DistributionMetric | SetMetric; - export const METRIC_MAP = { [COUNTER_METRIC_TYPE]: CounterMetric, [GAUGE_METRIC_TYPE]: GaugeMetric, diff --git a/packages/core/src/metrics/integration.ts b/packages/core/src/metrics/integration.ts index 0c3fa626a9e5..531b0aa698b2 100644 --- a/packages/core/src/metrics/integration.ts +++ b/packages/core/src/metrics/integration.ts @@ -1,7 +1,7 @@ import type { ClientOptions, IntegrationFn } from '@sentry/types'; import type { BaseClient } from '../baseclient'; import { convertIntegrationFnToClass } from '../integration'; -import { SimpleMetricsAggregator } from './simpleaggregator'; +import { BrowserMetricsAggregator } from './browser-aggregator'; const INTEGRATION_NAME = 'MetricsAggregator'; @@ -9,7 +9,7 @@ const metricsAggregatorIntegration: IntegrationFn = () => { return { name: INTEGRATION_NAME, setup(client: BaseClient) { - client.metricsAggregator = new SimpleMetricsAggregator(client); + client.metricsAggregator = new BrowserMetricsAggregator(client); }, }; }; diff --git a/packages/core/src/metrics/simpleaggregator.ts b/packages/core/src/metrics/simpleaggregator.ts deleted file mode 100644 index a628a3b5a406..000000000000 --- a/packages/core/src/metrics/simpleaggregator.ts +++ /dev/null @@ -1,91 +0,0 @@ -import type { Client, ClientOptions, MeasurementUnit, MetricsAggregator, Primitive } from '@sentry/types'; -import { timestampInSeconds } from '@sentry/utils'; -import { - DEFAULT_FLUSH_INTERVAL, - NAME_AND_TAG_KEY_NORMALIZATION_REGEX, - TAG_VALUE_NORMALIZATION_REGEX, -} from './constants'; -import { METRIC_MAP } from './instance'; -import type { MetricType, SimpleMetricBucket } from './types'; -import { getBucketKey } from './utils'; - -/** - * A simple metrics aggregator that aggregates metrics in memory and flushes them periodically. - * Default flush interval is 5 seconds. - * - * @experimental This API is experimental and might change in the future. - */ -export class SimpleMetricsAggregator implements MetricsAggregator { - private _buckets: SimpleMetricBucket; - private readonly _interval: ReturnType; - - public constructor(private readonly _client: Client) { - this._buckets = new Map(); - this._interval = setInterval(() => this.flush(), DEFAULT_FLUSH_INTERVAL); - } - - /** - * @inheritDoc - */ - public add( - metricType: MetricType, - unsanitizedName: string, - value: number | string, - unit: MeasurementUnit = 'none', - unsanitizedTags: Record = {}, - maybeFloatTimestamp = timestampInSeconds(), - ): void { - const timestamp = Math.floor(maybeFloatTimestamp); - const name = unsanitizedName.replace(NAME_AND_TAG_KEY_NORMALIZATION_REGEX, '_'); - const tags = sanitizeTags(unsanitizedTags); - - const bucketKey = getBucketKey(metricType, name, unit, tags); - const bucketItem = this._buckets.get(bucketKey); - if (bucketItem) { - const [bucketMetric, bucketTimestamp] = bucketItem; - bucketMetric.add(value); - // TODO(abhi): Do we need this check? - if (bucketTimestamp < timestamp) { - bucketItem[1] = timestamp; - } - } else { - // @ts-expect-error we don't need to narrow down the type of value here, saves bundle size. - const newMetric = new METRIC_MAP[metricType](value); - this._buckets.set(bucketKey, [newMetric, timestamp, metricType, name, unit, tags]); - } - } - - /** - * @inheritDoc - */ - public flush(): void { - // short circuit if buckets are empty. - if (this._buckets.size === 0) { - return; - } - if (this._client.captureAggregateMetrics) { - const metricBuckets = Array.from(this._buckets).map(([, bucketItem]) => bucketItem); - this._client.captureAggregateMetrics(metricBuckets); - } - this._buckets.clear(); - } - - /** - * @inheritDoc - */ - public close(): void { - clearInterval(this._interval); - this.flush(); - } -} - -function sanitizeTags(unsanitizedTags: Record): Record { - const tags: Record = {}; - for (const key in unsanitizedTags) { - if (Object.prototype.hasOwnProperty.call(unsanitizedTags, key)) { - const sanitizedKey = key.replace(NAME_AND_TAG_KEY_NORMALIZATION_REGEX, '_'); - tags[sanitizedKey] = String(unsanitizedTags[key]).replace(TAG_VALUE_NORMALIZATION_REGEX, '_'); - } - } - return tags; -} diff --git a/packages/core/src/metrics/types.ts b/packages/core/src/metrics/types.ts index de6032f811b8..000c401e7a34 100644 --- a/packages/core/src/metrics/types.ts +++ b/packages/core/src/metrics/types.ts @@ -7,4 +7,6 @@ export type MetricType = | typeof SET_METRIC_TYPE | typeof DISTRIBUTION_METRIC_TYPE; -export type SimpleMetricBucket = Map; +// TODO(@anonrig): Convert this to WeakMap when we support ES6 and +// use FinalizationRegistry to flush the buckets when the aggregator is garbage collected. +export type MetricBucket = Map; diff --git a/packages/core/src/metrics/utils.ts b/packages/core/src/metrics/utils.ts index 6e4d75fee5f6..a6674bcf30e1 100644 --- a/packages/core/src/metrics/utils.ts +++ b/packages/core/src/metrics/utils.ts @@ -1,5 +1,6 @@ -import type { MeasurementUnit, MetricBucketItem } from '@sentry/types'; +import type { MeasurementUnit, MetricBucketItem, Primitive } from '@sentry/types'; import { dropUndefinedKeys } from '@sentry/utils'; +import { NAME_AND_TAG_KEY_NORMALIZATION_REGEX, TAG_VALUE_NORMALIZATION_REGEX } from './constants'; import type { MetricType } from './types'; /** @@ -43,15 +44,26 @@ export function simpleHash(s: string): number { * tags: { a: value, b: anothervalue } * timestamp: 12345677 */ -export function serializeMetricBuckets(metricBucketItems: Array): string { +export function serializeMetricBuckets(metricBucketItems: MetricBucketItem[]): string { let out = ''; - for (const [metric, timestamp, metricType, name, unit, tags] of metricBucketItems) { - const maybeTags = Object.keys(tags).length - ? `|#${Object.entries(tags) - .map(([key, value]) => `${key}:${String(value)}`) - .join(',')}` - : ''; - out += `${name}@${unit}:${metric}|${metricType}${maybeTags}|T${timestamp}\n`; + for (const item of metricBucketItems) { + const tagEntries = Object.entries(item.tags); + const maybeTags = tagEntries.length > 0 ? `|#${tagEntries.map(([key, value]) => `${key}:${value}`).join(',')}` : ''; + out += `${item.name}@${item.unit}:${item.metric}|${item.metricType}${maybeTags}|T${item.timestamp}\n`; } return out; } + +/** + * Sanitizes tags. + */ +export function sanitizeTags(unsanitizedTags: Record): Record { + const tags: Record = {}; + for (const key in unsanitizedTags) { + if (Object.prototype.hasOwnProperty.call(unsanitizedTags, key)) { + const sanitizedKey = key.replace(NAME_AND_TAG_KEY_NORMALIZATION_REGEX, '_'); + tags[sanitizedKey] = String(unsanitizedTags[key]).replace(TAG_VALUE_NORMALIZATION_REGEX, '_'); + } + } + return tags; +} diff --git a/packages/core/src/server-runtime-client.ts b/packages/core/src/server-runtime-client.ts index f4abd134223f..66d846c23911 100644 --- a/packages/core/src/server-runtime-client.ts +++ b/packages/core/src/server-runtime-client.ts @@ -17,6 +17,7 @@ import { BaseClient } from './baseclient'; import { createCheckInEnvelope } from './checkin'; import { DEBUG_BUILD } from './debug-build'; import { getClient } from './exports'; +import { MetricsAggregator } from './metrics/aggregator'; import type { Scope } from './scope'; import { SessionFlusher } from './sessionflusher'; import { addTracingExtensions, getDynamicSamplingContextFromClient } from './tracing'; @@ -44,6 +45,10 @@ export class ServerRuntimeClient< addTracingExtensions(); super(options); + + if (options._experiments && options._experiments['metricsAggregator']) { + this.metricsAggregator = new MetricsAggregator(this); + } } /** diff --git a/packages/core/test/lib/metrics/aggregator.test.ts b/packages/core/test/lib/metrics/aggregator.test.ts new file mode 100644 index 000000000000..32396cfedcc2 --- /dev/null +++ b/packages/core/test/lib/metrics/aggregator.test.ts @@ -0,0 +1,157 @@ +import { MetricsAggregator } from '../../../src/metrics/aggregator'; +import { MAX_WEIGHT } from '../../../src/metrics/constants'; +import { CounterMetric } from '../../../src/metrics/instance'; +import { serializeMetricBuckets } from '../../../src/metrics/utils'; +import { TestClient, getDefaultTestClientOptions } from '../../mocks/client'; + +let testClient: TestClient; + +describe('MetricsAggregator', () => { + const options = getDefaultTestClientOptions({ tracesSampleRate: 0.0 }); + + beforeEach(() => { + jest.useFakeTimers('legacy'); + testClient = new TestClient(options); + }); + + it('adds items to buckets', () => { + const aggregator = new MetricsAggregator(testClient); + aggregator.add('c', 'requests', 1); + expect(aggregator['_buckets'].size).toEqual(1); + + const firstValue = aggregator['_buckets'].values().next().value; + expect(firstValue).toEqual({ + metric: expect.any(CounterMetric), + metricType: 'c', + name: 'requests', + tags: {}, + timestamp: expect.any(Number), + unit: 'none', + }); + }); + + it('groups same items together', () => { + const aggregator = new MetricsAggregator(testClient); + aggregator.add('c', 'requests', 1); + expect(aggregator['_buckets'].size).toEqual(1); + aggregator.add('c', 'requests', 1); + expect(aggregator['_buckets'].size).toEqual(1); + + const firstValue = aggregator['_buckets'].values().next().value; + expect(firstValue).toEqual({ + metric: expect.any(CounterMetric), + metricType: 'c', + name: 'requests', + tags: {}, + timestamp: expect.any(Number), + unit: 'none', + }); + expect(firstValue.metric._value).toEqual(2); + }); + + it('differentiates based on tag value', () => { + const aggregator = new MetricsAggregator(testClient); + aggregator.add('g', 'cpu', 50); + expect(aggregator['_buckets'].size).toEqual(1); + aggregator.add('g', 'cpu', 55, undefined, { a: 'value' }); + expect(aggregator['_buckets'].size).toEqual(2); + }); + + describe('serializeBuckets', () => { + it('serializes ', () => { + const aggregator = new MetricsAggregator(testClient); + aggregator.add('c', 'requests', 8); + aggregator.add('g', 'cpu', 50); + aggregator.add('g', 'cpu', 55); + aggregator.add('g', 'cpu', 52); + aggregator.add('d', 'lcp', 1, 'second', { a: 'value', b: 'anothervalue' }); + aggregator.add('d', 'lcp', 1.2, 'second', { a: 'value', b: 'anothervalue' }); + aggregator.add('s', 'important_people', 'a', 'none', { numericKey: 2 }); + aggregator.add('s', 'important_people', 'b', 'none', { numericKey: 2 }); + + const metricBuckets = Array.from(aggregator['_buckets']).map(([, bucketItem]) => bucketItem); + const serializedBuckets = serializeMetricBuckets(metricBuckets); + + expect(serializedBuckets).toContain('requests@none:8|c|T'); + expect(serializedBuckets).toContain('cpu@none:52:50:55:157:3|g|T'); + expect(serializedBuckets).toContain('lcp@second:1:1.2|d|#a:value,b:anothervalue|T'); + expect(serializedBuckets).toContain('important_people@none:97:98|s|#numericKey:2|T'); + }); + }); + + describe('close', () => { + test('should flush immediately', () => { + const capture = jest.spyOn(testClient, 'captureAggregateMetrics'); + const aggregator = new MetricsAggregator(testClient); + aggregator.add('c', 'requests', 1); + aggregator.close(); + // It should clear the interval. + expect(clearInterval).toHaveBeenCalled(); + expect(capture).toBeCalled(); + expect(capture).toBeCalledTimes(1); + expect(capture).toBeCalledWith([ + { + metric: { _value: 1 }, + metricType: 'c', + name: 'requests', + tags: {}, + timestamp: expect.any(Number), + unit: 'none', + }, + ]); + }); + }); + + describe('flush', () => { + test('should flush immediately', () => { + const capture = jest.spyOn(testClient, 'captureAggregateMetrics'); + const aggregator = new MetricsAggregator(testClient); + aggregator.add('c', 'requests', 1); + aggregator.flush(); + expect(capture).toBeCalled(); + expect(capture).toBeCalledTimes(1); + expect(capture).toBeCalledWith([ + { + metric: { _value: 1 }, + metricType: 'c', + name: 'requests', + tags: {}, + timestamp: expect.any(Number), + unit: 'none', + }, + ]); + capture.mockReset(); + aggregator.close(); + // It should clear the interval. + expect(clearInterval).toHaveBeenCalled(); + + // It shouldn't be called since it's been already flushed. + expect(capture).toBeCalledTimes(0); + }); + + test('should not capture if empty', () => { + const capture = jest.spyOn(testClient, 'captureAggregateMetrics'); + const aggregator = new MetricsAggregator(testClient); + aggregator.add('c', 'requests', 1); + aggregator.flush(); + expect(capture).toBeCalledTimes(1); + capture.mockReset(); + aggregator.close(); + expect(capture).toBeCalledTimes(0); + }); + }); + + describe('add', () => { + test('it should respect the max weight and flush if exceeded', () => { + const capture = jest.spyOn(testClient, 'captureAggregateMetrics'); + const aggregator = new MetricsAggregator(testClient); + + for (let i = 0; i < MAX_WEIGHT; i++) { + aggregator.add('c', 'requests', 1); + } + + expect(capture).toBeCalledTimes(1); + aggregator.close(); + }); + }); +}); diff --git a/packages/core/test/lib/metrics/simpleaggregator.test.ts b/packages/core/test/lib/metrics/browser-aggregator.test.ts similarity index 71% rename from packages/core/test/lib/metrics/simpleaggregator.test.ts rename to packages/core/test/lib/metrics/browser-aggregator.test.ts index cafc78d1e018..669959a03e05 100644 --- a/packages/core/test/lib/metrics/simpleaggregator.test.ts +++ b/packages/core/test/lib/metrics/browser-aggregator.test.ts @@ -1,36 +1,49 @@ +import { BrowserMetricsAggregator } from '../../../src/metrics/browser-aggregator'; import { CounterMetric } from '../../../src/metrics/instance'; -import { SimpleMetricsAggregator } from '../../../src/metrics/simpleaggregator'; import { serializeMetricBuckets } from '../../../src/metrics/utils'; import { TestClient, getDefaultTestClientOptions } from '../../mocks/client'; -describe('SimpleMetricsAggregator', () => { +describe('BrowserMetricsAggregator', () => { const options = getDefaultTestClientOptions({ tracesSampleRate: 0.0 }); const testClient = new TestClient(options); it('adds items to buckets', () => { - const aggregator = new SimpleMetricsAggregator(testClient); + const aggregator = new BrowserMetricsAggregator(testClient); aggregator.add('c', 'requests', 1); expect(aggregator['_buckets'].size).toEqual(1); const firstValue = aggregator['_buckets'].values().next().value; - expect(firstValue).toEqual([expect.any(CounterMetric), expect.any(Number), 'c', 'requests', 'none', {}]); + expect(firstValue).toEqual({ + metric: expect.any(CounterMetric), + metricType: 'c', + name: 'requests', + tags: {}, + timestamp: expect.any(Number), + unit: 'none', + }); }); it('groups same items together', () => { - const aggregator = new SimpleMetricsAggregator(testClient); + const aggregator = new BrowserMetricsAggregator(testClient); aggregator.add('c', 'requests', 1); expect(aggregator['_buckets'].size).toEqual(1); aggregator.add('c', 'requests', 1); expect(aggregator['_buckets'].size).toEqual(1); const firstValue = aggregator['_buckets'].values().next().value; - expect(firstValue).toEqual([expect.any(CounterMetric), expect.any(Number), 'c', 'requests', 'none', {}]); - - expect(firstValue[0]._value).toEqual(2); + expect(firstValue).toEqual({ + metric: expect.any(CounterMetric), + metricType: 'c', + name: 'requests', + tags: {}, + timestamp: expect.any(Number), + unit: 'none', + }); + expect(firstValue.metric._value).toEqual(2); }); it('differentiates based on tag value', () => { - const aggregator = new SimpleMetricsAggregator(testClient); + const aggregator = new BrowserMetricsAggregator(testClient); aggregator.add('g', 'cpu', 50); expect(aggregator['_buckets'].size).toEqual(1); aggregator.add('g', 'cpu', 55, undefined, { a: 'value' }); @@ -39,7 +52,7 @@ describe('SimpleMetricsAggregator', () => { describe('serializeBuckets', () => { it('serializes ', () => { - const aggregator = new SimpleMetricsAggregator(testClient); + const aggregator = new BrowserMetricsAggregator(testClient); aggregator.add('c', 'requests', 8); aggregator.add('g', 'cpu', 50); aggregator.add('g', 'cpu', 55); diff --git a/packages/deno/src/index.ts b/packages/deno/src/index.ts index 4a57bb6f2cfd..fe0b5ee4b620 100644 --- a/packages/deno/src/index.ts +++ b/packages/deno/src/index.ts @@ -68,6 +68,7 @@ export { startSpan, startInactiveSpan, startSpanManual, + metrics, } from '@sentry/core'; export type { SpanStatusType } from '@sentry/core'; diff --git a/packages/node/src/index.ts b/packages/node/src/index.ts index 46b1d6d742d4..36d2d8beac53 100644 --- a/packages/node/src/index.ts +++ b/packages/node/src/index.ts @@ -71,6 +71,7 @@ export { startInactiveSpan, startSpanManual, continueTrace, + metrics, } from '@sentry/core'; export type { SpanStatusType } from '@sentry/core'; export { autoDiscoverNodePerformanceMonitoringIntegrations } from './tracing'; diff --git a/packages/types/src/metrics.ts b/packages/types/src/metrics.ts index 18943ee3997e..9bfb990461eb 100644 --- a/packages/types/src/metrics.ts +++ b/packages/types/src/metrics.ts @@ -1,25 +1,41 @@ import type { MeasurementUnit } from './measurement'; import type { Primitive } from './misc'; -export interface MetricInstance { +/** + * An abstract definition of the minimum required API + * for a metric instance. + */ +export abstract class MetricInstance { + /** + * Returns the weight of the metric. + */ + public get weight(): number { + return 1; + } + /** * Adds a value to a metric. */ - add(value: number | string): void; + public add(value: number | string): void { + // Override this. + } + /** * Serializes the metric into a statsd format string. */ - toString(): string; + public toString(): string { + return ''; + } } -export type MetricBucketItem = [ - metric: MetricInstance, - timestamp: number, - metricType: 'c' | 'g' | 's' | 'd', - name: string, - unit: MeasurementUnit, - tags: { [key: string]: string }, -]; +export interface MetricBucketItem { + metric: MetricInstance; + timestamp: number; + metricType: 'c' | 'g' | 's' | 'd'; + name: string; + unit: MeasurementUnit; + tags: Record; +} /** * A metrics aggregator that aggregates metrics in memory and flushes them periodically. diff --git a/packages/vercel-edge/src/index.ts b/packages/vercel-edge/src/index.ts index cbc7f6a89d7c..2ef1217ab117 100644 --- a/packages/vercel-edge/src/index.ts +++ b/packages/vercel-edge/src/index.ts @@ -69,6 +69,7 @@ export { startInactiveSpan, startSpanManual, continueTrace, + metrics, } from '@sentry/core'; export type { SpanStatusType } from '@sentry/core'; From f56219a5f804cb4ead7846aee32e1e73b2718056 Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Fri, 22 Dec 2023 09:47:57 +0100 Subject: [PATCH 21/24] build: Fix size limit (#9967) I think this was just not updated... I aligned it with the webpack size limit for the "full" bundle. --- .size-limit.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.size-limit.js b/.size-limit.js index 5d690d69768a..b29677fce51a 100644 --- a/.size-limit.js +++ b/.size-limit.js @@ -60,7 +60,7 @@ module.exports = [ name: '@sentry/browser (incl. Tracing, Replay, Feedback) - ES6 CDN Bundle (gzipped)', path: 'packages/browser/build/bundles/bundle.tracing.replay.feedback.min.js', gzip: true, - limit: '75 KB', + limit: '90 KB', }, { name: '@sentry/browser (incl. Tracing, Replay) - ES6 CDN Bundle (gzipped)', @@ -101,7 +101,7 @@ module.exports = [ path: 'packages/browser/build/bundles/bundle.min.js', gzip: false, brotli: false, - limit: '70 KB', + limit: '80 KB', }, // Browser CDN bundles (ES5) @@ -110,7 +110,7 @@ module.exports = [ name: '@sentry/browser (incl. Tracing) - ES5 CDN Bundle (gzipped)', path: 'packages/browser/build/bundles/bundle.tracing.es5.min.js', gzip: true, - limit: '35 KB', + limit: '40 KB', }, // React From 948e7d32608fd385395d0e4cd1bbed286049a969 Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Fri, 22 Dec 2023 11:04:23 +0100 Subject: [PATCH 22/24] feat(sveltekit): Add options to configure fetch instrumentation script for CSP (#9969) This PR adds options to the `sentryHandle` request handler that allows control over CSP-relevant aspects for the ` - `; - return html.replace('', content); - } +/** + * Adds Sentry tracing tags to the returned html page. + * Adds Sentry fetch proxy script to the returned html page if enabled in options. + * Also adds a nonce attribute to the script tag if users specified one for CSP. + * + * Exported only for testing + */ +export function addSentryCodeToPage(options: SentryHandleOptions): NonNullable { + const { fetchProxyScriptNonce, injectFetchProxyScript } = options; + // if injectFetchProxyScript is not set, we default to true + const shouldInjectScript = injectFetchProxyScript !== false; + const nonce = fetchProxyScriptNonce ? `nonce="${fetchProxyScriptNonce}"` : ''; - return html; -}; + return ({ html }) => { + const transaction = getActiveTransaction(); + if (transaction) { + const traceparentData = transaction.toTraceparent(); + const dynamicSamplingContext = dynamicSamplingContextToSentryBaggageHeader( + transaction.getDynamicSamplingContext(), + ); + const contentMeta = ` + + + `; + const contentScript = shouldInjectScript ? `` : ''; + + const content = `${contentMeta}\n${contentScript}`; + + return html.replace('', content); + } + + return html; + }; +} /** * A SvelteKit handle function that wraps the request for Sentry error and @@ -89,13 +127,14 @@ export const transformPageChunk: NonNullable { - const res = await resolve(event, { transformPageChunk }); + const res = await resolve(event, { + transformPageChunk: addSentryCodeToPage(options), + }); if (span) { span.setHttpStatus(res.status); } diff --git a/packages/sveltekit/test/server/handle.test.ts b/packages/sveltekit/test/server/handle.test.ts index 1444b75d9ea5..cca809006d27 100644 --- a/packages/sveltekit/test/server/handle.test.ts +++ b/packages/sveltekit/test/server/handle.test.ts @@ -6,7 +6,7 @@ import type { Handle } from '@sveltejs/kit'; import { redirect } from '@sveltejs/kit'; import { vi } from 'vitest'; -import { sentryHandle, transformPageChunk } from '../../src/server/handle'; +import { FETCH_PROXY_SCRIPT, addSentryCodeToPage, sentryHandle } from '../../src/server/handle'; import { getDefaultNodeClientOptions } from '../utils'; const mockCaptureException = vi.spyOn(SentryNode, 'captureException').mockImplementation(() => 'xx'); @@ -337,7 +337,7 @@ describe('handleSentry', () => { }); }); -describe('transformPageChunk', () => { +describe('addSentryCodeToPage', () => { const html = ` @@ -351,16 +351,41 @@ describe('transformPageChunk', () => { `; it('does not add meta tags if no active transaction', () => { + const transformPageChunk = addSentryCodeToPage({}); const transformed = transformPageChunk({ html, done: true }); expect(transformed).toEqual(html); }); - it('adds meta tags if there is an active transaction', () => { + it('adds meta tags and the fetch proxy script if there is an active transaction', () => { + const transformPageChunk = addSentryCodeToPage({}); const transaction = hub.startTransaction({ name: 'test' }); hub.getScope().setSpan(transaction); const transformed = transformPageChunk({ html, done: true }) as string; - expect(transformed.includes('${FETCH_PROXY_SCRIPT}`); + }); + + it('adds a nonce attribute to the script if the `fetchProxyScriptNonce` option is specified', () => { + const transformPageChunk = addSentryCodeToPage({ fetchProxyScriptNonce: '123abc' }); + const transaction = hub.startTransaction({ name: 'test' }); + hub.getScope().setSpan(transaction); + const transformed = transformPageChunk({ html, done: true }) as string; + + expect(transformed).toContain('${FETCH_PROXY_SCRIPT}`); + }); + + it('does not add the fetch proxy script if the `injectFetchProxyScript` option is false', () => { + const transformPageChunk = addSentryCodeToPage({ injectFetchProxyScript: false }); + const transaction = hub.startTransaction({ name: 'test' }); + hub.getScope().setSpan(transaction); + const transformed = transformPageChunk({ html, done: true }) as string; + + expect(transformed).toContain('${FETCH_PROXY_SCRIPT}`); }); }); From cf412d9373960ddddf5eb7723c1cff7906b22488 Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Fri, 22 Dec 2023 11:23:49 +0100 Subject: [PATCH 23/24] fix(core): Rethrow caught promise rejections in `startSpan`, `startSpanManual`, `trace` (#9958) Our previous implementation of `startSpan` et.al. assumed that because we were "cloning" the original promise, unhandled promise rejections would still bubble up to the global `onunhandledrejection` handler so that Sentry would catch the error. However, we tried investigating this and found out that our "cloning" mechanism didn't work correctly and because we already added a rejection handler, the promise rejection would _not always_ make it to the global handler. After adding multiple integration tests, I further narrowed the buggy behaviour down to a rather special case: The `startSpan` call is not `await`ed which for some reason will lead to the SDK not catching the error. Unless, we apply the fix in this PR. This patch removes this cloning mechanism in favour of directly attaching a rejection handler to the promise. In this handler, we rethrow the error which should trigger the global handlers. --- .../public-api/startSpan/basic/subject.js | 9 +++++ .../suites/public-api/startSpan/basic/test.ts | 34 ++++++++++++++++++ .../error-async-reject/template.html | 23 ++++++++++++ .../startSpan/error-async-reject/test.ts | 31 ++++++++++++++++ .../template.html | 23 ++++++++++++ .../error-async-throw-not-awaited/test.ts | 28 +++++++++++++++ .../startSpan/error-async-throw/template.html | 24 +++++++++++++ .../startSpan/error-async-throw/test.ts | 25 +++++++++++++ .../startSpan/error-sync/subject.js | 9 +++++ .../public-api/startSpan/error-sync/test.ts | 22 ++++++++++++ .../suites/public-api/startSpan/init.js | 13 +++++++ packages/core/src/tracing/trace.ts | 35 ++++++++++++------- 12 files changed, 263 insertions(+), 13 deletions(-) create mode 100644 packages/browser-integration-tests/suites/public-api/startSpan/basic/subject.js create mode 100644 packages/browser-integration-tests/suites/public-api/startSpan/basic/test.ts create mode 100644 packages/browser-integration-tests/suites/public-api/startSpan/error-async-reject/template.html create mode 100644 packages/browser-integration-tests/suites/public-api/startSpan/error-async-reject/test.ts create mode 100644 packages/browser-integration-tests/suites/public-api/startSpan/error-async-throw-not-awaited/template.html create mode 100644 packages/browser-integration-tests/suites/public-api/startSpan/error-async-throw-not-awaited/test.ts create mode 100644 packages/browser-integration-tests/suites/public-api/startSpan/error-async-throw/template.html create mode 100644 packages/browser-integration-tests/suites/public-api/startSpan/error-async-throw/test.ts create mode 100644 packages/browser-integration-tests/suites/public-api/startSpan/error-sync/subject.js create mode 100644 packages/browser-integration-tests/suites/public-api/startSpan/error-sync/test.ts create mode 100644 packages/browser-integration-tests/suites/public-api/startSpan/init.js diff --git a/packages/browser-integration-tests/suites/public-api/startSpan/basic/subject.js b/packages/browser-integration-tests/suites/public-api/startSpan/basic/subject.js new file mode 100644 index 000000000000..5f4dbea513a8 --- /dev/null +++ b/packages/browser-integration-tests/suites/public-api/startSpan/basic/subject.js @@ -0,0 +1,9 @@ +async function run() { + Sentry.startSpan({ name: 'parent_span' }, () => { + Sentry.startSpan({ name: 'child_span' }, () => { + // whatever a user would do here + }); + }); +} + +run(); diff --git a/packages/browser-integration-tests/suites/public-api/startSpan/basic/test.ts b/packages/browser-integration-tests/suites/public-api/startSpan/basic/test.ts new file mode 100644 index 000000000000..a4b5d8b9bd02 --- /dev/null +++ b/packages/browser-integration-tests/suites/public-api/startSpan/basic/test.ts @@ -0,0 +1,34 @@ +import { expect } from '@playwright/test'; +import type { Event } from '@sentry/types'; + +import { sentryTest } from '../../../../utils/fixtures'; +import { getFirstSentryEnvelopeRequest, shouldSkipTracingTest } from '../../../../utils/helpers'; + +sentryTest('should send a transaction in an envelope', async ({ getLocalTestPath, page }) => { + if (shouldSkipTracingTest()) { + sentryTest.skip(); + } + + const url = await getLocalTestPath({ testDir: __dirname }); + const transaction = await getFirstSentryEnvelopeRequest(page, url); + + expect(transaction.transaction).toBe('parent_span'); + expect(transaction.spans).toBeDefined(); +}); + +sentryTest('should report finished spans as children of the root transaction', async ({ getLocalTestPath, page }) => { + if (shouldSkipTracingTest()) { + sentryTest.skip(); + } + + const url = await getLocalTestPath({ testDir: __dirname }); + const transaction = await getFirstSentryEnvelopeRequest(page, url); + + const rootSpanId = transaction?.contexts?.trace?.spanId; + + expect(transaction.spans).toHaveLength(1); + + const span_1 = transaction.spans?.[0]; + expect(span_1?.description).toBe('child_span'); + expect(span_1?.parentSpanId).toEqual(rootSpanId); +}); diff --git a/packages/browser-integration-tests/suites/public-api/startSpan/error-async-reject/template.html b/packages/browser-integration-tests/suites/public-api/startSpan/error-async-reject/template.html new file mode 100644 index 000000000000..1a1cec6b25d7 --- /dev/null +++ b/packages/browser-integration-tests/suites/public-api/startSpan/error-async-reject/template.html @@ -0,0 +1,23 @@ + + + + + + + + + + + + diff --git a/packages/browser-integration-tests/suites/public-api/startSpan/error-async-reject/test.ts b/packages/browser-integration-tests/suites/public-api/startSpan/error-async-reject/test.ts new file mode 100644 index 000000000000..e328d4869b24 --- /dev/null +++ b/packages/browser-integration-tests/suites/public-api/startSpan/error-async-reject/test.ts @@ -0,0 +1,31 @@ +import { expect } from '@playwright/test'; +import type { Event } from '@sentry/types'; + +import { sentryTest } from '../../../../utils/fixtures'; +import { getMultipleSentryEnvelopeRequests, shouldSkipTracingTest } from '../../../../utils/helpers'; + +sentryTest( + 'should capture a promise rejection within an async startSpan callback', + async ({ getLocalTestPath, page }) => { + if (shouldSkipTracingTest()) { + sentryTest.skip(); + } + + const url = await getLocalTestPath({ testDir: __dirname }); + const envelopePromise = getMultipleSentryEnvelopeRequests(page, 2); + + await page.goto(url); + + const clickPromise = page.getByText('Button 1').click(); + + const [, events] = await Promise.all([clickPromise, envelopePromise]); + const txn = events.find(event => event.type === 'transaction'); + const err = events.find(event => !event.type); + + expect(txn).toMatchObject({ transaction: 'parent_span' }); + + expect(err?.exception?.values?.[0]?.value).toBe( + 'Non-Error promise rejection captured with value: Async Promise Rejection', + ); + }, +); diff --git a/packages/browser-integration-tests/suites/public-api/startSpan/error-async-throw-not-awaited/template.html b/packages/browser-integration-tests/suites/public-api/startSpan/error-async-throw-not-awaited/template.html new file mode 100644 index 000000000000..9f4044dedff1 --- /dev/null +++ b/packages/browser-integration-tests/suites/public-api/startSpan/error-async-throw-not-awaited/template.html @@ -0,0 +1,23 @@ + + + + + + + + + + + + diff --git a/packages/browser-integration-tests/suites/public-api/startSpan/error-async-throw-not-awaited/test.ts b/packages/browser-integration-tests/suites/public-api/startSpan/error-async-throw-not-awaited/test.ts new file mode 100644 index 000000000000..5ef6f22419d1 --- /dev/null +++ b/packages/browser-integration-tests/suites/public-api/startSpan/error-async-throw-not-awaited/test.ts @@ -0,0 +1,28 @@ +import { expect } from '@playwright/test'; +import type { Event } from '@sentry/types'; + +import { sentryTest } from '../../../../utils/fixtures'; +import { getMultipleSentryEnvelopeRequests, shouldSkipTracingTest } from '../../../../utils/helpers'; + +sentryTest( + "should capture a thrown error within an async startSpan callback that's not awaited properly", + async ({ getLocalTestPath, page }) => { + if (shouldSkipTracingTest()) { + sentryTest.skip(); + } + const envelopePromise = getMultipleSentryEnvelopeRequests(page, 2); + + const url = await getLocalTestPath({ testDir: __dirname }); + await page.goto(url); + + const clickPromise = page.getByText('Button 1').click(); + + // awaiting both events simultaneously to avoid race conditions + const [, events] = await Promise.all([clickPromise, envelopePromise]); + const txn = events.find(event => event.type === 'transaction'); + const err = events.find(event => !event.type); + + expect(txn).toMatchObject({ transaction: 'parent_span' }); + expect(err?.exception?.values?.[0]?.value).toBe('Async Thrown Error'); + }, +); diff --git a/packages/browser-integration-tests/suites/public-api/startSpan/error-async-throw/template.html b/packages/browser-integration-tests/suites/public-api/startSpan/error-async-throw/template.html new file mode 100644 index 000000000000..e4a96b1f0691 --- /dev/null +++ b/packages/browser-integration-tests/suites/public-api/startSpan/error-async-throw/template.html @@ -0,0 +1,24 @@ + + + + + + + + + + + + diff --git a/packages/browser-integration-tests/suites/public-api/startSpan/error-async-throw/test.ts b/packages/browser-integration-tests/suites/public-api/startSpan/error-async-throw/test.ts new file mode 100644 index 000000000000..15ee3b9bd4de --- /dev/null +++ b/packages/browser-integration-tests/suites/public-api/startSpan/error-async-throw/test.ts @@ -0,0 +1,25 @@ +import { expect } from '@playwright/test'; +import type { Event } from '@sentry/types'; + +import { sentryTest } from '../../../../utils/fixtures'; +import { getMultipleSentryEnvelopeRequests, shouldSkipTracingTest } from '../../../../utils/helpers'; + +sentryTest('should capture a thrown error within an async startSpan callback', async ({ getLocalTestPath, page }) => { + if (shouldSkipTracingTest()) { + sentryTest.skip(); + } + const envelopePromise = getMultipleSentryEnvelopeRequests(page, 2); + + const url = await getLocalTestPath({ testDir: __dirname }); + await page.goto(url); + + const clickPromise = page.getByText('Button 1').click(); + + // awaiting both events simultaneously to avoid race conditions + const [, events] = await Promise.all([clickPromise, envelopePromise]); + const txn = events.find(event => event.type === 'transaction'); + const err = events.find(event => !event.type); + + expect(txn).toMatchObject({ transaction: 'parent_span' }); + expect(err?.exception?.values?.[0]?.value).toBe('Async Thrown Error'); +}); diff --git a/packages/browser-integration-tests/suites/public-api/startSpan/error-sync/subject.js b/packages/browser-integration-tests/suites/public-api/startSpan/error-sync/subject.js new file mode 100644 index 000000000000..55d9bf76d224 --- /dev/null +++ b/packages/browser-integration-tests/suites/public-api/startSpan/error-sync/subject.js @@ -0,0 +1,9 @@ +function run() { + Sentry.startSpan({ name: 'parent_span' }, () => { + throw new Error('Sync Error'); + }); +} + +// using `setTimeout` here because otherwise the thrown error will be +// thrown as a generic "Script Error." instead of the actual error". +setTimeout(run); diff --git a/packages/browser-integration-tests/suites/public-api/startSpan/error-sync/test.ts b/packages/browser-integration-tests/suites/public-api/startSpan/error-sync/test.ts new file mode 100644 index 000000000000..4cba6929396f --- /dev/null +++ b/packages/browser-integration-tests/suites/public-api/startSpan/error-sync/test.ts @@ -0,0 +1,22 @@ +import { expect } from '@playwright/test'; +import type { Event } from '@sentry/types'; + +import { sentryTest } from '../../../../utils/fixtures'; +import { getMultipleSentryEnvelopeRequests, shouldSkipTracingTest } from '../../../../utils/helpers'; + +sentryTest('should capture an error within a sync startSpan callback', async ({ getLocalTestPath, page }) => { + if (shouldSkipTracingTest()) { + sentryTest.skip(); + } + + const url = await getLocalTestPath({ testDir: __dirname }); + const gotoPromise = page.goto(url); + const envelopePromise = getMultipleSentryEnvelopeRequests(page, 2); + + const [_, events] = await Promise.all([gotoPromise, envelopePromise]); + const txn = events.find(event => event.type === 'transaction'); + const err = events.find(event => !event.type); + + expect(txn).toMatchObject({ transaction: 'parent_span' }); + expect(err?.exception?.values?.[0]?.value).toBe('Sync Error'); +}); diff --git a/packages/browser-integration-tests/suites/public-api/startSpan/init.js b/packages/browser-integration-tests/suites/public-api/startSpan/init.js new file mode 100644 index 000000000000..b0bf1e869254 --- /dev/null +++ b/packages/browser-integration-tests/suites/public-api/startSpan/init.js @@ -0,0 +1,13 @@ +/* eslint-disable no-unused-vars */ +import * as Sentry from '@sentry/browser'; +// biome-ignore lint/nursery/noUnusedImports: Need to import tracing for side effect +import * as _ from '@sentry/tracing'; + +window.Sentry = Sentry; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + tracesSampleRate: 1.0, + normalizeDepth: 10, + debug: true, +}); diff --git a/packages/core/src/tracing/trace.ts b/packages/core/src/tracing/trace.ts index db6e34aa487e..1b01ee20490c 100644 --- a/packages/core/src/tracing/trace.ts +++ b/packages/core/src/tracing/trace.ts @@ -55,23 +55,25 @@ export function trace( } if (isThenable(maybePromiseResult)) { - Promise.resolve(maybePromiseResult).then( - () => { + // @ts-expect-error - the isThenable check returns the "wrong" type here + return maybePromiseResult.then( + res => { finishAndSetSpan(); afterFinish(); + return res; }, e => { activeSpan && activeSpan.setStatus('internal_error'); onError(e, activeSpan); finishAndSetSpan(); afterFinish(); + throw e; }, ); - } else { - finishAndSetSpan(); - afterFinish(); } + finishAndSetSpan(); + afterFinish(); return maybePromiseResult; } @@ -89,6 +91,7 @@ export function trace( export function startSpan(context: TransactionContext, callback: (span: Span | undefined) => T): T { const ctx = normalizeContext(context); + // @ts-expect-error - isThenable returns the wrong type return withScope(scope => { const hub = getCurrentHub(); const parentSpan = scope.getSpan(); @@ -110,19 +113,20 @@ export function startSpan(context: TransactionContext, callback: (span: Span } if (isThenable(maybePromiseResult)) { - Promise.resolve(maybePromiseResult).then( - () => { + return maybePromiseResult.then( + res => { finishAndSetSpan(); + return res; }, - () => { + e => { activeSpan && activeSpan.setStatus('internal_error'); finishAndSetSpan(); + throw e; }, ); - } else { - finishAndSetSpan(); } + finishAndSetSpan(); return maybePromiseResult; }); } @@ -149,6 +153,7 @@ export function startSpanManual( ): T { const ctx = normalizeContext(context); + // @ts-expect-error - isThenable returns the wrong type return withScope(scope => { const hub = getCurrentHub(); const parentSpan = scope.getSpan(); @@ -169,9 +174,13 @@ export function startSpanManual( } if (isThenable(maybePromiseResult)) { - Promise.resolve(maybePromiseResult).then(undefined, () => { - activeSpan && activeSpan.setStatus('internal_error'); - }); + return maybePromiseResult.then( + res => res, + e => { + activeSpan && activeSpan.setStatus('internal_error'); + throw e; + }, + ); } return maybePromiseResult; From f08bec8d94cc6627bbe2446b3e0e3436860f1422 Mon Sep 17 00:00:00 2001 From: Abhijeet Prasad Date: Thu, 21 Dec 2023 17:08:03 -0500 Subject: [PATCH 24/24] meta(changelog): Update changelog for 7.91.0 --- CHANGELOG.md | 62 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 47895bc58fb1..eb90af03fa6b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,68 @@ - "You miss 100 percent of the chances you don't take. — Wayne Gretzky" — Michael Scott +## 7.91.0 + +### Important Changes + +- **feat: Add server runtime metrics aggregator (#9894)** + +The release adds alpha support for [Sentry developer metrics](https://github.com/getsentry/sentry/discussions/58584) in the server runtime SDKs (`@sentry/node`, `@sentry/deno`, `@sentry/nextjs` server-side, etc.). Via the newly introduced APIs, you can now flush metrics directly to Sentry. + +To enable capturing metrics, you first need to add the `metricsAggregator` experiment to your `Sentry.init` call. + +```js +Sentry.init({ + dsn: '__DSN__', + _experiments: { + metricsAggregator: true, + }, +}); +``` + +Then you'll be able to add `counters`, `sets`, `distributions`, and `gauges` under the `Sentry.metrics` namespace. + +```js +// Add 4 to a counter named `hits` +Sentry.metrics.increment('hits', 4); + +// Add 2 to gauge named `parallel_requests`, tagged with `type: "a"` +Sentry.metrics.gauge('parallel_requests', 2, { tags: { type: 'a' } }); + +// Add 4.6 to a distribution named `response_time` with unit seconds +Sentry.metrics.distribution('response_time', 4.6, { unit: 'seconds' }); + +// Add 2 to a set named `valuable.ids` +Sentry.metrics.set('valuable.ids', 2); +``` + +- **feat(node): Rework ANR to use worker script via an integration (#9945)** + +The [ANR tracking integration for Node](https://docs.sentry.io/platforms/node/configuration/application-not-responding/) has been reworked to use an integration. ANR tracking now requires a minimum Node version of 16 or higher. Previously you had to call `Sentry.enableANRDetection` before running your application, now you can simply add the `Anr` integration to your `Sentry.init` call. + +```js +import * as Sentry from '@sentry/node'; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + integrations: [new Sentry.Integrations.Anr({ captureStackTrace: true, anrThreshold: 200 })], +}); +``` + +### Other Changes + +- feat(breadcrumbs): Send component names on UI breadcrumbs (#9946) +- feat(core): Add `getGlobalScope()` method (#9920) +- feat(core): Add `getIsolationScope()` method (#9957) +- feat(core): Add `span.end()` to replace `span.finish()` (#9954) +- feat(core): Ensure `startSpan` & `startSpanManual` fork scope (#9955) +- feat(react): Send component name on spans (#9949) +- feat(replay): Send component names in replay breadcrumbs (#9947) +- feat(sveltekit): Add options to configure fetch instrumentation script for CSP (#9969) +- feat(tracing): Send component name on interaction spans (#9948) +- feat(utils): Add function to extract relevant component name (#9921) +- fix(core): Rethrow caught promise rejections in `startSpan`, `startSpanManual`, `trace` (#9958) + ## 7.90.0 - feat(replay): Change to use preset quality values (#9903)