From 7a10218c75e471c80226255f4128d32458b58c0e Mon Sep 17 00:00:00 2001 From: Luca Forstner Date: Fri, 22 Nov 2024 11:49:34 +0100 Subject: [PATCH 01/34] feat(core): Deprecate `urlEncode` (#14406) --- docs/migration/draft-v9-migration-guide.md | 1 + packages/core/src/api.ts | 19 +++++++++++++------ packages/core/src/utils-hoist/index.ts | 1 + packages/core/src/utils-hoist/object.ts | 2 ++ packages/core/test/lib/api.test.ts | 4 ++-- packages/core/test/utils-hoist/object.test.ts | 3 +++ packages/utils/src/index.ts | 1 + 7 files changed, 23 insertions(+), 8 deletions(-) diff --git a/docs/migration/draft-v9-migration-guide.md b/docs/migration/draft-v9-migration-guide.md index d117d66ecae3..1b8cab48aec0 100644 --- a/docs/migration/draft-v9-migration-guide.md +++ b/docs/migration/draft-v9-migration-guide.md @@ -7,6 +7,7 @@ - Deprecated `AddRequestDataToEventOptions.transaction`. This option effectively doesn't do anything anymore, and will be removed in v9. - Deprecated `TransactionNamingScheme` type. +- Deprecated `urlEncode`. No replacements. ## `@sentry/core` diff --git a/packages/core/src/api.ts b/packages/core/src/api.ts index f8bb8cfe8eac..b7cd786d215a 100644 --- a/packages/core/src/api.ts +++ b/packages/core/src/api.ts @@ -1,6 +1,5 @@ import type { DsnComponents, DsnLike, SdkInfo } from '@sentry/types'; import { dsnToString, makeDsn } from './utils-hoist/dsn'; -import { urlEncode } from './utils-hoist/object'; const SENTRY_API_VERSION = '7'; @@ -18,13 +17,21 @@ function _getIngestEndpoint(dsn: DsnComponents): string { /** Returns a URL-encoded string with auth config suitable for a query string. */ function _encodedAuth(dsn: DsnComponents, sdkInfo: SdkInfo | undefined): string { - return urlEncode({ + const params: Record = { + sentry_version: SENTRY_API_VERSION, + }; + + if (dsn.publicKey) { // We send only the minimum set of required information. See // https://github.com/getsentry/sentry-javascript/issues/2572. - sentry_key: dsn.publicKey, - sentry_version: SENTRY_API_VERSION, - ...(sdkInfo && { sentry_client: `${sdkInfo.name}/${sdkInfo.version}` }), - }); + params.sentry_key = dsn.publicKey; + } + + if (sdkInfo) { + params.sentry_client = `${sdkInfo.name}/${sdkInfo.version}`; + } + + return new URLSearchParams(params).toString(); } /** diff --git a/packages/core/src/utils-hoist/index.ts b/packages/core/src/utils-hoist/index.ts index 1625ea6c0868..b6bb7151a7e3 100644 --- a/packages/core/src/utils-hoist/index.ts +++ b/packages/core/src/utils-hoist/index.ts @@ -57,6 +57,7 @@ export { getOriginalFunction, markFunctionWrapped, objectify, + // eslint-disable-next-line deprecation/deprecation urlEncode, } from './object'; export { basename, dirname, isAbsolute, join, normalizePath, relative, resolve } from './path'; diff --git a/packages/core/src/utils-hoist/object.ts b/packages/core/src/utils-hoist/object.ts index 13ddff35664b..d3e785f7639d 100644 --- a/packages/core/src/utils-hoist/object.ts +++ b/packages/core/src/utils-hoist/object.ts @@ -90,6 +90,8 @@ export function getOriginalFunction(func: WrappedFunction): WrappedFunction | un * * @param object An object that contains serializable values * @returns string Encoded + * + * @deprecated This function is deprecated and will be removed in the next major version of the SDK. */ export function urlEncode(object: { [key: string]: any }): string { return Object.keys(object) diff --git a/packages/core/test/lib/api.test.ts b/packages/core/test/lib/api.test.ts index 2c581937cc42..89a8ad3cf20f 100644 --- a/packages/core/test/lib/api.test.ts +++ b/packages/core/test/lib/api.test.ts @@ -18,7 +18,7 @@ describe('API', () => { dsnPublicComponents, undefined, undefined, - 'https://sentry.io:1234/subpath/api/123/envelope/?sentry_key=abc&sentry_version=7', + 'https://sentry.io:1234/subpath/api/123/envelope/?sentry_version=7&sentry_key=abc', ], ['uses `tunnel` value when called with `tunnel` option', dsnPublicComponents, tunnel, undefined, tunnel], [ @@ -33,7 +33,7 @@ describe('API', () => { dsnPublicComponents, undefined, sdkInfo, - 'https://sentry.io:1234/subpath/api/123/envelope/?sentry_key=abc&sentry_version=7&sentry_client=sentry.javascript.browser%2F12.31.12', + 'https://sentry.io:1234/subpath/api/123/envelope/?sentry_version=7&sentry_key=abc&sentry_client=sentry.javascript.browser%2F12.31.12', ], ])( '%s', diff --git a/packages/core/test/utils-hoist/object.test.ts b/packages/core/test/utils-hoist/object.test.ts index 7e39c463336c..2fadb530bdf2 100644 --- a/packages/core/test/utils-hoist/object.test.ts +++ b/packages/core/test/utils-hoist/object.test.ts @@ -130,14 +130,17 @@ describe('fill()', () => { describe('urlEncode()', () => { test('returns empty string for empty object input', () => { + // eslint-disable-next-line deprecation/deprecation expect(urlEncode({})).toEqual(''); }); test('returns single key/value pair joined with = sign', () => { + // eslint-disable-next-line deprecation/deprecation expect(urlEncode({ foo: 'bar' })).toEqual('foo=bar'); }); test('returns multiple key/value pairs joined together with & sign', () => { + // eslint-disable-next-line deprecation/deprecation expect(urlEncode({ foo: 'bar', pickle: 'rick', morty: '4 2' })).toEqual('foo=bar&pickle=rick&morty=4%202'); }); }); diff --git a/packages/utils/src/index.ts b/packages/utils/src/index.ts index 0b4bcd669706..17e2bb02aa99 100644 --- a/packages/utils/src/index.ts +++ b/packages/utils/src/index.ts @@ -149,6 +149,7 @@ export { memoBuilder, arrayify, normalizeUrlToBase, + // eslint-disable-next-line deprecation/deprecation urlEncode, // eslint-disable-next-line deprecation/deprecation extractPathForTransaction, From fc79e9728981fa3b36bd3cc518abe797348b00f9 Mon Sep 17 00:00:00 2001 From: Luca Forstner Date: Fri, 22 Nov 2024 12:23:16 +0100 Subject: [PATCH 02/34] feat(core): Deprecate `arrayify` (#14405) --- docs/migration/draft-v9-migration-guide.md | 1 + packages/core/src/utils-hoist/index.ts | 1 + packages/core/src/utils-hoist/misc.ts | 2 ++ packages/core/test/utils-hoist/misc.test.ts | 8 ++++++++ packages/nextjs/src/config/webpack.ts | 6 +++--- packages/utils/src/index.ts | 1 + packages/vue/src/integration.ts | 4 ++-- 7 files changed, 18 insertions(+), 5 deletions(-) diff --git a/docs/migration/draft-v9-migration-guide.md b/docs/migration/draft-v9-migration-guide.md index 1b8cab48aec0..a3e197e4b47c 100644 --- a/docs/migration/draft-v9-migration-guide.md +++ b/docs/migration/draft-v9-migration-guide.md @@ -8,6 +8,7 @@ be removed in v9. - Deprecated `TransactionNamingScheme` type. - Deprecated `urlEncode`. No replacements. +- Deprecated `arrayify`. No replacements. ## `@sentry/core` diff --git a/packages/core/src/utils-hoist/index.ts b/packages/core/src/utils-hoist/index.ts index b6bb7151a7e3..1560187a90d9 100644 --- a/packages/core/src/utils-hoist/index.ts +++ b/packages/core/src/utils-hoist/index.ts @@ -40,6 +40,7 @@ export { addContextToFrame, addExceptionMechanism, addExceptionTypeValue, + // eslint-disable-next-line deprecation/deprecation arrayify, checkOrSetAlreadyCaught, getEventDescription, diff --git a/packages/core/src/utils-hoist/misc.ts b/packages/core/src/utils-hoist/misc.ts index ee48a2d60c2d..0f02b5ba14de 100644 --- a/packages/core/src/utils-hoist/misc.ts +++ b/packages/core/src/utils-hoist/misc.ts @@ -232,6 +232,8 @@ export function checkOrSetAlreadyCaught(exception: unknown): boolean { * * @param maybeArray Input to turn into an array, if necessary * @returns The input, if already an array, or an array with the input as the only element, if not + * + * @deprecated This function has been deprecated and will not be replaced. */ export function arrayify(maybeArray: T | T[]): T[] { return Array.isArray(maybeArray) ? maybeArray : [maybeArray]; diff --git a/packages/core/test/utils-hoist/misc.test.ts b/packages/core/test/utils-hoist/misc.test.ts index b2c37758e79c..679f0173ffca 100644 --- a/packages/core/test/utils-hoist/misc.test.ts +++ b/packages/core/test/utils-hoist/misc.test.ts @@ -366,16 +366,24 @@ describe('uuid4 generation', () => { describe('arrayify()', () => { it('returns arrays untouched', () => { + // eslint-disable-next-line deprecation/deprecation expect(arrayify([])).toEqual([]); + // eslint-disable-next-line deprecation/deprecation expect(arrayify(['dogs', 'are', 'great'])).toEqual(['dogs', 'are', 'great']); }); it('wraps non-arrays with an array', () => { + // eslint-disable-next-line deprecation/deprecation expect(arrayify(1231)).toEqual([1231]); + // eslint-disable-next-line deprecation/deprecation expect(arrayify('dogs are great')).toEqual(['dogs are great']); + // eslint-disable-next-line deprecation/deprecation expect(arrayify(true)).toEqual([true]); + // eslint-disable-next-line deprecation/deprecation expect(arrayify({})).toEqual([{}]); + // eslint-disable-next-line deprecation/deprecation expect(arrayify(null)).toEqual([null]); + // eslint-disable-next-line deprecation/deprecation expect(arrayify(undefined)).toEqual([undefined]); }); }); diff --git a/packages/nextjs/src/config/webpack.ts b/packages/nextjs/src/config/webpack.ts index 026622318e3f..445ef32ab1e3 100644 --- a/packages/nextjs/src/config/webpack.ts +++ b/packages/nextjs/src/config/webpack.ts @@ -3,7 +3,7 @@ import * as fs from 'fs'; import * as path from 'path'; -import { arrayify, escapeStringForRegex, loadModule, logger } from '@sentry/core'; +import { escapeStringForRegex, loadModule, logger } from '@sentry/core'; import { getSentryRelease } from '@sentry/node'; import * as chalk from 'chalk'; import { sync as resolveSync } from 'resolve'; @@ -491,7 +491,7 @@ function addFilesToWebpackEntryPoint( let newEntryPoint = currentEntryPoint; if (typeof currentEntryPoint === 'string' || Array.isArray(currentEntryPoint)) { - newEntryPoint = arrayify(currentEntryPoint); + newEntryPoint = Array.isArray(currentEntryPoint) ? currentEntryPoint : [currentEntryPoint]; if (newEntryPoint.some(entry => filesToInsert.includes(entry))) { return; } @@ -507,7 +507,7 @@ function addFilesToWebpackEntryPoint( // descriptor object (webpack 5+) else if (typeof currentEntryPoint === 'object' && 'import' in currentEntryPoint) { const currentImportValue = currentEntryPoint.import; - const newImportValue = arrayify(currentImportValue); + const newImportValue = Array.isArray(currentImportValue) ? currentImportValue : [currentImportValue]; if (newImportValue.some(entry => filesToInsert.includes(entry))) { return; } diff --git a/packages/utils/src/index.ts b/packages/utils/src/index.ts index 17e2bb02aa99..126c7a08e36c 100644 --- a/packages/utils/src/index.ts +++ b/packages/utils/src/index.ts @@ -147,6 +147,7 @@ export { loadModule, flatten, memoBuilder, + // eslint-disable-next-line deprecation/deprecation arrayify, normalizeUrlToBase, // eslint-disable-next-line deprecation/deprecation diff --git a/packages/vue/src/integration.ts b/packages/vue/src/integration.ts index c7b7145ac4b1..1ffa6000f1ea 100644 --- a/packages/vue/src/integration.ts +++ b/packages/vue/src/integration.ts @@ -1,5 +1,5 @@ import { defineIntegration, hasTracingEnabled } from '@sentry/core'; -import { GLOBAL_OBJ, arrayify, consoleSandbox } from '@sentry/core'; +import { GLOBAL_OBJ, consoleSandbox } from '@sentry/core'; import type { Client, IntegrationFn } from '@sentry/types'; import { DEFAULT_HOOKS } from './constants'; @@ -48,7 +48,7 @@ Update your \`Sentry.init\` call with an appropriate config option: } if (options.app) { - const apps = arrayify(options.app); + const apps = Array.isArray(options.app) ? options.app : [options.app]; apps.forEach(app => vueInit(app, options)); } else if (options.Vue) { vueInit(options.Vue, options); From 1b0382ebfaa9d69d346b94062202f5dfdad99ceb Mon Sep 17 00:00:00 2001 From: Luca Forstner Date: Fri, 22 Nov 2024 12:23:44 +0100 Subject: [PATCH 03/34] feat(core): Deprecate `validSeverityLevels` (#14407) --- docs/migration/draft-v9-migration-guide.md | 1 + packages/core/src/utils-hoist/index.ts | 1 + packages/core/src/utils-hoist/severity.ts | 17 ++++++----------- packages/core/test/utils-hoist/severity.test.ts | 4 ++-- packages/types/src/severity.ts | 2 -- packages/utils/src/index.ts | 1 + 6 files changed, 11 insertions(+), 15 deletions(-) diff --git a/docs/migration/draft-v9-migration-guide.md b/docs/migration/draft-v9-migration-guide.md index a3e197e4b47c..75cca8f37477 100644 --- a/docs/migration/draft-v9-migration-guide.md +++ b/docs/migration/draft-v9-migration-guide.md @@ -7,6 +7,7 @@ - Deprecated `AddRequestDataToEventOptions.transaction`. This option effectively doesn't do anything anymore, and will be removed in v9. - Deprecated `TransactionNamingScheme` type. +- Deprecated `validSeverityLevels`. Will not be replaced. - Deprecated `urlEncode`. No replacements. - Deprecated `arrayify`. No replacements. diff --git a/packages/core/src/utils-hoist/index.ts b/packages/core/src/utils-hoist/index.ts index 1560187a90d9..7f90dd5fb386 100644 --- a/packages/core/src/utils-hoist/index.ts +++ b/packages/core/src/utils-hoist/index.ts @@ -85,6 +85,7 @@ export type { TransactionNamingScheme, } from './requestdata'; +// eslint-disable-next-line deprecation/deprecation export { severityLevelFromString, validSeverityLevels } from './severity'; export { UNKNOWN_FUNCTION, diff --git a/packages/core/src/utils-hoist/severity.ts b/packages/core/src/utils-hoist/severity.ts index c19c047c90bf..f5217b8b87c9 100644 --- a/packages/core/src/utils-hoist/severity.ts +++ b/packages/core/src/utils-hoist/severity.ts @@ -1,15 +1,8 @@ import type { SeverityLevel } from '@sentry/types'; -// Note: Ideally the `SeverityLevel` type would be derived from `validSeverityLevels`, but that would mean either -// -// a) moving `validSeverityLevels` to `@sentry/types`, -// b) moving the`SeverityLevel` type here, or -// c) importing `validSeverityLevels` from here into `@sentry/types`. -// -// Option A would make `@sentry/types` a runtime dependency of `@sentry/core` (not good), and options B and C would -// create a circular dependency between `@sentry/types` and `@sentry/core` (also not good). So a TODO accompanying the -// type, reminding anyone who changes it to change this list also, will have to do. - +/** + * @deprecated This variable has been deprecated and will be removed in the next major version. + */ export const validSeverityLevels = ['fatal', 'error', 'warning', 'log', 'info', 'debug']; /** @@ -19,5 +12,7 @@ export const validSeverityLevels = ['fatal', 'error', 'warning', 'log', 'info', * @returns The `SeverityLevel` corresponding to the given string, or 'log' if the string isn't a valid level. */ export function severityLevelFromString(level: SeverityLevel | string): SeverityLevel { - return (level === 'warn' ? 'warning' : validSeverityLevels.includes(level) ? level : 'log') as SeverityLevel; + return ( + level === 'warn' ? 'warning' : ['fatal', 'error', 'warning', 'log', 'info', 'debug'].includes(level) ? level : 'log' + ) as SeverityLevel; } diff --git a/packages/core/test/utils-hoist/severity.test.ts b/packages/core/test/utils-hoist/severity.test.ts index 30e3dbb90e97..65388428b65c 100644 --- a/packages/core/test/utils-hoist/severity.test.ts +++ b/packages/core/test/utils-hoist/severity.test.ts @@ -1,4 +1,4 @@ -import { severityLevelFromString, validSeverityLevels } from '../../src/utils-hoist/severity'; +import { severityLevelFromString } from '../../src/utils-hoist/severity'; describe('severityLevelFromString()', () => { test("converts 'warn' to 'warning'", () => { @@ -10,7 +10,7 @@ describe('severityLevelFromString()', () => { }); test('acts as a pass-through for valid level strings', () => { - for (const level of validSeverityLevels) { + for (const level of ['fatal', 'error', 'warning', 'log', 'info', 'debug']) { expect(severityLevelFromString(level)).toBe(level); } }); diff --git a/packages/types/src/severity.ts b/packages/types/src/severity.ts index 8a59bef56f30..73a685f5c5a6 100644 --- a/packages/types/src/severity.ts +++ b/packages/types/src/severity.ts @@ -1,3 +1 @@ -// Note: If this is ever changed, the `validSeverityLevels` array in `@sentry/core` needs to be changed, also. (See -// note there for why we can't derive one from the other.) export type SeverityLevel = 'fatal' | 'error' | 'warning' | 'log' | 'info' | 'debug'; diff --git a/packages/utils/src/index.ts b/packages/utils/src/index.ts index 126c7a08e36c..263ef577777e 100644 --- a/packages/utils/src/index.ts +++ b/packages/utils/src/index.ts @@ -58,6 +58,7 @@ export { winterCGHeadersToDict, winterCGRequestToRequestData, severityLevelFromString, + // eslint-disable-next-line deprecation/deprecation validSeverityLevels, UNKNOWN_FUNCTION, createStackParser, From 9c6028ee9f3c076de3c3c1e0ef439d7d0a189c3a Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Fri, 22 Nov 2024 13:10:25 +0100 Subject: [PATCH 04/34] ci: Fix canary test workflow (#14429) Oops, we forgot this when moving the stuff from utils to core. The canary test workflow only kept the utils build in cache, which lead to this failing - now, we just keep all build output in cache, to be on the safe side... see: https://github.com/getsentry/sentry-javascript/actions/runs/11971041974 --- .github/workflows/canary.yml | 4 ++-- dev-packages/e2e-tests/package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/canary.yml b/.github/workflows/canary.yml index b964e6b3d1b0..046ede6e84d6 100644 --- a/.github/workflows/canary.yml +++ b/.github/workflows/canary.yml @@ -14,11 +14,11 @@ env: CACHED_BUILD_PATHS: | ${{ github.workspace }}/packages/*/*.tgz - ${{ github.workspace }}/dev-packages/test-utils/build ${{ github.workspace }}/node_modules ${{ github.workspace }}/packages/*/node_modules ${{ github.workspace }}/dev-packages/*/node_modules - ${{ github.workspace }}/packages/utils/build + ${{ github.workspace }}/dev-packages/*/build + ${{ github.workspace }}/packages/*/build permissions: contents: read diff --git a/dev-packages/e2e-tests/package.json b/dev-packages/e2e-tests/package.json index 160c27c6fad4..579a22f41d31 100644 --- a/dev-packages/e2e-tests/package.json +++ b/dev-packages/e2e-tests/package.json @@ -16,7 +16,7 @@ "clean": "rimraf tmp node_modules pnpm-lock.yaml && yarn clean:test-applications", "ci:build-matrix": "ts-node ./lib/getTestMatrix.ts", "ci:build-matrix-optional": "ts-node ./lib/getTestMatrix.ts --optional=true", - "clean:test-applications": "rimraf --glob test-applications/**/{node_modules,dist,build,.next,.sveltekit,pnpm-lock.yaml} .last-run.json && pnpm store prune" + "clean:test-applications": "rimraf --glob test-applications/**/{node_modules,dist,build,.next,.sveltekit,pnpm-lock.yaml,.last-run.json,test-results} && pnpm store prune" }, "devDependencies": { "@types/glob": "8.0.0", From 498d6d1a25afac99793dda9700f691223cc6f991 Mon Sep 17 00:00:00 2001 From: Luca Forstner Date: Fri, 22 Nov 2024 13:33:55 +0100 Subject: [PATCH 05/34] feat(utils): Deprecate `@sentry/utils` (#14431) --- docs/migration/draft-v9-migration-guide.md | 2 + packages/utils/README.md | 6 +- packages/utils/src/index.ts | 1236 +++++++++++--------- 3 files changed, 707 insertions(+), 537 deletions(-) diff --git a/docs/migration/draft-v9-migration-guide.md b/docs/migration/draft-v9-migration-guide.md index 75cca8f37477..a71853ee1292 100644 --- a/docs/migration/draft-v9-migration-guide.md +++ b/docs/migration/draft-v9-migration-guide.md @@ -4,6 +4,8 @@ ## `@sentry/utils` +- **The `@sentry/utils` package has been deprecated. Import everything from `@sentry/core` instead.** + - Deprecated `AddRequestDataToEventOptions.transaction`. This option effectively doesn't do anything anymore, and will be removed in v9. - Deprecated `TransactionNamingScheme` type. diff --git a/packages/utils/README.md b/packages/utils/README.md index 812bb86a3525..028fd82c4483 100644 --- a/packages/utils/README.md +++ b/packages/utils/README.md @@ -4,7 +4,11 @@

-# Sentry JavaScript SDK Utilities +# Sentry JavaScript SDK Utilities (DEPRECATED) + +> DEPRECATION NOTICE: The `@sentry/utils` package is deprecated. +> All exports have been moved to `@sentry/core`. +> Import everything from `@sentry/core` instead. [![npm version](https://img.shields.io/npm/v/@sentry/utils.svg)](https://www.npmjs.com/package/@sentry/utils) [![npm dm](https://img.shields.io/npm/dm/@sentry/utils.svg)](https://www.npmjs.com/package/@sentry/utils) diff --git a/packages/utils/src/index.ts b/packages/utils/src/index.ts index 263ef577777e..22fe99a41929 100644 --- a/packages/utils/src/index.ts +++ b/packages/utils/src/index.ts @@ -1,540 +1,704 @@ -export { - applyAggregateErrorsToEvent, - getBreadcrumbLogLevelFromHttpStatusCode, - dsnFromString, - dsnToString, - makeDsn, - SentryError, - GLOBAL_OBJ, - getGlobalSingleton, - addConsoleInstrumentationHandler, - addFetchEndInstrumentationHandler, - addFetchInstrumentationHandler, - addGlobalErrorInstrumentationHandler, - addGlobalUnhandledRejectionInstrumentationHandler, - addHandler, - maybeInstrument, - resetInstrumentationHandlers, - triggerHandlers, - isDOMError, - isDOMException, - isElement, - isError, - isErrorEvent, - isEvent, - isInstanceOf, - isParameterizedString, - isPlainObject, - isPrimitive, - isRegExp, - isString, - isSyntheticEvent, - isThenable, - isVueViewModel, - isBrowser, - CONSOLE_LEVELS, - consoleSandbox, - logger, - originalConsoleMethods, - addContextToFrame, - addExceptionMechanism, - addExceptionTypeValue, - checkOrSetAlreadyCaught, - getEventDescription, - parseSemver, - uuid4, - normalize, - normalizeToSize, - addNonEnumerableProperty, - convertToPlainObject, - dropUndefinedKeys, - extractExceptionKeysForMessage, - fill, - getOriginalFunction, - markFunctionWrapped, - objectify, - makePromiseBuffer, - addNormalizedRequestDataToEvent, - winterCGHeadersToDict, - winterCGRequestToRequestData, - severityLevelFromString, - // eslint-disable-next-line deprecation/deprecation - validSeverityLevels, - UNKNOWN_FUNCTION, - createStackParser, - getFramesFromEvent, - getFunctionName, - stackParserFromStackParserOptions, - stripSentryFramesAndReverse, - filenameIsInApp, - node, - nodeStackLineParser, - isMatchingPattern, - safeJoin, - snipLine, - stringMatchesSomePattern, - truncate, - SyncPromise, - rejectedSyncPromise, - resolvedSyncPromise, - dateTimestampInSeconds, - timestampInSeconds, - TRACEPARENT_REGEXP, - extractTraceparentData, - generateSentryTraceHeader, - propagationContextFromHeaders, - getSDKSource, - isBrowserBundle, - MAX_BAGGAGE_STRING_LENGTH, - SENTRY_BAGGAGE_KEY_PREFIX, - SENTRY_BAGGAGE_KEY_PREFIX_REGEX, - baggageHeaderToDynamicSamplingContext, - dynamicSamplingContextToSentryBaggageHeader, - parseBaggageHeader, - addItemToEnvelope, - createAttachmentEnvelopeItem, - createEnvelope, - createEventEnvelopeHeaders, - createSpanEnvelopeItem, - envelopeContainsItemType, - envelopeItemTypeToDataCategory, - forEachEnvelopeItem, - getSdkMetadataForEnvelopeHeader, - parseEnvelope, - serializeEnvelope, - createClientReportEnvelope, - DEFAULT_RETRY_AFTER, - disabledUntil, - isRateLimited, - parseRetryAfterHeader, - updateRateLimits, - eventFromMessage, - eventFromUnknownInput, - exceptionFromError, - parseStackFrames, - callFrameToStackFrame, - watchdogTimer, - LRUMap, - generatePropagationContext, - vercelWaitUntil, - SDK_VERSION, - getDebugImagesForResources, - getFilenameToDebugIdMap, - escapeStringForRegex, - basename, - dirname, - isAbsolute, - join, - normalizePath, - relative, - resolve, - getComponentName, - getDomElement, - getLocationHref, - htmlTreeAsString, - isNativeFunction, - supportsDOMError, - supportsDOMException, - supportsErrorEvent, - supportsFetch, - supportsNativeFetch, - supportsReferrerPolicy, - supportsReportingObserver, - _browserPerformanceTimeOriginMode, - browserPerformanceTimeOrigin, - supportsHistory, - dynamicRequire, - isNodeEnv, - loadModule, - flatten, - memoBuilder, - // eslint-disable-next-line deprecation/deprecation - arrayify, - normalizeUrlToBase, - // eslint-disable-next-line deprecation/deprecation - urlEncode, - // eslint-disable-next-line deprecation/deprecation - extractPathForTransaction, - DEFAULT_USER_INCLUDES, - extractRequestData, - addRequestDataToEvent, - _asyncNullishCoalesce, - _asyncOptionalChain, - _asyncOptionalChainDelete, - _nullishCoalesce, - _optionalChain, - _optionalChainDelete, - BAGGAGE_HEADER_NAME, - getNumberOfUrlSegments, - getSanitizedUrlString, - parseUrl, - stripUrlQueryAndFragment, - makeFifoCache, +/* eslint-disable max-lines */ +import { + BAGGAGE_HEADER_NAME as BAGGAGE_HEADER_NAME_imported, + CONSOLE_LEVELS as CONSOLE_LEVELS_imported, + DEFAULT_RETRY_AFTER as DEFAULT_RETRY_AFTER_imported, + DEFAULT_USER_INCLUDES as DEFAULT_USER_INCLUDES_imported, + GLOBAL_OBJ as GLOBAL_OBJ_imported, + LRUMap as LRUMap_imported, + MAX_BAGGAGE_STRING_LENGTH as MAX_BAGGAGE_STRING_LENGTH_imported, + SDK_VERSION as SDK_VERSION_imported, + SENTRY_BAGGAGE_KEY_PREFIX as SENTRY_BAGGAGE_KEY_PREFIX_imported, + SENTRY_BAGGAGE_KEY_PREFIX_REGEX as SENTRY_BAGGAGE_KEY_PREFIX_REGEX_imported, + SentryError as SentryError_imported, + SyncPromise as SyncPromise_imported, + TRACEPARENT_REGEXP as TRACEPARENT_REGEXP_imported, + UNKNOWN_FUNCTION as UNKNOWN_FUNCTION_imported, + _asyncNullishCoalesce as _asyncNullishCoalesce_imported, + _asyncOptionalChain as _asyncOptionalChain_imported, + _asyncOptionalChainDelete as _asyncOptionalChainDelete_imported, + _browserPerformanceTimeOriginMode as _browserPerformanceTimeOriginMode_imported, + _nullishCoalesce as _nullishCoalesce_imported, + _optionalChain as _optionalChain_imported, + _optionalChainDelete as _optionalChainDelete_imported, + addConsoleInstrumentationHandler as addConsoleInstrumentationHandler_imported, + addContextToFrame as addContextToFrame_imported, + addExceptionMechanism as addExceptionMechanism_imported, + addExceptionTypeValue as addExceptionTypeValue_imported, + addFetchEndInstrumentationHandler as addFetchEndInstrumentationHandler_imported, + addFetchInstrumentationHandler as addFetchInstrumentationHandler_imported, + addGlobalErrorInstrumentationHandler as addGlobalErrorInstrumentationHandler_imported, + addGlobalUnhandledRejectionInstrumentationHandler as addGlobalUnhandledRejectionInstrumentationHandler_imported, + addHandler as addHandler_imported, + addItemToEnvelope as addItemToEnvelope_imported, + addNonEnumerableProperty as addNonEnumerableProperty_imported, + addNormalizedRequestDataToEvent as addNormalizedRequestDataToEvent_imported, + addRequestDataToEvent as addRequestDataToEvent_imported, + applyAggregateErrorsToEvent as applyAggregateErrorsToEvent_imported, + arrayify as arrayify_imported, + baggageHeaderToDynamicSamplingContext as baggageHeaderToDynamicSamplingContext_imported, + basename as basename_imported, + browserPerformanceTimeOrigin as browserPerformanceTimeOrigin_imported, + callFrameToStackFrame as callFrameToStackFrame_imported, + checkOrSetAlreadyCaught as checkOrSetAlreadyCaught_imported, + consoleSandbox as consoleSandbox_imported, + convertToPlainObject as convertToPlainObject_imported, + createAttachmentEnvelopeItem as createAttachmentEnvelopeItem_imported, + createClientReportEnvelope as createClientReportEnvelope_imported, + createEnvelope as createEnvelope_imported, + createEventEnvelopeHeaders as createEventEnvelopeHeaders_imported, + createSpanEnvelopeItem as createSpanEnvelopeItem_imported, + createStackParser as createStackParser_imported, + dateTimestampInSeconds as dateTimestampInSeconds_imported, + dirname as dirname_imported, + disabledUntil as disabledUntil_imported, + dropUndefinedKeys as dropUndefinedKeys_imported, + dsnFromString as dsnFromString_imported, + dsnToString as dsnToString_imported, + dynamicRequire as dynamicRequire_imported, + dynamicSamplingContextToSentryBaggageHeader as dynamicSamplingContextToSentryBaggageHeader_imported, + envelopeContainsItemType as envelopeContainsItemType_imported, + envelopeItemTypeToDataCategory as envelopeItemTypeToDataCategory_imported, + escapeStringForRegex as escapeStringForRegex_imported, + eventFromMessage as eventFromMessage_imported, + eventFromUnknownInput as eventFromUnknownInput_imported, + exceptionFromError as exceptionFromError_imported, + extractExceptionKeysForMessage as extractExceptionKeysForMessage_imported, + extractPathForTransaction as extractPathForTransaction_imported, + extractRequestData as extractRequestData_imported, + extractTraceparentData as extractTraceparentData_imported, + filenameIsInApp as filenameIsInApp_imported, + fill as fill_imported, + flatten as flatten_imported, + forEachEnvelopeItem as forEachEnvelopeItem_imported, + generatePropagationContext as generatePropagationContext_imported, + generateSentryTraceHeader as generateSentryTraceHeader_imported, + getBreadcrumbLogLevelFromHttpStatusCode as getBreadcrumbLogLevelFromHttpStatusCode_imported, + getComponentName as getComponentName_imported, + getDebugImagesForResources as getDebugImagesForResources_imported, + getDomElement as getDomElement_imported, + getEventDescription as getEventDescription_imported, + getFilenameToDebugIdMap as getFilenameToDebugIdMap_imported, + getFramesFromEvent as getFramesFromEvent_imported, + getFunctionName as getFunctionName_imported, + getGlobalSingleton as getGlobalSingleton_imported, + getLocationHref as getLocationHref_imported, + getNumberOfUrlSegments as getNumberOfUrlSegments_imported, + getOriginalFunction as getOriginalFunction_imported, + getSDKSource as getSDKSource_imported, + getSanitizedUrlString as getSanitizedUrlString_imported, + getSdkMetadataForEnvelopeHeader as getSdkMetadataForEnvelopeHeader_imported, + htmlTreeAsString as htmlTreeAsString_imported, + isAbsolute as isAbsolute_imported, + isBrowser as isBrowser_imported, + isBrowserBundle as isBrowserBundle_imported, + isDOMError as isDOMError_imported, + isDOMException as isDOMException_imported, + isElement as isElement_imported, + isError as isError_imported, + isErrorEvent as isErrorEvent_imported, + isEvent as isEvent_imported, + isInstanceOf as isInstanceOf_imported, + isMatchingPattern as isMatchingPattern_imported, + isNativeFunction as isNativeFunction_imported, + isNodeEnv as isNodeEnv_imported, + isParameterizedString as isParameterizedString_imported, + isPlainObject as isPlainObject_imported, + isPrimitive as isPrimitive_imported, + isRateLimited as isRateLimited_imported, + isRegExp as isRegExp_imported, + isString as isString_imported, + isSyntheticEvent as isSyntheticEvent_imported, + isThenable as isThenable_imported, + isVueViewModel as isVueViewModel_imported, + join as join_imported, + loadModule as loadModule_imported, + logger as logger_imported, + makeDsn as makeDsn_imported, + makeFifoCache as makeFifoCache_imported, + makePromiseBuffer as makePromiseBuffer_imported, + markFunctionWrapped as markFunctionWrapped_imported, + maybeInstrument as maybeInstrument_imported, + memoBuilder as memoBuilder_imported, + node as node_imported, + nodeStackLineParser as nodeStackLineParser_imported, + normalize as normalize_imported, + normalizePath as normalizePath_imported, + normalizeToSize as normalizeToSize_imported, + normalizeUrlToBase as normalizeUrlToBase_imported, + objectify as objectify_imported, + originalConsoleMethods as originalConsoleMethods_imported, + parseBaggageHeader as parseBaggageHeader_imported, + parseEnvelope as parseEnvelope_imported, + parseRetryAfterHeader as parseRetryAfterHeader_imported, + parseSemver as parseSemver_imported, + parseStackFrames as parseStackFrames_imported, + parseUrl as parseUrl_imported, + propagationContextFromHeaders as propagationContextFromHeaders_imported, + rejectedSyncPromise as rejectedSyncPromise_imported, + relative as relative_imported, + resetInstrumentationHandlers as resetInstrumentationHandlers_imported, + resolve as resolve_imported, + resolvedSyncPromise as resolvedSyncPromise_imported, + safeJoin as safeJoin_imported, + serializeEnvelope as serializeEnvelope_imported, + severityLevelFromString as severityLevelFromString_imported, + snipLine as snipLine_imported, + stackParserFromStackParserOptions as stackParserFromStackParserOptions_imported, + stringMatchesSomePattern as stringMatchesSomePattern_imported, + stripSentryFramesAndReverse as stripSentryFramesAndReverse_imported, + stripUrlQueryAndFragment as stripUrlQueryAndFragment_imported, + supportsDOMError as supportsDOMError_imported, + supportsDOMException as supportsDOMException_imported, + supportsErrorEvent as supportsErrorEvent_imported, + supportsFetch as supportsFetch_imported, + supportsHistory as supportsHistory_imported, + supportsNativeFetch as supportsNativeFetch_imported, + supportsReferrerPolicy as supportsReferrerPolicy_imported, + supportsReportingObserver as supportsReportingObserver_imported, + timestampInSeconds as timestampInSeconds_imported, + triggerHandlers as triggerHandlers_imported, + truncate as truncate_imported, + updateRateLimits as updateRateLimits_imported, + urlEncode as urlEncode_imported, + uuid4 as uuid4_imported, + validSeverityLevels as validSeverityLevels_imported, + vercelWaitUntil as vercelWaitUntil_imported, + watchdogTimer as watchdogTimer_imported, + winterCGHeadersToDict as winterCGHeadersToDict_imported, + winterCGRequestToRequestData as winterCGRequestToRequestData_imported, } from '@sentry/core'; -export type { - InternalGlobal, - PromiseBuffer, - RateLimits, - AddRequestDataToEventOptions, - SdkSource, - // eslint-disable-next-line deprecation/deprecation - TransactionNamingScheme, +/** @deprecated Import from `@sentry/core` instead. */ +export const applyAggregateErrorsToEvent = applyAggregateErrorsToEvent_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const getBreadcrumbLogLevelFromHttpStatusCode = getBreadcrumbLogLevelFromHttpStatusCode_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const dsnFromString = dsnFromString_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const dsnToString = dsnToString_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const makeDsn = makeDsn_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const SentryError = SentryError_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const GLOBAL_OBJ = GLOBAL_OBJ_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const getGlobalSingleton = getGlobalSingleton_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const addConsoleInstrumentationHandler = addConsoleInstrumentationHandler_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const addFetchEndInstrumentationHandler = addFetchEndInstrumentationHandler_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const addFetchInstrumentationHandler = addFetchInstrumentationHandler_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const addGlobalErrorInstrumentationHandler = addGlobalErrorInstrumentationHandler_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const addGlobalUnhandledRejectionInstrumentationHandler = + addGlobalUnhandledRejectionInstrumentationHandler_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const addHandler = addHandler_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const maybeInstrument = maybeInstrument_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const resetInstrumentationHandlers = resetInstrumentationHandlers_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const triggerHandlers = triggerHandlers_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const isDOMError = isDOMError_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const isDOMException = isDOMException_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const isElement = isElement_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const isError = isError_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const isErrorEvent = isErrorEvent_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const isEvent = isEvent_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const isInstanceOf = isInstanceOf_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const isParameterizedString = isParameterizedString_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const isPlainObject = isPlainObject_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const isPrimitive = isPrimitive_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const isRegExp = isRegExp_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const isString = isString_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const isSyntheticEvent = isSyntheticEvent_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const isThenable = isThenable_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const isVueViewModel = isVueViewModel_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const isBrowser = isBrowser_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const CONSOLE_LEVELS = CONSOLE_LEVELS_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const consoleSandbox = consoleSandbox_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const logger = logger_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const originalConsoleMethods = originalConsoleMethods_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const addContextToFrame = addContextToFrame_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const addExceptionMechanism = addExceptionMechanism_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const addExceptionTypeValue = addExceptionTypeValue_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const checkOrSetAlreadyCaught = checkOrSetAlreadyCaught_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const getEventDescription = getEventDescription_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const parseSemver = parseSemver_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const uuid4 = uuid4_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const normalize = normalize_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const normalizeToSize = normalizeToSize_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const addNonEnumerableProperty = addNonEnumerableProperty_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const convertToPlainObject = convertToPlainObject_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const dropUndefinedKeys = dropUndefinedKeys_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const extractExceptionKeysForMessage = extractExceptionKeysForMessage_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const fill = fill_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const getOriginalFunction = getOriginalFunction_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const markFunctionWrapped = markFunctionWrapped_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const objectify = objectify_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const makePromiseBuffer = makePromiseBuffer_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const addNormalizedRequestDataToEvent = addNormalizedRequestDataToEvent_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const winterCGHeadersToDict = winterCGHeadersToDict_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const winterCGRequestToRequestData = winterCGRequestToRequestData_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const severityLevelFromString = severityLevelFromString_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +// eslint-disable-next-line deprecation/deprecation +export const validSeverityLevels = validSeverityLevels_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const UNKNOWN_FUNCTION = UNKNOWN_FUNCTION_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const createStackParser = createStackParser_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const getFramesFromEvent = getFramesFromEvent_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const getFunctionName = getFunctionName_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const stackParserFromStackParserOptions = stackParserFromStackParserOptions_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const stripSentryFramesAndReverse = stripSentryFramesAndReverse_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const filenameIsInApp = filenameIsInApp_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const node = node_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const nodeStackLineParser = nodeStackLineParser_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const isMatchingPattern = isMatchingPattern_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const safeJoin = safeJoin_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const snipLine = snipLine_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const stringMatchesSomePattern = stringMatchesSomePattern_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const truncate = truncate_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const SyncPromise = SyncPromise_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const rejectedSyncPromise = rejectedSyncPromise_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const resolvedSyncPromise = resolvedSyncPromise_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const dateTimestampInSeconds = dateTimestampInSeconds_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const timestampInSeconds = timestampInSeconds_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const TRACEPARENT_REGEXP = TRACEPARENT_REGEXP_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const extractTraceparentData = extractTraceparentData_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const generateSentryTraceHeader = generateSentryTraceHeader_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const propagationContextFromHeaders = propagationContextFromHeaders_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const getSDKSource = getSDKSource_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const isBrowserBundle = isBrowserBundle_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const MAX_BAGGAGE_STRING_LENGTH = MAX_BAGGAGE_STRING_LENGTH_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const SENTRY_BAGGAGE_KEY_PREFIX = SENTRY_BAGGAGE_KEY_PREFIX_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const SENTRY_BAGGAGE_KEY_PREFIX_REGEX = SENTRY_BAGGAGE_KEY_PREFIX_REGEX_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const baggageHeaderToDynamicSamplingContext = baggageHeaderToDynamicSamplingContext_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const dynamicSamplingContextToSentryBaggageHeader = dynamicSamplingContextToSentryBaggageHeader_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const parseBaggageHeader = parseBaggageHeader_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const addItemToEnvelope = addItemToEnvelope_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const createAttachmentEnvelopeItem = createAttachmentEnvelopeItem_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const createEnvelope = createEnvelope_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const createEventEnvelopeHeaders = createEventEnvelopeHeaders_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const createSpanEnvelopeItem = createSpanEnvelopeItem_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const envelopeContainsItemType = envelopeContainsItemType_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const envelopeItemTypeToDataCategory = envelopeItemTypeToDataCategory_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const forEachEnvelopeItem = forEachEnvelopeItem_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const getSdkMetadataForEnvelopeHeader = getSdkMetadataForEnvelopeHeader_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const parseEnvelope = parseEnvelope_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const serializeEnvelope = serializeEnvelope_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const createClientReportEnvelope = createClientReportEnvelope_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const DEFAULT_RETRY_AFTER = DEFAULT_RETRY_AFTER_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const disabledUntil = disabledUntil_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const isRateLimited = isRateLimited_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const parseRetryAfterHeader = parseRetryAfterHeader_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const updateRateLimits = updateRateLimits_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const eventFromMessage = eventFromMessage_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const eventFromUnknownInput = eventFromUnknownInput_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const exceptionFromError = exceptionFromError_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const parseStackFrames = parseStackFrames_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const callFrameToStackFrame = callFrameToStackFrame_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const watchdogTimer = watchdogTimer_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const LRUMap = LRUMap_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const generatePropagationContext = generatePropagationContext_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const vercelWaitUntil = vercelWaitUntil_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const SDK_VERSION = SDK_VERSION_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const getDebugImagesForResources = getDebugImagesForResources_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const getFilenameToDebugIdMap = getFilenameToDebugIdMap_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const escapeStringForRegex = escapeStringForRegex_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const basename = basename_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const dirname = dirname_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const isAbsolute = isAbsolute_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const join = join_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const normalizePath = normalizePath_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const relative = relative_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const resolve = resolve_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const getComponentName = getComponentName_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const getDomElement = getDomElement_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const getLocationHref = getLocationHref_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const htmlTreeAsString = htmlTreeAsString_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const isNativeFunction = isNativeFunction_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const supportsDOMError = supportsDOMError_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const supportsDOMException = supportsDOMException_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const supportsErrorEvent = supportsErrorEvent_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const supportsFetch = supportsFetch_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const supportsNativeFetch = supportsNativeFetch_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const supportsReferrerPolicy = supportsReferrerPolicy_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const supportsReportingObserver = supportsReportingObserver_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const _browserPerformanceTimeOriginMode = _browserPerformanceTimeOriginMode_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const browserPerformanceTimeOrigin = browserPerformanceTimeOrigin_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const supportsHistory = supportsHistory_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const dynamicRequire = dynamicRequire_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const isNodeEnv = isNodeEnv_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const loadModule = loadModule_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const flatten = flatten_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const memoBuilder = memoBuilder_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +// eslint-disable-next-line deprecation/deprecation +export const arrayify = arrayify_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const normalizeUrlToBase = normalizeUrlToBase_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +// eslint-disable-next-line deprecation/deprecation +export const urlEncode = urlEncode_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +// eslint-disable-next-line deprecation/deprecation +export const extractPathForTransaction = extractPathForTransaction_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const DEFAULT_USER_INCLUDES = DEFAULT_USER_INCLUDES_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const extractRequestData = extractRequestData_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const addRequestDataToEvent = addRequestDataToEvent_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const _asyncNullishCoalesce = _asyncNullishCoalesce_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const _asyncOptionalChain = _asyncOptionalChain_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const _asyncOptionalChainDelete = _asyncOptionalChainDelete_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const _nullishCoalesce = _nullishCoalesce_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const _optionalChain = _optionalChain_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const _optionalChainDelete = _optionalChainDelete_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const BAGGAGE_HEADER_NAME = BAGGAGE_HEADER_NAME_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const getNumberOfUrlSegments = getNumberOfUrlSegments_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const getSanitizedUrlString = getSanitizedUrlString_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const parseUrl = parseUrl_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const stripUrlQueryAndFragment = stripUrlQueryAndFragment_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export const makeFifoCache = makeFifoCache_imported; + +import type { + AddRequestDataToEventOptions as AddRequestDataToEventOptions_imported, + InternalGlobal as InternalGlobal_imported, + PromiseBuffer as PromiseBuffer_imported, + RateLimits as RateLimits_imported, + SdkSource as SdkSource_imported, + TransactionNamingScheme as TransactionNamingScheme_imported, } from '@sentry/core'; -// TODO(v9/lforst): Uncomment below to add deprecation notices - -// import { -// applyAggregateErrorsToEvent as applyAggregateErrorsToEvent_imported, -// getBreadcrumbLogLevelFromHttpStatusCode as getBreadcrumbLogLevelFromHttpStatusCode_imported, -// dsnFromString as dsnFromString_imported, -// dsnToString as dsnToString_imported, -// makeDsn as makeDsn_imported, -// SentryError as SentryError_imported, -// GLOBAL_OBJ as GLOBAL_OBJ_imported, -// getGlobalSingleton as getGlobalSingleton_imported, -// addConsoleInstrumentationHandler as addConsoleInstrumentationHandler_imported, -// addFetchEndInstrumentationHandler as addFetchEndInstrumentationHandler_imported, -// addFetchInstrumentationHandler as addFetchInstrumentationHandler_imported, -// addGlobalErrorInstrumentationHandler as addGlobalErrorInstrumentationHandler_imported, -// addGlobalUnhandledRejectionInstrumentationHandler as addGlobalUnhandledRejectionInstrumentationHandler_imported, -// addHandler as addHandler_imported, -// maybeInstrument as maybeInstrument_imported, -// resetInstrumentationHandlers as resetInstrumentationHandlers_imported, -// triggerHandlers as triggerHandlers_imported, -// isDOMError as isDOMError_imported, -// isDOMException as isDOMException_imported, -// isElement as isElement_imported, -// isError as isError_imported, -// isErrorEvent as isErrorEvent_imported, -// isEvent as isEvent_imported, -// isInstanceOf as isInstanceOf_imported, -// isParameterizedString as isParameterizedString_imported, -// isPlainObject as isPlainObject_imported, -// isPrimitive as isPrimitive_imported, -// isRegExp as isRegExp_imported, -// isString as isString_imported, -// isSyntheticEvent as isSyntheticEvent_imported, -// isThenable as isThenable_imported, -// isVueViewModel as isVueViewModel_imported, -// isBrowser as isBrowser_imported, -// CONSOLE_LEVELS as CONSOLE_LEVELS_imported, -// consoleSandbox as consoleSandbox_imported, -// logger as logger_imported, -// originalConsoleMethods as originalConsoleMethods_imported, -// addContextToFrame as addContextToFrame_imported, -// addExceptionMechanism as addExceptionMechanism_imported, -// addExceptionTypeValue as addExceptionTypeValue_imported, -// checkOrSetAlreadyCaught as checkOrSetAlreadyCaught_imported, -// getEventDescription as getEventDescription_imported, -// parseSemver as parseSemver_imported, -// uuid4 as uuid4_imported, -// normalize as normalize_imported, -// normalizeToSize as normalizeToSize_imported, -// addNonEnumerableProperty as addNonEnumerableProperty_imported, -// convertToPlainObject as convertToPlainObject_imported, -// dropUndefinedKeys as dropUndefinedKeys_imported, -// extractExceptionKeysForMessage as extractExceptionKeysForMessage_imported, -// fill as fill_imported, -// getOriginalFunction as getOriginalFunction_imported, -// markFunctionWrapped as markFunctionWrapped_imported, -// objectify as objectify_imported, -// makePromiseBuffer as makePromiseBuffer_imported, -// addNormalizedRequestDataToEvent as addNormalizedRequestDataToEvent_imported, -// winterCGHeadersToDict as winterCGHeadersToDict_imported, -// winterCGRequestToRequestData as winterCGRequestToRequestData_imported, -// severityLevelFromString as severityLevelFromString_imported, -// validSeverityLevels as validSeverityLevels_imported, -// UNKNOWN_FUNCTION as UNKNOWN_FUNCTION_imported, -// createStackParser as createStackParser_imported, -// getFramesFromEvent as getFramesFromEvent_imported, -// getFunctionName as getFunctionName_imported, -// stackParserFromStackParserOptions as stackParserFromStackParserOptions_imported, -// stripSentryFramesAndReverse as stripSentryFramesAndReverse_imported, -// filenameIsInApp as filenameIsInApp_imported, -// node as node_imported, -// nodeStackLineParser as nodeStackLineParser_imported, -// isMatchingPattern as isMatchingPattern_imported, -// safeJoin as safeJoin_imported, -// snipLine as snipLine_imported, -// stringMatchesSomePattern as stringMatchesSomePattern_imported, -// truncate as truncate_imported, -// SyncPromise as SyncPromise_imported, -// rejectedSyncPromise as rejectedSyncPromise_imported, -// resolvedSyncPromise as resolvedSyncPromise_imported, -// dateTimestampInSeconds as dateTimestampInSeconds_imported, -// timestampInSeconds as timestampInSeconds_imported, -// TRACEPARENT_REGEXP as TRACEPARENT_REGEXP_imported, -// extractTraceparentData as extractTraceparentData_imported, -// generateSentryTraceHeader as generateSentryTraceHeader_imported, -// propagationContextFromHeaders as propagationContextFromHeaders_imported, -// getSDKSource as getSDKSource_imported, -// isBrowserBundle as isBrowserBundle_imported, -// MAX_BAGGAGE_STRING_LENGTH as MAX_BAGGAGE_STRING_LENGTH_imported, -// SENTRY_BAGGAGE_KEY_PREFIX as SENTRY_BAGGAGE_KEY_PREFIX_imported, -// SENTRY_BAGGAGE_KEY_PREFIX_REGEX as SENTRY_BAGGAGE_KEY_PREFIX_REGEX_imported, -// baggageHeaderToDynamicSamplingContext as baggageHeaderToDynamicSamplingContext_imported, -// dynamicSamplingContextToSentryBaggageHeader as dynamicSamplingContextToSentryBaggageHeader_imported, -// parseBaggageHeader as parseBaggageHeader_imported, -// addItemToEnvelope as addItemToEnvelope_imported, -// createAttachmentEnvelopeItem as createAttachmentEnvelopeItem_imported, -// createEnvelope as createEnvelope_imported, -// createEventEnvelopeHeaders as createEventEnvelopeHeaders_imported, -// createSpanEnvelopeItem as createSpanEnvelopeItem_imported, -// envelopeContainsItemType as envelopeContainsItemType_imported, -// envelopeItemTypeToDataCategory as envelopeItemTypeToDataCategory_imported, -// forEachEnvelopeItem as forEachEnvelopeItem_imported, -// getSdkMetadataForEnvelopeHeader as getSdkMetadataForEnvelopeHeader_imported, -// parseEnvelope as parseEnvelope_imported, -// serializeEnvelope as serializeEnvelope_imported, -// createClientReportEnvelope as createClientReportEnvelope_imported, -// DEFAULT_RETRY_AFTER as DEFAULT_RETRY_AFTER_imported, -// disabledUntil as disabledUntil_imported, -// isRateLimited as isRateLimited_imported, -// parseRetryAfterHeader as parseRetryAfterHeader_imported, -// updateRateLimits as updateRateLimits_imported, -// eventFromMessage as eventFromMessage_imported, -// eventFromUnknownInput as eventFromUnknownInput_imported, -// exceptionFromError as exceptionFromError_imported, -// parseStackFrames as parseStackFrames_imported, -// callFrameToStackFrame as callFrameToStackFrame_imported, -// watchdogTimer as watchdogTimer_imported, -// LRUMap as LRUMap_imported, -// generatePropagationContext as generatePropagationContext_imported, -// vercelWaitUntil as vercelWaitUntil_imported, -// SDK_VERSION as SDK_VERSION_imported, -// getDebugImagesForResources as getDebugImagesForResources_imported, -// getFilenameToDebugIdMap as getFilenameToDebugIdMap_imported, -// escapeStringForRegex as escapeStringForRegex_imported, -// basename as basename_imported, -// dirname as dirname_imported, -// isAbsolute as isAbsolute_imported, -// join as join_imported, -// normalizePath as normalizePath_imported, -// relative as relative_imported, -// resolve as resolve_imported, -// getComponentName as getComponentName_imported, -// getDomElement as getDomElement_imported, -// getLocationHref as getLocationHref_imported, -// htmlTreeAsString as htmlTreeAsString_imported, -// isNativeFunction as isNativeFunction_imported, -// supportsDOMError as supportsDOMError_imported, -// supportsDOMException as supportsDOMException_imported, -// supportsErrorEvent as supportsErrorEvent_imported, -// supportsFetch as supportsFetch_imported, -// supportsNativeFetch as supportsNativeFetch_imported, -// supportsReferrerPolicy as supportsReferrerPolicy_imported, -// supportsReportingObserver as supportsReportingObserver_imported, -// _browserPerformanceTimeOriginMode as _browserPerformanceTimeOriginMode_imported, -// browserPerformanceTimeOrigin as browserPerformanceTimeOrigin_imported, -// supportsHistory as supportsHistory_imported, -// dynamicRequire as dynamicRequire_imported, -// isNodeEnv as isNodeEnv_imported, -// loadModule as loadModule_imported, -// flatten as flatten_imported, -// memoBuilder as memoBuilder_imported, -// arrayify as arrayify_imported, -// normalizeUrlToBase as normalizeUrlToBase_imported, -// urlEncode as urlEncode_imported, -// extractPathForTransaction as extractPathForTransaction_imported, -// DEFAULT_USER_INCLUDES as DEFAULT_USER_INCLUDES_imported, -// extractRequestData as extractRequestData_imported, -// addRequestDataToEvent as addRequestDataToEvent_imported, -// _asyncNullishCoalesce as _asyncNullishCoalesce_imported, -// _asyncOptionalChain as _asyncOptionalChain_imported, -// _asyncOptionalChainDelete as _asyncOptionalChainDelete_imported, -// _nullishCoalesce as _nullishCoalesce_imported, -// _optionalChain as _optionalChain_imported, -// _optionalChainDelete as _optionalChainDelete_imported, -// BAGGAGE_HEADER_NAME as BAGGAGE_HEADER_NAME_imported, -// getNumberOfUrlSegments as getNumberOfUrlSegments_imported, -// getSanitizedUrlString as getSanitizedUrlString_imported, -// parseUrl as parseUrl_imported, -// stripUrlQueryAndFragment as stripUrlQueryAndFragment_imported, -// makeFifoCache as makeFifoCache_imported, -// } from '@sentry/core'; - -// export const applyAggregateErrorsToEvent = applyAggregateErrorsToEvent_imported; -// export const getBreadcrumbLogLevelFromHttpStatusCode = getBreadcrumbLogLevelFromHttpStatusCode_imported; -// export const dsnFromString = dsnFromString_imported; -// export const dsnToString = dsnToString_imported; -// export const makeDsn = makeDsn_imported; -// export const SentryError = SentryError_imported; -// export const GLOBAL_OBJ = GLOBAL_OBJ_imported; -// export const getGlobalSingleton = getGlobalSingleton_imported; -// export const addConsoleInstrumentationHandler = addConsoleInstrumentationHandler_imported; -// export const addFetchEndInstrumentationHandler = addFetchEndInstrumentationHandler_imported; -// export const addFetchInstrumentationHandler = addFetchInstrumentationHandler_imported; -// export const addGlobalErrorInstrumentationHandler = addGlobalErrorInstrumentationHandler_imported; -// export const addGlobalUnhandledRejectionInstrumentationHandler = -// addGlobalUnhandledRejectionInstrumentationHandler_imported; -// export const addHandler = addHandler_imported; -// export const maybeInstrument = maybeInstrument_imported; -// export const resetInstrumentationHandlers = resetInstrumentationHandlers_imported; -// export const triggerHandlers = triggerHandlers_imported; -// export const isDOMError = isDOMError_imported; -// export const isDOMException = isDOMException_imported; -// export const isElement = isElement_imported; -// export const isError = isError_imported; -// export const isErrorEvent = isErrorEvent_imported; -// export const isEvent = isEvent_imported; -// export const isInstanceOf = isInstanceOf_imported; -// export const isParameterizedString = isParameterizedString_imported; -// export const isPlainObject = isPlainObject_imported; -// export const isPrimitive = isPrimitive_imported; -// export const isRegExp = isRegExp_imported; -// export const isString = isString_imported; -// export const isSyntheticEvent = isSyntheticEvent_imported; -// export const isThenable = isThenable_imported; -// export const isVueViewModel = isVueViewModel_imported; -// export const isBrowser = isBrowser_imported; -// export const CONSOLE_LEVELS = CONSOLE_LEVELS_imported; -// export const consoleSandbox = consoleSandbox_imported; -// export const logger = logger_imported; -// export const originalConsoleMethods = originalConsoleMethods_imported; -// export const addContextToFrame = addContextToFrame_imported; -// export const addExceptionMechanism = addExceptionMechanism_imported; -// export const addExceptionTypeValue = addExceptionTypeValue_imported; -// export const checkOrSetAlreadyCaught = checkOrSetAlreadyCaught_imported; -// export const getEventDescription = getEventDescription_imported; -// export const parseSemver = parseSemver_imported; -// export const uuid4 = uuid4_imported; -// export const normalize = normalize_imported; -// export const normalizeToSize = normalizeToSize_imported; -// export const addNonEnumerableProperty = addNonEnumerableProperty_imported; -// export const convertToPlainObject = convertToPlainObject_imported; -// export const dropUndefinedKeys = dropUndefinedKeys_imported; -// export const extractExceptionKeysForMessage = extractExceptionKeysForMessage_imported; -// export const fill = fill_imported; -// export const getOriginalFunction = getOriginalFunction_imported; -// export const markFunctionWrapped = markFunctionWrapped_imported; -// export const objectify = objectify_imported; -// export const makePromiseBuffer = makePromiseBuffer_imported; -// export const addNormalizedRequestDataToEvent = addNormalizedRequestDataToEvent_imported; -// export const winterCGHeadersToDict = winterCGHeadersToDict_imported; -// export const winterCGRequestToRequestData = winterCGRequestToRequestData_imported; -// export const severityLevelFromString = severityLevelFromString_imported; -// export const validSeverityLevels = validSeverityLevels_imported; -// export const UNKNOWN_FUNCTION = UNKNOWN_FUNCTION_imported; -// export const createStackParser = createStackParser_imported; -// export const getFramesFromEvent = getFramesFromEvent_imported; -// export const getFunctionName = getFunctionName_imported; -// export const stackParserFromStackParserOptions = stackParserFromStackParserOptions_imported; -// export const stripSentryFramesAndReverse = stripSentryFramesAndReverse_imported; -// export const filenameIsInApp = filenameIsInApp_imported; -// export const node = node_imported; -// export const nodeStackLineParser = nodeStackLineParser_imported; -// export const isMatchingPattern = isMatchingPattern_imported; -// export const safeJoin = safeJoin_imported; -// export const snipLine = snipLine_imported; -// export const stringMatchesSomePattern = stringMatchesSomePattern_imported; -// export const truncate = truncate_imported; -// export const SyncPromise = SyncPromise_imported; -// export const rejectedSyncPromise = rejectedSyncPromise_imported; -// export const resolvedSyncPromise = resolvedSyncPromise_imported; -// export const dateTimestampInSeconds = dateTimestampInSeconds_imported; -// export const timestampInSeconds = timestampInSeconds_imported; -// export const TRACEPARENT_REGEXP = TRACEPARENT_REGEXP_imported; -// export const extractTraceparentData = extractTraceparentData_imported; -// export const generateSentryTraceHeader = generateSentryTraceHeader_imported; -// export const propagationContextFromHeaders = propagationContextFromHeaders_imported; -// export const getSDKSource = getSDKSource_imported; -// export const isBrowserBundle = isBrowserBundle_imported; -// export const MAX_BAGGAGE_STRING_LENGTH = MAX_BAGGAGE_STRING_LENGTH_imported; -// export const SENTRY_BAGGAGE_KEY_PREFIX = SENTRY_BAGGAGE_KEY_PREFIX_imported; -// export const SENTRY_BAGGAGE_KEY_PREFIX_REGEX = SENTRY_BAGGAGE_KEY_PREFIX_REGEX_imported; -// export const baggageHeaderToDynamicSamplingContext = baggageHeaderToDynamicSamplingContext_imported; -// export const dynamicSamplingContextToSentryBaggageHeader = dynamicSamplingContextToSentryBaggageHeader_imported; -// export const parseBaggageHeader = parseBaggageHeader_imported; -// export const addItemToEnvelope = addItemToEnvelope_imported; -// export const createAttachmentEnvelopeItem = createAttachmentEnvelopeItem_imported; -// export const createEnvelope = createEnvelope_imported; -// export const createEventEnvelopeHeaders = createEventEnvelopeHeaders_imported; -// export const createSpanEnvelopeItem = createSpanEnvelopeItem_imported; -// export const envelopeContainsItemType = envelopeContainsItemType_imported; -// export const envelopeItemTypeToDataCategory = envelopeItemTypeToDataCategory_imported; -// export const forEachEnvelopeItem = forEachEnvelopeItem_imported; -// export const getSdkMetadataForEnvelopeHeader = getSdkMetadataForEnvelopeHeader_imported; -// export const parseEnvelope = parseEnvelope_imported; -// export const serializeEnvelope = serializeEnvelope_imported; -// export const createClientReportEnvelope = createClientReportEnvelope_imported; -// export const DEFAULT_RETRY_AFTER = DEFAULT_RETRY_AFTER_imported; -// export const disabledUntil = disabledUntil_imported; -// export const isRateLimited = isRateLimited_imported; -// export const parseRetryAfterHeader = parseRetryAfterHeader_imported; -// export const updateRateLimits = updateRateLimits_imported; -// export const eventFromMessage = eventFromMessage_imported; -// export const eventFromUnknownInput = eventFromUnknownInput_imported; -// export const exceptionFromError = exceptionFromError_imported; -// export const parseStackFrames = parseStackFrames_imported; -// export const callFrameToStackFrame = callFrameToStackFrame_imported; -// export const watchdogTimer = watchdogTimer_imported; -// export const LRUMap = LRUMap_imported; -// export const generatePropagationContext = generatePropagationContext_imported; -// export const vercelWaitUntil = vercelWaitUntil_imported; -// export const SDK_VERSION = SDK_VERSION_imported; -// export const getDebugImagesForResources = getDebugImagesForResources_imported; -// export const getFilenameToDebugIdMap = getFilenameToDebugIdMap_imported; -// export const escapeStringForRegex = escapeStringForRegex_imported; -// export const basename = basename_imported; -// export const dirname = dirname_imported; -// export const isAbsolute = isAbsolute_imported; -// export const join = join_imported; -// export const normalizePath = normalizePath_imported; -// export const relative = relative_imported; -// export const resolve = resolve_imported; -// export const getComponentName = getComponentName_imported; -// export const getDomElement = getDomElement_imported; -// export const getLocationHref = getLocationHref_imported; -// export const htmlTreeAsString = htmlTreeAsString_imported; -// export const isNativeFunction = isNativeFunction_imported; -// export const supportsDOMError = supportsDOMError_imported; -// export const supportsDOMException = supportsDOMException_imported; -// export const supportsErrorEvent = supportsErrorEvent_imported; -// export const supportsFetch = supportsFetch_imported; -// export const supportsNativeFetch = supportsNativeFetch_imported; -// export const supportsReferrerPolicy = supportsReferrerPolicy_imported; -// export const supportsReportingObserver = supportsReportingObserver_imported; -// export const _browserPerformanceTimeOriginMode = _browserPerformanceTimeOriginMode_imported; -// export const browserPerformanceTimeOrigin = browserPerformanceTimeOrigin_imported; -// export const supportsHistory = supportsHistory_imported; -// export const dynamicRequire = dynamicRequire_imported; -// export const isNodeEnv = isNodeEnv_imported; -// export const loadModule = loadModule_imported; -// export const flatten = flatten_imported; -// export const memoBuilder = memoBuilder_imported; -// export const arrayify = arrayify_imported; -// export const normalizeUrlToBase = normalizeUrlToBase_imported; -// export const urlEncode = urlEncode_imported; -// export const extractPathForTransaction = extractPathForTransaction_imported; -// export const DEFAULT_USER_INCLUDES = DEFAULT_USER_INCLUDES_imported; -// export const extractRequestData = extractRequestData_imported; -// export const addRequestDataToEvent = addRequestDataToEvent_imported; -// export const _asyncNullishCoalesce = _asyncNullishCoalesce_imported; -// export const _asyncOptionalChain = _asyncOptionalChain_imported; -// export const _asyncOptionalChainDelete = _asyncOptionalChainDelete_imported; -// export const _nullishCoalesce = _nullishCoalesce_imported; -// export const _optionalChain = _optionalChain_imported; -// export const _optionalChainDelete = _optionalChainDelete_imported; -// export const BAGGAGE_HEADER_NAME = BAGGAGE_HEADER_NAME_imported; -// export const getNumberOfUrlSegments = getNumberOfUrlSegments_imported; -// export const getSanitizedUrlString = getSanitizedUrlString_imported; -// export const parseUrl = parseUrl_imported; -// export const stripUrlQueryAndFragment = stripUrlQueryAndFragment_imported; -// export const makeFifoCache = makeFifoCache_imported; - -// import type { -// InternalGlobal as InternalGlobal_imported, -// PromiseBuffer as PromiseBuffer_imported, -// RateLimits as RateLimits_imported, -// AddRequestDataToEventOptions as AddRequestDataToEventOptions_imported, -// SdkSource as SdkSource_imported, -// TransactionNamingScheme as TransactionNamingScheme_imported, -// } from '@sentry/core'; - -// export type InternalGlobal = InternalGlobal_imported; -// export type SdkSource = SdkSource_imported; -// export type RateLimits = RateLimits_imported; -// export type AddRequestDataToEventOptions = AddRequestDataToEventOptions_imported; -// export type PromiseBuffer = PromiseBuffer_imported; -// export type TransactionNamingScheme = TransactionNamingScheme_imported; +/** @deprecated Import from `@sentry/core` instead. */ +export type InternalGlobal = InternalGlobal_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export type SdkSource = SdkSource_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export type RateLimits = RateLimits_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export type AddRequestDataToEventOptions = AddRequestDataToEventOptions_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +export type PromiseBuffer = PromiseBuffer_imported; + +/** @deprecated Import from `@sentry/core` instead. */ +// eslint-disable-next-line deprecation/deprecation +export type TransactionNamingScheme = TransactionNamingScheme_imported; From d49f1cbbd1d83a86b0c88fa6f79f5c3322ffef1e Mon Sep 17 00:00:00 2001 From: Luca Forstner Date: Fri, 22 Nov 2024 14:02:28 +0100 Subject: [PATCH 06/34] feat(utils/core): Deprecate `addRequestDataToEvent` and `extractRequestData` (#14430) --- docs/migration/draft-v9-migration-guide.md | 2 ++ packages/astro/src/index.server.ts | 2 ++ packages/aws-serverless/src/index.ts | 2 ++ packages/bun/src/index.ts | 2 ++ packages/core/src/integrations/requestdata.ts | 1 + packages/core/src/utils-hoist/index.ts | 2 ++ packages/core/src/utils-hoist/requestdata.ts | 5 +++++ packages/core/test/utils-hoist/requestdata.test.ts | 1 + packages/google-cloud-serverless/src/index.ts | 2 ++ packages/node/src/index.ts | 1 + packages/remix/src/index.server.ts | 2 ++ packages/solidstart/src/server/index.ts | 2 ++ packages/sveltekit/src/server/index.ts | 2 ++ packages/utils/src/index.ts | 2 ++ 14 files changed, 28 insertions(+) diff --git a/docs/migration/draft-v9-migration-guide.md b/docs/migration/draft-v9-migration-guide.md index a71853ee1292..2dae18a68037 100644 --- a/docs/migration/draft-v9-migration-guide.md +++ b/docs/migration/draft-v9-migration-guide.md @@ -11,6 +11,8 @@ - Deprecated `TransactionNamingScheme` type. - Deprecated `validSeverityLevels`. Will not be replaced. - Deprecated `urlEncode`. No replacements. +- Deprecated `addRequestDataToEvent`. Use `addNormalizedRequestDataToEvent` instead. +- Deprecated `extractRequestData`. Instead manually extract relevant data off request. - Deprecated `arrayify`. No replacements. ## `@sentry/core` diff --git a/packages/astro/src/index.server.ts b/packages/astro/src/index.server.ts index 92e175b9205e..922fdd15dce1 100644 --- a/packages/astro/src/index.server.ts +++ b/packages/astro/src/index.server.ts @@ -12,6 +12,7 @@ export { addEventProcessor, addIntegration, addOpenTelemetryInstrumentation, + // eslint-disable-next-line deprecation/deprecation addRequestDataToEvent, amqplibIntegration, anrIntegration, @@ -39,6 +40,7 @@ export { endSession, expressErrorHandler, expressIntegration, + // eslint-disable-next-line deprecation/deprecation extractRequestData, extraErrorDataIntegration, fastifyIntegration, diff --git a/packages/aws-serverless/src/index.ts b/packages/aws-serverless/src/index.ts index e5e38100a257..369e8824a3b9 100644 --- a/packages/aws-serverless/src/index.ts +++ b/packages/aws-serverless/src/index.ts @@ -42,8 +42,10 @@ export { flush, close, getSentryRelease, + // eslint-disable-next-line deprecation/deprecation addRequestDataToEvent, DEFAULT_USER_INCLUDES, + // eslint-disable-next-line deprecation/deprecation extractRequestData, createGetModuleFromFilename, anrIntegration, diff --git a/packages/bun/src/index.ts b/packages/bun/src/index.ts index 29776ef4ab1d..02718da1153c 100644 --- a/packages/bun/src/index.ts +++ b/packages/bun/src/index.ts @@ -64,8 +64,10 @@ export { flush, close, getSentryRelease, + // eslint-disable-next-line deprecation/deprecation addRequestDataToEvent, DEFAULT_USER_INCLUDES, + // eslint-disable-next-line deprecation/deprecation extractRequestData, createGetModuleFromFilename, anrIntegration, diff --git a/packages/core/src/integrations/requestdata.ts b/packages/core/src/integrations/requestdata.ts index ae016959d9c3..cce86a8966c8 100644 --- a/packages/core/src/integrations/requestdata.ts +++ b/packages/core/src/integrations/requestdata.ts @@ -98,6 +98,7 @@ const _requestDataIntegration = ((options: RequestDataIntegrationOptions = {}) = return event; } + // eslint-disable-next-line deprecation/deprecation return addRequestDataToEvent(event, request, addRequestDataOptions); }, }; diff --git a/packages/core/src/utils-hoist/index.ts b/packages/core/src/utils-hoist/index.ts index 7f90dd5fb386..6d5f49553020 100644 --- a/packages/core/src/utils-hoist/index.ts +++ b/packages/core/src/utils-hoist/index.ts @@ -69,9 +69,11 @@ export type { PromiseBuffer } from './promisebuffer'; export { DEFAULT_USER_INCLUDES, addNormalizedRequestDataToEvent, + // eslint-disable-next-line deprecation/deprecation addRequestDataToEvent, // eslint-disable-next-line deprecation/deprecation extractPathForTransaction, + // eslint-disable-next-line deprecation/deprecation extractRequestData, winterCGHeadersToDict, winterCGRequestToRequestData, diff --git a/packages/core/src/utils-hoist/requestdata.ts b/packages/core/src/utils-hoist/requestdata.ts index 5a40c1fa5945..975b993f69c7 100644 --- a/packages/core/src/utils-hoist/requestdata.ts +++ b/packages/core/src/utils-hoist/requestdata.ts @@ -134,6 +134,8 @@ function extractUserData( * DEFAULT_REQUEST_INCLUDES if not provided. * @param options.deps Injected, platform-specific dependencies * @returns An object containing normalized request data + * + * @deprecated Instead manually normalize the request data into a format that fits `addNormalizedRequestDataToEvent`. */ export function extractRequestData( req: PolymorphicRequest, @@ -318,6 +320,8 @@ export function addNormalizedRequestDataToEvent( * @param options.include Flags to control what data is included * @param options.deps Injected platform-specific dependencies * @returns The mutated `Event` object + * + * @deprecated Use `addNormalizedRequestDataToEvent` instead. */ export function addRequestDataToEvent( event: Event, @@ -335,6 +339,7 @@ export function addRequestDataToEvent( includeRequest.push('ip'); } + // eslint-disable-next-line deprecation/deprecation const extractedRequestData = extractRequestData(req, { include: includeRequest }); event.request = { diff --git a/packages/core/test/utils-hoist/requestdata.test.ts b/packages/core/test/utils-hoist/requestdata.test.ts index 0b1f198ebddc..3df3ee7e84eb 100644 --- a/packages/core/test/utils-hoist/requestdata.test.ts +++ b/packages/core/test/utils-hoist/requestdata.test.ts @@ -1,3 +1,4 @@ +/* eslint-disable deprecation/deprecation */ import type * as net from 'net'; import { addRequestDataToEvent, extractPathForTransaction, extractRequestData } from '@sentry/core'; import type { Event, PolymorphicRequest, TransactionSource, User } from '@sentry/types'; diff --git a/packages/google-cloud-serverless/src/index.ts b/packages/google-cloud-serverless/src/index.ts index 5be557af86c5..415062811fb8 100644 --- a/packages/google-cloud-serverless/src/index.ts +++ b/packages/google-cloud-serverless/src/index.ts @@ -42,8 +42,10 @@ export { flush, close, getSentryRelease, + // eslint-disable-next-line deprecation/deprecation addRequestDataToEvent, DEFAULT_USER_INCLUDES, + // eslint-disable-next-line deprecation/deprecation extractRequestData, createGetModuleFromFilename, anrIntegration, diff --git a/packages/node/src/index.ts b/packages/node/src/index.ts index 2df1e48767ce..df6cce5383a3 100644 --- a/packages/node/src/index.ts +++ b/packages/node/src/index.ts @@ -57,6 +57,7 @@ export { cron } from './cron'; export type { NodeOptions } from './types'; +// eslint-disable-next-line deprecation/deprecation export { addRequestDataToEvent, DEFAULT_USER_INCLUDES, extractRequestData } from '@sentry/core'; export { diff --git a/packages/remix/src/index.server.ts b/packages/remix/src/index.server.ts index d61f759dbf38..2e7dd3708806 100644 --- a/packages/remix/src/index.server.ts +++ b/packages/remix/src/index.server.ts @@ -17,6 +17,7 @@ export { addEventProcessor, addIntegration, addOpenTelemetryInstrumentation, + // eslint-disable-next-line deprecation/deprecation addRequestDataToEvent, amqplibIntegration, anrIntegration, @@ -43,6 +44,7 @@ export { endSession, expressErrorHandler, expressIntegration, + // eslint-disable-next-line deprecation/deprecation extractRequestData, extraErrorDataIntegration, fastifyIntegration, diff --git a/packages/solidstart/src/server/index.ts b/packages/solidstart/src/server/index.ts index b2faa21768ba..d709a373e501 100644 --- a/packages/solidstart/src/server/index.ts +++ b/packages/solidstart/src/server/index.ts @@ -8,6 +8,7 @@ export { addEventProcessor, addIntegration, addOpenTelemetryInstrumentation, + // eslint-disable-next-line deprecation/deprecation addRequestDataToEvent, amqplibIntegration, anrIntegration, @@ -34,6 +35,7 @@ export { endSession, expressErrorHandler, expressIntegration, + // eslint-disable-next-line deprecation/deprecation extractRequestData, extraErrorDataIntegration, fastifyIntegration, diff --git a/packages/sveltekit/src/server/index.ts b/packages/sveltekit/src/server/index.ts index c954d4b1bf78..4247dd46ff7a 100644 --- a/packages/sveltekit/src/server/index.ts +++ b/packages/sveltekit/src/server/index.ts @@ -8,6 +8,7 @@ export { addEventProcessor, addIntegration, addOpenTelemetryInstrumentation, + // eslint-disable-next-line deprecation/deprecation addRequestDataToEvent, amqplibIntegration, anrIntegration, @@ -34,6 +35,7 @@ export { endSession, expressErrorHandler, expressIntegration, + // eslint-disable-next-line deprecation/deprecation extractRequestData, extraErrorDataIntegration, fastifyIntegration, diff --git a/packages/utils/src/index.ts b/packages/utils/src/index.ts index 22fe99a41929..fbd9f9d2aff8 100644 --- a/packages/utils/src/index.ts +++ b/packages/utils/src/index.ts @@ -634,9 +634,11 @@ export const extractPathForTransaction = extractPathForTransaction_imported; export const DEFAULT_USER_INCLUDES = DEFAULT_USER_INCLUDES_imported; /** @deprecated Import from `@sentry/core` instead. */ +// eslint-disable-next-line deprecation/deprecation export const extractRequestData = extractRequestData_imported; /** @deprecated Import from `@sentry/core` instead. */ +// eslint-disable-next-line deprecation/deprecation export const addRequestDataToEvent = addRequestDataToEvent_imported; /** @deprecated Import from `@sentry/core` instead. */ From ce1df3ee836faf5459dc1034a7e1cec7412c59a1 Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Fri, 22 Nov 2024 16:02:37 +0100 Subject: [PATCH 07/34] test(nextjs): Adapt slow test (#14403) --- .../nextjs-app-dir/app/very-slow-component/page.tsx | 2 +- .../nextjs-app-dir/tests/transactions.test.ts | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/app/very-slow-component/page.tsx b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/app/very-slow-component/page.tsx index ab40d1e62d5f..79e534a9e89e 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/app/very-slow-component/page.tsx +++ b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/app/very-slow-component/page.tsx @@ -1,6 +1,6 @@ export const dynamic = 'force-dynamic'; export default async function SuperSlowPage() { - await new Promise(resolve => setTimeout(resolve, 10000)); + await new Promise(resolve => setTimeout(resolve, 5000)); return null; } diff --git a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/transactions.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/transactions.test.ts index 278b6b1074eb..cc22b9da1a40 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/transactions.test.ts +++ b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/transactions.test.ts @@ -125,13 +125,14 @@ test('Should set not_found status for server actions calling notFound()', async test('Will not include spans in pageload transaction with faulty timestamps for slow loading pages', async ({ page, }) => { + test.slow(); const pageloadTransactionEventPromise = waitForTransaction('nextjs-app-dir', transactionEvent => { return ( transactionEvent?.contexts?.trace?.op === 'pageload' && transactionEvent?.transaction === '/very-slow-component' ); }); - await page.goto('/very-slow-component'); + await page.goto('/very-slow-component', { timeout: 11000 }); const pageLoadTransaction = await pageloadTransactionEventPromise; From b0c3f5f0331be0de5b3ced9e182ab893ca8c7044 Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Fri, 22 Nov 2024 16:28:04 +0100 Subject: [PATCH 08/34] feat(core): Log warnings when returning `null` in `beforeSendSpan` (#14433) --- .size-limit.js | 2 +- packages/core/src/baseclient.ts | 2 ++ packages/core/src/envelope.ts | 10 ++++++++-- packages/core/src/utils/spanUtils.ts | 21 +++++++++++++++++++++ 4 files changed, 32 insertions(+), 3 deletions(-) diff --git a/.size-limit.js b/.size-limit.js index b28892aecb89..e97e15f6cd4e 100644 --- a/.size-limit.js +++ b/.size-limit.js @@ -187,7 +187,7 @@ module.exports = [ path: createCDNPath('bundle.tracing.min.js'), gzip: false, brotli: false, - limit: '113 KB', + limit: '120 KB', }, { name: 'CDN Bundle (incl. Tracing, Replay) - uncompressed', diff --git a/packages/core/src/baseclient.ts b/packages/core/src/baseclient.ts index 840992c4ea79..2387523d46ee 100644 --- a/packages/core/src/baseclient.ts +++ b/packages/core/src/baseclient.ts @@ -52,6 +52,7 @@ import { dropUndefinedKeys } from './utils-hoist/object'; import { SyncPromise, rejectedSyncPromise, resolvedSyncPromise } from './utils-hoist/syncpromise'; import { parseSampleRate } from './utils/parseSampleRate'; import { prepareEvent } from './utils/prepareEvent'; +import { showSpanDropWarning } from './utils/spanUtils'; const ALREADY_SEEN_ERROR = "Not capturing exception because it's already been captured."; @@ -977,6 +978,7 @@ function processBeforeSend( if (processedSpan) { processedSpans.push(processedSpan); } else { + showSpanDropWarning(); client.recordDroppedEvent('before_send', 'span'); } } diff --git a/packages/core/src/envelope.ts b/packages/core/src/envelope.ts index ddeb2ce21997..8ecdeb0d604f 100644 --- a/packages/core/src/envelope.ts +++ b/packages/core/src/envelope.ts @@ -24,7 +24,7 @@ import { createSpanEnvelopeItem, getSdkMetadataForEnvelopeHeader, } from './utils-hoist/envelope'; -import { spanToJSON } from './utils/spanUtils'; +import { showSpanDropWarning, spanToJSON } from './utils/spanUtils'; /** * Apply SdkInfo (name, version, packages, integrations) to the corresponding event key. @@ -122,7 +122,13 @@ export function createSpanEnvelope(spans: [SentrySpan, ...SentrySpan[]], client? const beforeSendSpan = client && client.getOptions().beforeSendSpan; const convertToSpanJSON = beforeSendSpan - ? (span: SentrySpan) => beforeSendSpan(spanToJSON(span) as SpanJSON) + ? (span: SentrySpan) => { + const spanJson = beforeSendSpan(spanToJSON(span) as SpanJSON); + if (!spanJson) { + showSpanDropWarning(); + } + return spanJson; + } : (span: SentrySpan) => spanToJSON(span); const items: SpanItem[] = []; diff --git a/packages/core/src/utils/spanUtils.ts b/packages/core/src/utils/spanUtils.ts index 5f0c443919a3..d9232b1f48bc 100644 --- a/packages/core/src/utils/spanUtils.ts +++ b/packages/core/src/utils/spanUtils.ts @@ -17,6 +17,7 @@ import type { MetricType } from '../metrics/types'; import { SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '../semanticAttributes'; import type { SentrySpan } from '../tracing/sentrySpan'; import { SPAN_STATUS_OK, SPAN_STATUS_UNSET } from '../tracing/spanstatus'; +import { consoleSandbox } from '../utils-hoist/logger'; import { addNonEnumerableProperty, dropUndefinedKeys } from '../utils-hoist/object'; import { timestampInSeconds } from '../utils-hoist/time'; import { generateSentryTraceHeader } from '../utils-hoist/tracing'; @@ -26,6 +27,9 @@ import { _getSpanForScope } from './spanOnScope'; export const TRACE_FLAG_NONE = 0x0; export const TRACE_FLAG_SAMPLED = 0x1; +// todo(v9): Remove this once we've stopped dropping spans via `beforeSendSpan` +let hasShownSpanDropWarning = false; + /** * Convert a span to a trace context, which can be sent as the `trace` context in an event. * By default, this will only include trace_id, span_id & parent_span_id. @@ -280,3 +284,20 @@ export function updateMetricSummaryOnActiveSpan( updateMetricSummaryOnSpan(span, metricType, sanitizedName, value, unit, tags, bucketKey); } } + +/** + * Logs a warning once if `beforeSendSpan` is used to drop spans. + * + * todo(v9): Remove this once we've stopped dropping spans via `beforeSendSpan`. + */ +export function showSpanDropWarning(): void { + if (!hasShownSpanDropWarning) { + consoleSandbox(() => { + // eslint-disable-next-line no-console + console.warn( + '[Sentry] Deprecation warning: Returning null from `beforeSendSpan` will be disallowed from SDK version 9.0.0 onwards. The callback will only support mutating spans. To drop certain spans, configure the respective integrations directly.', + ); + }); + hasShownSpanDropWarning = true; + } +} From 6fbab435652ec3a10039c35071ddb79784c69fc1 Mon Sep 17 00:00:00 2001 From: Sigrid Huemer <32902192+s1gr1d@users.noreply.github.com> Date: Fri, 22 Nov 2024 21:51:32 +0100 Subject: [PATCH 09/34] feat(nuxt): Only delete public source maps (#14438) As Nuxt generates and keeps server source maps per default, only the source maps in the public folder are deleted after uploading them to Sentry. --- packages/nuxt/src/vite/sourceMaps.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/nuxt/src/vite/sourceMaps.ts b/packages/nuxt/src/vite/sourceMaps.ts index beea7e18d8f2..2f90094e6138 100644 --- a/packages/nuxt/src/vite/sourceMaps.ts +++ b/packages/nuxt/src/vite/sourceMaps.ts @@ -81,7 +81,7 @@ export function getPluginOptions( consoleSandbox(() => { // eslint-disable-next-line no-console console.log( - '[Sentry] Setting `sentry.sourceMapsUploadOptions.sourcemaps.filesToDeleteAfterUpload: [".*/**/*.map"]` to delete generated source maps after they were uploaded to Sentry.', + '[Sentry] Setting `sentry.sourceMapsUploadOptions.sourcemaps.filesToDeleteAfterUpload: [".*/**/public/**/*.map"]` to delete generated source maps after they were uploaded to Sentry.', ); }); } @@ -108,7 +108,7 @@ export function getPluginOptions( filesToDeleteAfterUpload: sourceMapsUploadOptions.sourcemaps?.filesToDeleteAfterUpload ? sourceMapsUploadOptions.sourcemaps?.filesToDeleteAfterUpload : deleteFilesAfterUpload - ? ['.*/**/*.map'] + ? ['.*/**/public/**/*.map'] : undefined, rewriteSources: (source: string) => normalizePath(source), ...moduleOptions?.unstable_sentryBundlerPluginOptions?.sourcemaps, @@ -279,7 +279,7 @@ function warnExplicitlyDisabledSourceMap(settingKey: string): void { consoleSandbox(() => { // eslint-disable-next-line no-console console.warn( - `[Sentry] Parts of source map generation are currently disabled in your Nuxt configuration (\`${settingKey}: false\`). This setting is either a default setting or was explicitly set in your configuration. Sentry won't override this setting. Without source maps, code snippets on the Sentry Issues page will remain minified. To show unminified code, enable source maps in \`${settingKey}\`.`, + `[Sentry] Parts of source map generation are currently disabled in your Nuxt configuration (\`${settingKey}: false\`). This setting is either a default setting or was explicitly set in your configuration. Sentry won't override this setting. Without source maps, code snippets on the Sentry Issues page will remain minified. To show unminified code, enable source maps in \`${settingKey}\` (e.g. by setting them to \`hidden\`).`, ); }); } From 05479b87e9e0c6cf4e13b3ea688c7e2335c65571 Mon Sep 17 00:00:00 2001 From: Sigrid Huemer <32902192+s1gr1d@users.noreply.github.com> Date: Fri, 22 Nov 2024 23:10:30 +0100 Subject: [PATCH 10/34] feat(nuxt): Add filter for not found source maps (devtools) (#14437) Dev tools sometimes try to access source maps. This filter makes sure this error is not reported in Sentry as it adds noise to the data. --- packages/nuxt/src/server/sdk.ts | 64 +++++++++++++------- packages/nuxt/test/server/sdk.test.ts | 87 ++++++++++++++++++++++++++- 2 files changed, 129 insertions(+), 22 deletions(-) diff --git a/packages/nuxt/src/server/sdk.ts b/packages/nuxt/src/server/sdk.ts index 81ab93e6ccb2..6c4dd4712a1a 100644 --- a/packages/nuxt/src/server/sdk.ts +++ b/packages/nuxt/src/server/sdk.ts @@ -26,30 +26,52 @@ export function init(options: SentryNuxtServerOptions): Client | undefined { const client = initNode(sentryOptions); - getGlobalScope().addEventProcessor( - Object.assign( - (event => { - if (event.type === 'transaction') { - // Filter out transactions for Nuxt build assets - // This regex matches the default path to the nuxt-generated build assets (`_nuxt`). - // todo: the buildAssetDir could be changed in the nuxt config - change this to a more generic solution - if (event.transaction?.match(/^GET \/_nuxt\//)) { - options.debug && - DEBUG_BUILD && - logger.log('NuxtLowQualityTransactionsFilter filtered transaction: ', event.transaction); - return null; - } + getGlobalScope().addEventProcessor(lowQualityTransactionsFilter(options)); + getGlobalScope().addEventProcessor(clientSourceMapErrorFilter(options)); - return event; - } else { - return event; - } - }) satisfies EventProcessor, - { id: 'NuxtLowQualityTransactionsFilter' }, - ), + return client; +} + +/** + * Filter out transactions for Nuxt build assets + * This regex matches the default path to the nuxt-generated build assets (`_nuxt`). + * + * Only exported for testing + */ +export function lowQualityTransactionsFilter(options: SentryNuxtServerOptions): EventProcessor { + return Object.assign( + (event => { + if (event.type === 'transaction' && event.transaction?.match(/^GET \/_nuxt\//)) { + // todo: the buildAssetDir could be changed in the nuxt config - change this to a more generic solution + options.debug && + DEBUG_BUILD && + logger.log('NuxtLowQualityTransactionsFilter filtered transaction: ', event.transaction); + return null; + } else { + return event; + } + }) satisfies EventProcessor, + { id: 'NuxtLowQualityTransactionsFilter' }, ); +} - return client; +/** + * The browser devtools try to get the source maps, but as client source maps may not be available there is going to be an error (no problem for the application though). + * + * Only exported for testing + */ +export function clientSourceMapErrorFilter(options: SentryNuxtServerOptions): EventProcessor { + return Object.assign( + (event => { + const errorMsg = event.exception?.values?.[0]?.value; + if (errorMsg?.match(/^ENOENT: no such file or directory, open '.*\/_nuxt\/.*\.js\.map'/)) { + options.debug && DEBUG_BUILD && logger.log('NuxtClientSourceMapErrorFilter filtered error: ', errorMsg); + return null; + } + return event; + }) satisfies EventProcessor, + { id: 'NuxtClientSourceMapErrorFilter' }, + ); } function getNuxtDefaultIntegrations(options: NodeOptions): Integration[] { diff --git a/packages/nuxt/test/server/sdk.test.ts b/packages/nuxt/test/server/sdk.test.ts index 7ff68478e36d..56888afc9a79 100644 --- a/packages/nuxt/test/server/sdk.test.ts +++ b/packages/nuxt/test/server/sdk.test.ts @@ -1,10 +1,13 @@ import * as SentryNode from '@sentry/node'; import type { NodeClient } from '@sentry/node'; +import { Scope } from '@sentry/node'; +import { getGlobalScope } from '@sentry/node'; import { SDK_VERSION } from '@sentry/node'; +import type { EventProcessor } from '@sentry/types'; import { beforeEach, describe, expect, it, vi } from 'vitest'; import type { SentryNuxtServerOptions } from '../../src/common/types'; import { init } from '../../src/server'; -import { mergeRegisterEsmLoaderHooks } from '../../src/server/sdk'; +import { clientSourceMapErrorFilter, mergeRegisterEsmLoaderHooks } from '../../src/server/sdk'; const nodeInit = vi.spyOn(SentryNode, 'init'); @@ -83,6 +86,88 @@ describe('Nuxt Server SDK', () => { expect.any(Object), ); }); + + it('registers an event processor', async () => { + let passedEventProcessors: EventProcessor[] = []; + const addEventProcessor = vi + .spyOn(getGlobalScope(), 'addEventProcessor') + .mockImplementation((eventProcessor: EventProcessor) => { + passedEventProcessors = [...passedEventProcessors, eventProcessor]; + return new Scope(); + }); + + init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + }); + + expect(addEventProcessor).toHaveBeenCalledTimes(2); + expect(passedEventProcessors[0]?.id).toEqual('NuxtLowQualityTransactionsFilter'); + expect(passedEventProcessors[1]?.id).toEqual('NuxtClientSourceMapErrorFilter'); + }); + }); + + describe('clientSourceMapErrorFilter', () => { + const options = { debug: false }; + const filter = clientSourceMapErrorFilter(options); + + describe('filters out errors', () => { + it.each([ + [ + 'source map errors with leading /', + { + exception: { values: [{ value: "ENOENT: no such file or directory, open '/path/to/_nuxt/file.js.map'" }] }, + }, + ], + [ + 'source map errors without leading /', + { exception: { values: [{ value: "ENOENT: no such file or directory, open 'path/to/_nuxt/file.js.map'" }] } }, + ], + [ + 'source map errors with long path', + { + exception: { + values: [ + { + value: + "ENOENT: no such file or directory, open 'path/to/public/_nuxt/public/long/long/path/file.js.map'", + }, + ], + }, + }, + ], + ])('filters out %s', (_, event) => { + // @ts-expect-error Event type is not correct in tests + expect(filter(event)).toBeNull(); + }); + }); + + describe('does not filter out errors', () => { + it.each([ + ['other errors', { exception: { values: [{ value: 'Some other error' }] } }], + ['events with no exceptions', {}], + [ + 'events without _nuxt in path', + { + exception: { values: [{ value: "ENOENT: no such file or directory, open '/path/to/other/file.js.map'" }] }, + }, + ], + [ + 'source map errors with different casing', + { + exception: { values: [{ value: "ENOENT: No Such file or directory, open '/path/to/_nuxt/file.js.map'" }] }, + }, + ], + [ + 'non-source-map file', + { exception: { values: [{ value: "ENOENT: no such file or directory, open '/path/to/_nuxt/file.js'" }] } }, + ], + ['events with no exception values', { exception: { values: [] } }], + ['events with null exception value', { exception: { values: [null] } }], + ])('does not filter out %s', (_, event) => { + // @ts-expect-error Event type is not correct in tests + expect(filter(event)).toEqual(event); + }); + }); }); describe('mergeRegisterEsmLoaderHooks', () => { From d8d932443cd79ea0438d1646f812a90aa098a23b Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Mon, 25 Nov 2024 09:06:49 +0100 Subject: [PATCH 11/34] test: Add tests to demonstrate root trace ID behavior (#14426) This adds node & browser integration tests to demonstrate the current behavior of "trace propagation" based on the current scope. It shows that the behavior is not really consistent and we should probably adjust it, but for now this PR does no changes. ## Browser In browser, the propagation context of the current scope is always picked up. This means that if you call `startSpan` multiple times in the root, both started spans will have the same trace ID, and possibly the same `parentSpanId` if there was an incoming trace. ## Node In node, the propagation context is ignored for the root context. So in the root, started spans will have different traces, and will also ignore parentSpanId (which is only theoretical, there will never really be a parentSpanId on the root scope in node, realistically). Outside of the root (so e.g. within any route handler, or even any `withScope` call), the behavior is the same as in browser - any started spans will share the same trace ID/parent span ID. ## What should we take away from this? In node, I would align the behavior to ignore the trace ID when we have no incoming trace (so no parentSpanId), and always use the traceId & parentSpanId if there is a parentSpanId on the scope (even the root scope, to be consistent). In browser, we cannot make this change because we rely on this behavior for the extended traces after pageload/navigation spans have ended. --- .../parallel-root-spans-in-scope/subject.js | 4 ++ .../parallel-root-spans-in-scope/test.ts | 41 +++++++++++++++++++ .../subject.js | 8 ++++ .../test.ts | 38 +++++++++++++++++ .../startSpan/parallel-root-spans/subject.js | 2 + .../startSpan/parallel-root-spans/test.ts | 38 +++++++++++++++++ .../utils/helpers.ts | 16 +++++++- .../startSpan/parallel-root-spans/scenario.ts | 30 ++++++++++++++ .../startSpan/parallel-root-spans/test.ts | 29 +++++++++++++ .../scenario.ts | 20 +++++++++ .../test.ts | 34 +++++++++++++++ .../parallel-spans-in-scope/scenario.ts | 26 ++++++++++++ .../startSpan/parallel-spans-in-scope/test.ts | 27 ++++++++++++ packages/opentelemetry/src/trace.ts | 3 +- 14 files changed, 313 insertions(+), 3 deletions(-) create mode 100644 dev-packages/browser-integration-tests/suites/public-api/startSpan/parallel-root-spans-in-scope/subject.js create mode 100644 dev-packages/browser-integration-tests/suites/public-api/startSpan/parallel-root-spans-in-scope/test.ts create mode 100644 dev-packages/browser-integration-tests/suites/public-api/startSpan/parallel-root-spans-with-parentSpanId/subject.js create mode 100644 dev-packages/browser-integration-tests/suites/public-api/startSpan/parallel-root-spans-with-parentSpanId/test.ts create mode 100644 dev-packages/browser-integration-tests/suites/public-api/startSpan/parallel-root-spans/subject.js create mode 100644 dev-packages/browser-integration-tests/suites/public-api/startSpan/parallel-root-spans/test.ts create mode 100644 dev-packages/node-integration-tests/suites/public-api/startSpan/parallel-root-spans/scenario.ts create mode 100644 dev-packages/node-integration-tests/suites/public-api/startSpan/parallel-root-spans/test.ts create mode 100644 dev-packages/node-integration-tests/suites/public-api/startSpan/parallel-spans-in-scope-with-parentSpanId/scenario.ts create mode 100644 dev-packages/node-integration-tests/suites/public-api/startSpan/parallel-spans-in-scope-with-parentSpanId/test.ts create mode 100644 dev-packages/node-integration-tests/suites/public-api/startSpan/parallel-spans-in-scope/scenario.ts create mode 100644 dev-packages/node-integration-tests/suites/public-api/startSpan/parallel-spans-in-scope/test.ts diff --git a/dev-packages/browser-integration-tests/suites/public-api/startSpan/parallel-root-spans-in-scope/subject.js b/dev-packages/browser-integration-tests/suites/public-api/startSpan/parallel-root-spans-in-scope/subject.js new file mode 100644 index 000000000000..1fa6434c2634 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/public-api/startSpan/parallel-root-spans-in-scope/subject.js @@ -0,0 +1,4 @@ +Sentry.withScope(() => { + Sentry.startSpan({ name: 'test_span_1' }, () => undefined); + Sentry.startSpan({ name: 'test_span_2' }, () => undefined); +}); diff --git a/dev-packages/browser-integration-tests/suites/public-api/startSpan/parallel-root-spans-in-scope/test.ts b/dev-packages/browser-integration-tests/suites/public-api/startSpan/parallel-root-spans-in-scope/test.ts new file mode 100644 index 000000000000..8ad6e31eccce --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/public-api/startSpan/parallel-root-spans-in-scope/test.ts @@ -0,0 +1,41 @@ +import { expect } from '@playwright/test'; + +import type { TransactionEvent } from '@sentry/types'; +import { sentryTest } from '../../../../utils/fixtures'; +import { envelopeRequestParser, shouldSkipTracingTest, waitForTransactionRequest } from '../../../../utils/helpers'; + +sentryTest( + 'should send manually started parallel root spans outside of root context', + async ({ getLocalTestUrl, page }) => { + if (shouldSkipTracingTest()) { + sentryTest.skip(); + } + + const url = await getLocalTestUrl({ testDir: __dirname }); + + const transaction1ReqPromise = waitForTransactionRequest(page, event => event.transaction === 'test_span_1'); + const transaction2ReqPromise = waitForTransactionRequest(page, event => event.transaction === 'test_span_2'); + + await page.goto(url); + + const [transaction1Req, transaction2Req] = await Promise.all([transaction1ReqPromise, transaction2ReqPromise]); + + const transaction1 = envelopeRequestParser(transaction1Req); + const transaction2 = envelopeRequestParser(transaction2Req); + + expect(transaction1).toBeDefined(); + expect(transaction2).toBeDefined(); + + const trace1Id = transaction1.contexts?.trace?.trace_id; + const trace2Id = transaction2.contexts?.trace?.trace_id; + + expect(trace1Id).toBeDefined(); + expect(trace2Id).toBeDefined(); + + // We use the same traceID from the root propagation context here + expect(trace1Id).toBe(trace2Id); + + expect(transaction1.contexts?.trace?.parent_span_id).toBeUndefined(); + expect(transaction2.contexts?.trace?.parent_span_id).toBeUndefined(); + }, +); diff --git a/dev-packages/browser-integration-tests/suites/public-api/startSpan/parallel-root-spans-with-parentSpanId/subject.js b/dev-packages/browser-integration-tests/suites/public-api/startSpan/parallel-root-spans-with-parentSpanId/subject.js new file mode 100644 index 000000000000..56c0e05a269c --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/public-api/startSpan/parallel-root-spans-with-parentSpanId/subject.js @@ -0,0 +1,8 @@ +Sentry.getCurrentScope().setPropagationContext({ + parentSpanId: '1234567890123456', + spanId: '123456789012345x', + traceId: '12345678901234567890123456789012', +}); + +Sentry.startSpan({ name: 'test_span_1' }, () => undefined); +Sentry.startSpan({ name: 'test_span_2' }, () => undefined); diff --git a/dev-packages/browser-integration-tests/suites/public-api/startSpan/parallel-root-spans-with-parentSpanId/test.ts b/dev-packages/browser-integration-tests/suites/public-api/startSpan/parallel-root-spans-with-parentSpanId/test.ts new file mode 100644 index 000000000000..212e4808f3e7 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/public-api/startSpan/parallel-root-spans-with-parentSpanId/test.ts @@ -0,0 +1,38 @@ +import { expect } from '@playwright/test'; + +import type { TransactionEvent } from '@sentry/types'; +import { sentryTest } from '../../../../utils/fixtures'; +import { envelopeRequestParser, shouldSkipTracingTest, waitForTransactionRequest } from '../../../../utils/helpers'; + +sentryTest( + 'should send manually started parallel root spans in root context with parentSpanId', + async ({ getLocalTestUrl, page }) => { + if (shouldSkipTracingTest()) { + sentryTest.skip(); + } + + const url = await getLocalTestUrl({ testDir: __dirname }); + + const transaction1ReqPromise = waitForTransactionRequest(page, event => event.transaction === 'test_span_1'); + const transaction2ReqPromise = waitForTransactionRequest(page, event => event.transaction === 'test_span_2'); + + await page.goto(url); + + const [transaction1Req, transaction2Req] = await Promise.all([transaction1ReqPromise, transaction2ReqPromise]); + + const transaction1 = envelopeRequestParser(transaction1Req); + const transaction2 = envelopeRequestParser(transaction2Req); + + expect(transaction1).toBeDefined(); + expect(transaction2).toBeDefined(); + + const trace1Id = transaction1.contexts?.trace?.trace_id; + const trace2Id = transaction2.contexts?.trace?.trace_id; + + expect(trace1Id).toBe('12345678901234567890123456789012'); + expect(trace2Id).toBe('12345678901234567890123456789012'); + + expect(transaction1.contexts?.trace?.parent_span_id).toBe('1234567890123456'); + expect(transaction2.contexts?.trace?.parent_span_id).toBe('1234567890123456'); + }, +); diff --git a/dev-packages/browser-integration-tests/suites/public-api/startSpan/parallel-root-spans/subject.js b/dev-packages/browser-integration-tests/suites/public-api/startSpan/parallel-root-spans/subject.js new file mode 100644 index 000000000000..b07ba4e8aab2 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/public-api/startSpan/parallel-root-spans/subject.js @@ -0,0 +1,2 @@ +Sentry.startSpan({ name: 'test_span_1' }, () => undefined); +Sentry.startSpan({ name: 'test_span_2' }, () => undefined); diff --git a/dev-packages/browser-integration-tests/suites/public-api/startSpan/parallel-root-spans/test.ts b/dev-packages/browser-integration-tests/suites/public-api/startSpan/parallel-root-spans/test.ts new file mode 100644 index 000000000000..47e8e9fa3ac5 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/public-api/startSpan/parallel-root-spans/test.ts @@ -0,0 +1,38 @@ +import { expect } from '@playwright/test'; + +import type { TransactionEvent } from '@sentry/types'; +import { sentryTest } from '../../../../utils/fixtures'; +import { envelopeRequestParser, shouldSkipTracingTest, waitForTransactionRequest } from '../../../../utils/helpers'; + +sentryTest('should send manually started parallel root spans in root context', async ({ getLocalTestUrl, page }) => { + if (shouldSkipTracingTest()) { + sentryTest.skip(); + } + + const url = await getLocalTestUrl({ testDir: __dirname }); + + const transaction1ReqPromise = waitForTransactionRequest(page, event => event.transaction === 'test_span_1'); + const transaction2ReqPromise = waitForTransactionRequest(page, event => event.transaction === 'test_span_2'); + + await page.goto(url); + + const [transaction1Req, transaction2Req] = await Promise.all([transaction1ReqPromise, transaction2ReqPromise]); + + const transaction1 = envelopeRequestParser(transaction1Req); + const transaction2 = envelopeRequestParser(transaction2Req); + + expect(transaction1).toBeDefined(); + expect(transaction2).toBeDefined(); + + const trace1Id = transaction1.contexts?.trace?.trace_id; + const trace2Id = transaction2.contexts?.trace?.trace_id; + + expect(trace1Id).toBeDefined(); + expect(trace2Id).toBeDefined(); + + // We use the same traceID from the root propagation context here + expect(trace1Id).toBe(trace2Id); + + expect(transaction1.contexts?.trace?.parent_span_id).toBeUndefined(); + expect(transaction2.contexts?.trace?.parent_span_id).toBeUndefined(); +}); diff --git a/dev-packages/browser-integration-tests/utils/helpers.ts b/dev-packages/browser-integration-tests/utils/helpers.ts index 01d92a07a8e5..34e591cad0dc 100644 --- a/dev-packages/browser-integration-tests/utils/helpers.ts +++ b/dev-packages/browser-integration-tests/utils/helpers.ts @@ -7,6 +7,7 @@ import type { Event, EventEnvelope, EventEnvelopeHeaders, + TransactionEvent, } from '@sentry/types'; export const envelopeUrlRegex = /\.sentry\.io\/api\/\d+\/envelope\//; @@ -224,7 +225,10 @@ export function waitForErrorRequest(page: Page, callback?: (event: Event) => boo }); } -export function waitForTransactionRequest(page: Page): Promise { +export function waitForTransactionRequest( + page: Page, + callback?: (event: TransactionEvent) => boolean, +): Promise { return page.waitForRequest(req => { const postData = req.postData(); if (!postData) { @@ -234,7 +238,15 @@ export function waitForTransactionRequest(page: Page): Promise { try { const event = envelopeRequestParser(req); - return event.type === 'transaction'; + if (event.type !== 'transaction') { + return false; + } + + if (callback) { + return callback(event as TransactionEvent); + } + + return true; } catch { return false; } diff --git a/dev-packages/node-integration-tests/suites/public-api/startSpan/parallel-root-spans/scenario.ts b/dev-packages/node-integration-tests/suites/public-api/startSpan/parallel-root-spans/scenario.ts new file mode 100644 index 000000000000..e352fff5c02c --- /dev/null +++ b/dev-packages/node-integration-tests/suites/public-api/startSpan/parallel-root-spans/scenario.ts @@ -0,0 +1,30 @@ +import { loggingTransport } from '@sentry-internal/node-integration-tests'; +import * as Sentry from '@sentry/node'; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', + tracesSampleRate: 1.0, + transport: loggingTransport, +}); + +Sentry.getCurrentScope().setPropagationContext({ + parentSpanId: '1234567890123456', + spanId: '123456789012345x', + traceId: '12345678901234567890123456789012', +}); + +const spanIdTraceId = Sentry.startSpan( + { + name: 'test_span_1', + }, + span1 => span1.spanContext().traceId, +); + +Sentry.startSpan( + { + name: 'test_span_2', + attributes: { spanIdTraceId }, + }, + () => undefined, +); diff --git a/dev-packages/node-integration-tests/suites/public-api/startSpan/parallel-root-spans/test.ts b/dev-packages/node-integration-tests/suites/public-api/startSpan/parallel-root-spans/test.ts new file mode 100644 index 000000000000..ed7726d72389 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/public-api/startSpan/parallel-root-spans/test.ts @@ -0,0 +1,29 @@ +import { cleanupChildProcesses, createRunner } from '../../../../utils/runner'; + +afterAll(() => { + cleanupChildProcesses(); +}); + +test('should send manually started parallel root spans in root context', done => { + expect.assertions(7); + + createRunner(__dirname, 'scenario.ts') + .expect({ transaction: { transaction: 'test_span_1' } }) + .expect({ + transaction: transaction => { + expect(transaction).toBeDefined(); + const traceId = transaction.contexts?.trace?.trace_id; + expect(traceId).toBeDefined(); + + // It ignores propagation context of the root context + expect(traceId).not.toBe('12345678901234567890123456789012'); + expect(transaction.contexts?.trace?.parent_span_id).toBeUndefined(); + + // Different trace ID than the first span + const trace1Id = transaction.contexts?.trace?.data?.spanIdTraceId; + expect(trace1Id).toBeDefined(); + expect(trace1Id).not.toBe(traceId); + }, + }) + .start(done); +}); diff --git a/dev-packages/node-integration-tests/suites/public-api/startSpan/parallel-spans-in-scope-with-parentSpanId/scenario.ts b/dev-packages/node-integration-tests/suites/public-api/startSpan/parallel-spans-in-scope-with-parentSpanId/scenario.ts new file mode 100644 index 000000000000..ec761a7d591d --- /dev/null +++ b/dev-packages/node-integration-tests/suites/public-api/startSpan/parallel-spans-in-scope-with-parentSpanId/scenario.ts @@ -0,0 +1,20 @@ +import { loggingTransport } from '@sentry-internal/node-integration-tests'; +import * as Sentry from '@sentry/node'; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', + tracesSampleRate: 1.0, + transport: loggingTransport, +}); + +Sentry.withScope(scope => { + scope.setPropagationContext({ + parentSpanId: '1234567890123456', + spanId: '123456789012345x', + traceId: '12345678901234567890123456789012', + }); + + Sentry.startSpan({ name: 'test_span_1' }, () => undefined); + Sentry.startSpan({ name: 'test_span_2' }, () => undefined); +}); diff --git a/dev-packages/node-integration-tests/suites/public-api/startSpan/parallel-spans-in-scope-with-parentSpanId/test.ts b/dev-packages/node-integration-tests/suites/public-api/startSpan/parallel-spans-in-scope-with-parentSpanId/test.ts new file mode 100644 index 000000000000..9a561ffd391a --- /dev/null +++ b/dev-packages/node-integration-tests/suites/public-api/startSpan/parallel-spans-in-scope-with-parentSpanId/test.ts @@ -0,0 +1,34 @@ +import { cleanupChildProcesses, createRunner } from '../../../../utils/runner'; + +afterAll(() => { + cleanupChildProcesses(); +}); + +test('should send manually started parallel root spans outside of root context with parentSpanId', done => { + createRunner(__dirname, 'scenario.ts') + .expect({ + transaction: { + transaction: 'test_span_1', + contexts: { + trace: { + span_id: expect.any(String), + parent_span_id: '1234567890123456', + trace_id: '12345678901234567890123456789012', + }, + }, + }, + }) + .expect({ + transaction: { + transaction: 'test_span_2', + contexts: { + trace: { + span_id: expect.any(String), + parent_span_id: '1234567890123456', + trace_id: '12345678901234567890123456789012', + }, + }, + }, + }) + .start(done); +}); diff --git a/dev-packages/node-integration-tests/suites/public-api/startSpan/parallel-spans-in-scope/scenario.ts b/dev-packages/node-integration-tests/suites/public-api/startSpan/parallel-spans-in-scope/scenario.ts new file mode 100644 index 000000000000..69a9d5e2c0ef --- /dev/null +++ b/dev-packages/node-integration-tests/suites/public-api/startSpan/parallel-spans-in-scope/scenario.ts @@ -0,0 +1,26 @@ +import { loggingTransport } from '@sentry-internal/node-integration-tests'; +import * as Sentry from '@sentry/node'; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', + tracesSampleRate: 1.0, + transport: loggingTransport, +}); + +Sentry.withScope(() => { + const spanIdTraceId = Sentry.startSpan( + { + name: 'test_span_1', + }, + span1 => span1.spanContext().traceId, + ); + + Sentry.startSpan( + { + name: 'test_span_2', + attributes: { spanIdTraceId }, + }, + () => undefined, + ); +}); diff --git a/dev-packages/node-integration-tests/suites/public-api/startSpan/parallel-spans-in-scope/test.ts b/dev-packages/node-integration-tests/suites/public-api/startSpan/parallel-spans-in-scope/test.ts new file mode 100644 index 000000000000..97ceaa1e382c --- /dev/null +++ b/dev-packages/node-integration-tests/suites/public-api/startSpan/parallel-spans-in-scope/test.ts @@ -0,0 +1,27 @@ +import { cleanupChildProcesses, createRunner } from '../../../../utils/runner'; + +afterAll(() => { + cleanupChildProcesses(); +}); + +test('should send manually started parallel root spans outside of root context', done => { + expect.assertions(6); + + createRunner(__dirname, 'scenario.ts') + .expect({ transaction: { transaction: 'test_span_1' } }) + .expect({ + transaction: transaction => { + expect(transaction).toBeDefined(); + const traceId = transaction.contexts?.trace?.trace_id; + expect(traceId).toBeDefined(); + expect(transaction.contexts?.trace?.parent_span_id).toBeUndefined(); + + const trace1Id = transaction.contexts?.trace?.data?.spanIdTraceId; + expect(trace1Id).toBeDefined(); + + // Same trace ID as the first span + expect(trace1Id).toBe(traceId); + }, + }) + .start(done); +}); diff --git a/packages/opentelemetry/src/trace.ts b/packages/opentelemetry/src/trace.ts index 6ba41eec51e2..1ed0fa6a1322 100644 --- a/packages/opentelemetry/src/trace.ts +++ b/packages/opentelemetry/src/trace.ts @@ -176,8 +176,9 @@ function ensureTimestampInMilliseconds(timestamp: number): number { function getContext(scope: Scope | undefined, forceTransaction: boolean | undefined): Context { const ctx = getContextForScope(scope); + // Note: If the context is the ROOT_CONTEXT, no scope is attached + // Thus we will not use the propagation context in this case, which is desired const actualScope = getScopesFromContext(ctx)?.scope; - const parentSpan = trace.getSpan(ctx); // In the case that we have no parent span, we need to "simulate" one to ensure the propagation context is correct From 807883edfb8799a7560e325e7ca61abe40f68b70 Mon Sep 17 00:00:00 2001 From: Luca Forstner Date: Mon, 25 Nov 2024 09:57:55 +0100 Subject: [PATCH 12/34] feat(vue): Deprecate configuring Vue tracing options anywhere else other than through the `vueIntegration`'s `tracingOptions` option (#14385) --- docs/migration/draft-v9-migration-guide.md | 21 ++++++++ packages/vue/src/integration.ts | 52 ++++++++----------- packages/vue/src/types.ts | 59 +++++++++++++++++++++- packages/vue/test/integration/init.test.ts | 4 +- 4 files changed, 100 insertions(+), 36 deletions(-) diff --git a/docs/migration/draft-v9-migration-guide.md b/docs/migration/draft-v9-migration-guide.md index 2dae18a68037..7fa63bf33b6c 100644 --- a/docs/migration/draft-v9-migration-guide.md +++ b/docs/migration/draft-v9-migration-guide.md @@ -41,6 +41,27 @@ - Deprecated `Request` in favor of `RequestEventData`. +## `@sentry/vue` + +- Deprecated `tracingOptions`, `trackComponents`, `timeout`, `hooks` options everywhere other than in the `tracingOptions` option of the `vueIntegration()`. + These options should now be set as follows: + + ```ts + import * as Sentry from '@sentry/vue'; + + Sentry.init({ + integrations: [ + Sentry.vueIntegration({ + tracingOptions: { + trackComponents: true, + timeout: 1000, + hooks: ['mount', 'update', 'unmount'], + }, + }), + ], + }); + ``` + ## Server-side SDKs (`@sentry/node` and all dependents) - Deprecated `processThreadBreadcrumbIntegration` in favor of `childProcessIntegration`. Functionally they are the same. diff --git a/packages/vue/src/integration.ts b/packages/vue/src/integration.ts index 1ffa6000f1ea..22f394d72720 100644 --- a/packages/vue/src/integration.ts +++ b/packages/vue/src/integration.ts @@ -1,7 +1,4 @@ -import { defineIntegration, hasTracingEnabled } from '@sentry/core'; -import { GLOBAL_OBJ, consoleSandbox } from '@sentry/core'; -import type { Client, IntegrationFn } from '@sentry/types'; - +import { GLOBAL_OBJ, consoleSandbox, defineIntegration, hasTracingEnabled } from '@sentry/core'; import { DEFAULT_HOOKS } from './constants'; import { DEBUG_BUILD } from './debug-build'; import { attachErrorHandler } from './errorhandler'; @@ -22,38 +19,30 @@ const DEFAULT_CONFIG: VueOptions = { const INTEGRATION_NAME = 'Vue'; -const _vueIntegration = ((integrationOptions: Partial = {}) => { +export const vueIntegration = defineIntegration((integrationOptions: Partial = {}) => { return { name: INTEGRATION_NAME, setup(client) { - _setupIntegration(client, integrationOptions); + const options: Options = { ...DEFAULT_CONFIG, ...client.getOptions(), ...integrationOptions }; + if (!options.Vue && !options.app) { + consoleSandbox(() => { + // eslint-disable-next-line no-console + console.warn( + '[@sentry/vue]: Misconfigured SDK. Vue specific errors will not be captured. Update your `Sentry.init` call with an appropriate config option: `app` (Application Instance - Vue 3) or `Vue` (Vue Constructor - Vue 2).', + ); + }); + return; + } + + if (options.app) { + const apps = Array.isArray(options.app) ? options.app : [options.app]; + apps.forEach(app => vueInit(app, options)); + } else if (options.Vue) { + vueInit(options.Vue, options); + } }, }; -}) satisfies IntegrationFn; - -export const vueIntegration = defineIntegration(_vueIntegration); - -function _setupIntegration(client: Client, integrationOptions: Partial): void { - const options: Options = { ...DEFAULT_CONFIG, ...client.getOptions(), ...integrationOptions }; - if (!options.Vue && !options.app) { - consoleSandbox(() => { - // eslint-disable-next-line no-console - console.warn( - `[@sentry/vue]: Misconfigured SDK. Vue specific errors will not be captured. -Update your \`Sentry.init\` call with an appropriate config option: -\`app\` (Application Instance - Vue 3) or \`Vue\` (Vue Constructor - Vue 2).`, - ); - }); - return; - } - - if (options.app) { - const apps = Array.isArray(options.app) ? options.app : [options.app]; - apps.forEach(app => vueInit(app, options)); - } else if (options.Vue) { - vueInit(options.Vue, options); - } -} +}); const vueInit = (app: Vue, options: Options): void => { if (DEBUG_BUILD) { @@ -85,6 +74,7 @@ const vueInit = (app: Vue, options: Options): void => { app.mixin( createTracingMixins({ ...options, + // eslint-disable-next-line deprecation/deprecation ...options.tracingOptions, }), ); diff --git a/packages/vue/src/types.ts b/packages/vue/src/types.ts index 9735923cd52c..8b23a2389e69 100644 --- a/packages/vue/src/types.ts +++ b/packages/vue/src/types.ts @@ -26,7 +26,7 @@ export type ViewModel = { }; }; -export interface VueOptions extends TracingOptions { +export interface VueOptions { /** Vue constructor to be used inside the integration (as imported by `import Vue from 'vue'` in Vue2) */ Vue?: Vue; @@ -60,9 +60,64 @@ export interface VueOptions extends TracingOptions { /** {@link TracingOptions} */ tracingOptions?: Partial; + + /** + * Decides whether to track components by hooking into its lifecycle methods. + * Can be either set to `boolean` to enable/disable tracking for all of them. + * Or to an array of specific component names (case-sensitive). + * + * @deprecated Use tracingOptions + */ + trackComponents: boolean | string[]; + + /** + * How long to wait until the tracked root activity is marked as finished and sent of to Sentry + * + * @deprecated Use tracingOptions + */ + timeout: number; + + /** + * List of hooks to keep track of during component lifecycle. + * Available hooks: 'activate' | 'create' | 'destroy' | 'mount' | 'unmount' | 'update' + * Based on https://vuejs.org/v2/api/#Options-Lifecycle-Hooks + * + * @deprecated Use tracingOptions + */ + hooks: Operation[]; } -export interface Options extends BrowserOptions, VueOptions {} +export interface Options extends BrowserOptions, VueOptions { + /** + * @deprecated Use `vueIntegration` tracingOptions + */ + tracingOptions?: Partial; + + /** + * Decides whether to track components by hooking into its lifecycle methods. + * Can be either set to `boolean` to enable/disable tracking for all of them. + * Or to an array of specific component names (case-sensitive). + * + * @deprecated Use `vueIntegration` tracingOptions + */ + trackComponents: boolean | string[]; + + /** + * How long to wait until the tracked root activity is marked as finished and sent of to Sentry + * + * @deprecated Use `vueIntegration` tracingOptions + */ + timeout: number; + + /** + * List of hooks to keep track of during component lifecycle. + * Available hooks: 'activate' | 'create' | 'destroy' | 'mount' | 'unmount' | 'update' + * Based on https://vuejs.org/v2/api/#Options-Lifecycle-Hooks + * + * @deprecated Use `vueIntegration` tracingOptions + */ + hooks: Operation[]; +} /** Vue specific configuration for Tracing Integration */ export interface TracingOptions { diff --git a/packages/vue/test/integration/init.test.ts b/packages/vue/test/integration/init.test.ts index fd9d7c56fc93..699f99a7057c 100644 --- a/packages/vue/test/integration/init.test.ts +++ b/packages/vue/test/integration/init.test.ts @@ -85,9 +85,7 @@ describe('Sentry.init', () => { app.mount(el); expect(warnings).toEqual([ - `[@sentry/vue]: Misconfigured SDK. Vue specific errors will not be captured. -Update your \`Sentry.init\` call with an appropriate config option: -\`app\` (Application Instance - Vue 3) or \`Vue\` (Vue Constructor - Vue 2).`, + '[@sentry/vue]: Misconfigured SDK. Vue specific errors will not be captured. Update your `Sentry.init` call with an appropriate config option: `app` (Application Instance - Vue 3) or `Vue` (Vue Constructor - Vue 2).', ]); }); From 8f6dd04872a4f6d858f28f512ef90ef828b16c6e Mon Sep 17 00:00:00 2001 From: Taiyu Yoshizawa Date: Mon, 25 Nov 2024 18:01:27 +0900 Subject: [PATCH 13/34] fix(nextjs): Don't report `NEXT_REDIRECT` from browser (#14440) --- packages/nextjs/src/client/index.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/nextjs/src/client/index.ts b/packages/nextjs/src/client/index.ts index 10e1d94566af..5d8c6c46b911 100644 --- a/packages/nextjs/src/client/index.ts +++ b/packages/nextjs/src/client/index.ts @@ -6,6 +6,7 @@ import type { Client, EventProcessor, Integration } from '@sentry/types'; import { devErrorSymbolicationEventProcessor } from '../common/devErrorSymbolicationEventProcessor'; import { getVercelEnv } from '../common/getVercelEnv'; +import { isRedirectNavigationError } from '../common/nextNavigationErrorUtils'; import { browserTracingIntegration } from './browserTracingIntegration'; import { nextjsClientStackFrameNormalizationIntegration } from './clientNormalizationIntegration'; import { INCOMPLETE_APP_ROUTER_INSTRUMENTATION_TRANSACTION_NAME } from './routing/appRouterRoutingInstrumentation'; @@ -47,6 +48,11 @@ export function init(options: BrowserOptions): Client | undefined { filterIncompleteNavigationTransactions.id = 'IncompleteTransactionFilter'; addEventProcessor(filterIncompleteNavigationTransactions); + const filterNextRedirectError: EventProcessor = (event, hint) => + isRedirectNavigationError(hint?.originalException) ? null : event; + filterNextRedirectError.id = 'NextRedirectErrorFilter'; + addEventProcessor(filterNextRedirectError); + if (process.env.NODE_ENV === 'development') { addEventProcessor(devErrorSymbolicationEventProcessor); } From 56e73ce8a512a88dd971e90ad69b993e49105953 Mon Sep 17 00:00:00 2001 From: Daniel Griesser Date: Mon, 25 Nov 2024 10:24:37 +0100 Subject: [PATCH 14/34] chore: Add external contributor to CHANGELOG.md (#14452) This PR adds the external contributor to the CHANGELOG.md file, so that they are credited for their contribution. See #14440 Co-authored-by: chargome <20254395+chargome@users.noreply.github.com> --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1c0db74fffcf..bfacfe82c2a4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,8 @@ - "You miss 100 percent of the chances you don't take. — Wayne Gretzky" — Michael Scott +Work in this release was contributed by @NEKOYASAN. Thank you for your contribution! + ## 8.40.0 ### Important Changes From 8d1dbb1eaa4a4335be9706580fc25aec0f9194a1 Mon Sep 17 00:00:00 2001 From: Luca Forstner Date: Mon, 25 Nov 2024 10:43:27 +0100 Subject: [PATCH 15/34] feat(utils): Deprecate `memoBuilder`, `BAGGAGE_HEADER_NAME`, and `makeFifoCache` (#14434) --- docs/migration/draft-v9-migration-guide.md | 3 +++ packages/browser/src/tracing/request.ts | 3 +-- packages/core/src/fetch.ts | 16 ++++++---------- packages/core/src/utils-hoist/baggage.ts | 3 +++ packages/core/src/utils-hoist/cache.ts | 2 ++ packages/core/src/utils-hoist/index.ts | 3 +++ packages/core/src/utils-hoist/memo.ts | 3 +++ packages/core/src/utils-hoist/normalize.ts | 1 + packages/utils/src/index.ts | 3 +++ 9 files changed, 25 insertions(+), 12 deletions(-) diff --git a/docs/migration/draft-v9-migration-guide.md b/docs/migration/draft-v9-migration-guide.md index 7fa63bf33b6c..dba02dcdd509 100644 --- a/docs/migration/draft-v9-migration-guide.md +++ b/docs/migration/draft-v9-migration-guide.md @@ -14,6 +14,9 @@ - Deprecated `addRequestDataToEvent`. Use `addNormalizedRequestDataToEvent` instead. - Deprecated `extractRequestData`. Instead manually extract relevant data off request. - Deprecated `arrayify`. No replacements. +- Deprecated `memoBuilder`. No replacements. +- Deprecated `BAGGAGE_HEADER_NAME`. No replacements. +- Deprecated `makeFifoCache`. No replacements. ## `@sentry/core` diff --git a/packages/browser/src/tracing/request.ts b/packages/browser/src/tracing/request.ts index 79ed4d0f05b4..225903d32a4b 100644 --- a/packages/browser/src/tracing/request.ts +++ b/packages/browser/src/tracing/request.ts @@ -22,7 +22,6 @@ import { startInactiveSpan, } from '@sentry/core'; import { - BAGGAGE_HEADER_NAME, addFetchEndInstrumentationHandler, addFetchInstrumentationHandler, browserPerformanceTimeOrigin, @@ -449,7 +448,7 @@ function setHeaderOnXhr( // We can therefore simply set a baggage header without checking what was there before // https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/setRequestHeader // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - xhr.setRequestHeader!(BAGGAGE_HEADER_NAME, sentryBaggageHeader); + xhr.setRequestHeader!('baggage', sentryBaggageHeader); } } catch (_) { // Error: InvalidStateError: Failed to execute 'setRequestHeader' on 'XMLHttpRequest': The object's state must be OPENED. diff --git a/packages/core/src/fetch.ts b/packages/core/src/fetch.ts index 5880d2594baa..65629d1291ac 100644 --- a/packages/core/src/fetch.ts +++ b/packages/core/src/fetch.ts @@ -9,11 +9,7 @@ import { startInactiveSpan, } from './tracing'; import { SentryNonRecordingSpan } from './tracing/sentryNonRecordingSpan'; -import { - BAGGAGE_HEADER_NAME, - SENTRY_BAGGAGE_KEY_PREFIX, - dynamicSamplingContextToSentryBaggageHeader, -} from './utils-hoist/baggage'; +import { SENTRY_BAGGAGE_KEY_PREFIX, dynamicSamplingContextToSentryBaggageHeader } from './utils-hoist/baggage'; import { isInstanceOf } from './utils-hoist/is'; import { generateSentryTraceHeader } from './utils-hoist/tracing'; import { parseUrl } from './utils-hoist/url'; @@ -157,11 +153,11 @@ export function addTracingHeadersToFetchRequest( newHeaders.set('sentry-trace', sentryTraceHeader); if (sentryBaggageHeader) { - const prevBaggageHeader = newHeaders.get(BAGGAGE_HEADER_NAME); + const prevBaggageHeader = newHeaders.get('baggage'); if (prevBaggageHeader) { const prevHeaderStrippedFromSentryBaggage = stripBaggageHeaderOfSentryBaggageValues(prevBaggageHeader); newHeaders.set( - BAGGAGE_HEADER_NAME, + 'baggage', // If there are non-sentry entries (i.e. if the stripped string is non-empty/truthy) combine the stripped header and sentry baggage header // otherwise just set the sentry baggage header prevHeaderStrippedFromSentryBaggage @@ -169,7 +165,7 @@ export function addTracingHeadersToFetchRequest( : sentryBaggageHeader, ); } else { - newHeaders.set(BAGGAGE_HEADER_NAME, sentryBaggageHeader); + newHeaders.set('baggage', sentryBaggageHeader); } } @@ -183,7 +179,7 @@ export function addTracingHeadersToFetchRequest( }) // Get rid of previous sentry baggage values in baggage header .map(header => { - if (Array.isArray(header) && header[0] === BAGGAGE_HEADER_NAME && typeof header[1] === 'string') { + if (Array.isArray(header) && header[0] === 'baggage' && typeof header[1] === 'string') { const [headerName, headerValue, ...rest] = header; return [headerName, stripBaggageHeaderOfSentryBaggageValues(headerValue), ...rest]; } else { @@ -197,7 +193,7 @@ export function addTracingHeadersToFetchRequest( if (sentryBaggageHeader) { // If there are multiple entries with the same key, the browser will merge the values into a single request header. // Its therefore safe to simply push a "baggage" entry, even though there might already be another baggage header. - newHeaders.push([BAGGAGE_HEADER_NAME, sentryBaggageHeader]); + newHeaders.push(['baggage', sentryBaggageHeader]); } return newHeaders as PolymorphicRequestHeaders; diff --git a/packages/core/src/utils-hoist/baggage.ts b/packages/core/src/utils-hoist/baggage.ts index 8cc2dfd68ef2..93c8c3918748 100644 --- a/packages/core/src/utils-hoist/baggage.ts +++ b/packages/core/src/utils-hoist/baggage.ts @@ -4,6 +4,9 @@ import { DEBUG_BUILD } from './debug-build'; import { isString } from './is'; import { logger } from './logger'; +/** + * @deprecated Use a `"baggage"` string directly + */ export const BAGGAGE_HEADER_NAME = 'baggage'; export const SENTRY_BAGGAGE_KEY_PREFIX = 'sentry-'; diff --git a/packages/core/src/utils-hoist/cache.ts b/packages/core/src/utils-hoist/cache.ts index 412970e77c76..376f8ef970cc 100644 --- a/packages/core/src/utils-hoist/cache.ts +++ b/packages/core/src/utils-hoist/cache.ts @@ -1,6 +1,8 @@ /** * Creates a cache that evicts keys in fifo order * @param size {Number} + * + * @deprecated This function is deprecated and will be removed in the next major version. */ export function makeFifoCache( size: number, diff --git a/packages/core/src/utils-hoist/index.ts b/packages/core/src/utils-hoist/index.ts index 6d5f49553020..cd5b107ce6e6 100644 --- a/packages/core/src/utils-hoist/index.ts +++ b/packages/core/src/utils-hoist/index.ts @@ -35,6 +35,7 @@ export { } from './is'; export { isBrowser } from './isBrowser'; export { CONSOLE_LEVELS, consoleSandbox, logger, originalConsoleMethods } from './logger'; +// eslint-disable-next-line deprecation/deprecation export { memoBuilder } from './memo'; export { addContextToFrame, @@ -147,6 +148,7 @@ export { } from './ratelimit'; export type { RateLimits } from './ratelimit'; export { + // eslint-disable-next-line deprecation/deprecation BAGGAGE_HEADER_NAME, MAX_BAGGAGE_STRING_LENGTH, SENTRY_BAGGAGE_KEY_PREFIX, @@ -157,6 +159,7 @@ export { } from './baggage'; export { getNumberOfUrlSegments, getSanitizedUrlString, parseUrl, stripUrlQueryAndFragment } from './url'; +// eslint-disable-next-line deprecation/deprecation export { makeFifoCache } from './cache'; export { eventFromMessage, eventFromUnknownInput, exceptionFromError, parseStackFrames } from './eventbuilder'; export { callFrameToStackFrame, watchdogTimer } from './anr'; diff --git a/packages/core/src/utils-hoist/memo.ts b/packages/core/src/utils-hoist/memo.ts index d76f60579bc4..f7303bd44ece 100644 --- a/packages/core/src/utils-hoist/memo.ts +++ b/packages/core/src/utils-hoist/memo.ts @@ -10,7 +10,10 @@ export type MemoFunc = [ /** * Helper to decycle json objects + * + * @deprecated This function is deprecated and will be removed in the next major version. */ +// TODO(v9): Move this function into normalize() directly export function memoBuilder(): MemoFunc { const hasWeakSet = typeof WeakSet === 'function'; const inner: any = hasWeakSet ? new WeakSet() : []; diff --git a/packages/core/src/utils-hoist/normalize.ts b/packages/core/src/utils-hoist/normalize.ts index e88b1edd8513..fe108fdbdd0e 100644 --- a/packages/core/src/utils-hoist/normalize.ts +++ b/packages/core/src/utils-hoist/normalize.ts @@ -74,6 +74,7 @@ function visit( value: unknown, depth: number = +Infinity, maxProperties: number = +Infinity, + // eslint-disable-next-line deprecation/deprecation memo: MemoFunc = memoBuilder(), ): Primitive | ObjOrArray { const [memoize, unmemoize] = memo; diff --git a/packages/utils/src/index.ts b/packages/utils/src/index.ts index fbd9f9d2aff8..bf0963131200 100644 --- a/packages/utils/src/index.ts +++ b/packages/utils/src/index.ts @@ -613,6 +613,7 @@ export const loadModule = loadModule_imported; export const flatten = flatten_imported; /** @deprecated Import from `@sentry/core` instead. */ +// eslint-disable-next-line deprecation/deprecation export const memoBuilder = memoBuilder_imported; /** @deprecated Import from `@sentry/core` instead. */ @@ -660,6 +661,7 @@ export const _optionalChain = _optionalChain_imported; export const _optionalChainDelete = _optionalChainDelete_imported; /** @deprecated Import from `@sentry/core` instead. */ +// eslint-disable-next-line deprecation/deprecation export const BAGGAGE_HEADER_NAME = BAGGAGE_HEADER_NAME_imported; /** @deprecated Import from `@sentry/core` instead. */ @@ -675,6 +677,7 @@ export const parseUrl = parseUrl_imported; export const stripUrlQueryAndFragment = stripUrlQueryAndFragment_imported; /** @deprecated Import from `@sentry/core` instead. */ +// eslint-disable-next-line deprecation/deprecation export const makeFifoCache = makeFifoCache_imported; import type { From 1fd377fd9c8c5e46fa36ad3c11d23412baff6572 Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Mon, 25 Nov 2024 11:39:45 +0100 Subject: [PATCH 16/34] test(nextjs): Add test for redirects from server actions (#14451) --- .../app/redirect/destination/page.tsx | 7 +++ .../nextjs-15/app/redirect/origin/page.tsx | 18 +++++++ .../tests/server-action-redirect.test.ts | 47 +++++++++++++++++++ 3 files changed, 72 insertions(+) create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-15/app/redirect/destination/page.tsx create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-15/app/redirect/origin/page.tsx create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-15/tests/server-action-redirect.test.ts diff --git a/dev-packages/e2e-tests/test-applications/nextjs-15/app/redirect/destination/page.tsx b/dev-packages/e2e-tests/test-applications/nextjs-15/app/redirect/destination/page.tsx new file mode 100644 index 000000000000..5583d36b04b0 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-15/app/redirect/destination/page.tsx @@ -0,0 +1,7 @@ +export default function RedirectDestinationPage() { + return ( +
+

Redirect Destination

+
+ ); +} diff --git a/dev-packages/e2e-tests/test-applications/nextjs-15/app/redirect/origin/page.tsx b/dev-packages/e2e-tests/test-applications/nextjs-15/app/redirect/origin/page.tsx new file mode 100644 index 000000000000..52615e0a054b --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-15/app/redirect/origin/page.tsx @@ -0,0 +1,18 @@ +import { redirect } from 'next/navigation'; + +async function redirectAction() { + 'use server'; + + redirect('/redirect/destination'); +} + +export default function RedirectOriginPage() { + return ( + <> + {/* @ts-ignore */} +
+ +
+ + ); +} diff --git a/dev-packages/e2e-tests/test-applications/nextjs-15/tests/server-action-redirect.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-15/tests/server-action-redirect.test.ts new file mode 100644 index 000000000000..d46936fa6b2f --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-15/tests/server-action-redirect.test.ts @@ -0,0 +1,47 @@ +import { expect, test } from '@playwright/test'; +import { waitForError, waitForTransaction } from '@sentry-internal/test-utils'; + +test('Should handle server action redirect without capturing errors', async ({ page }) => { + // Wait for the initial page load transaction + const pageLoadTransactionPromise = waitForTransaction('nextjs-15', async transactionEvent => { + return transactionEvent?.transaction === '/redirect/origin'; + }); + + // Navigate to the origin page + await page.goto('/redirect/origin'); + + const pageLoadTransaction = await pageLoadTransactionPromise; + expect(pageLoadTransaction).toBeDefined(); + + // Wait for the redirect transaction + const redirectTransactionPromise = waitForTransaction('nextjs-15', async transactionEvent => { + return transactionEvent?.transaction === 'GET /redirect/destination'; + }); + + // No error should be captured + const redirectErrorPromise = waitForError('nextjs-15', async errorEvent => { + return !!errorEvent; + }); + + // Click the redirect button + await page.click('button[type="submit"]'); + + await redirectTransactionPromise; + + // Verify we got redirected to the destination page + await expect(page).toHaveURL('/redirect/destination'); + + // Wait for potential errors with a 2 second timeout + const errorTimeout = new Promise((_, reject) => + setTimeout(() => reject(new Error('No error captured (timeout)')), 2000), + ); + + // We expect this to timeout since no error should be captured during the redirect + try { + await Promise.race([redirectErrorPromise, errorTimeout]); + throw new Error('Expected no error to be captured, but an error was found'); + } catch (e) { + // If we get a timeout error (as expected), no error was captured + expect((e as Error).message).toBe('No error captured (timeout)'); + } +}); From 1b7815aab90740b305ab471c231a325866dec3df Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Mon, 25 Nov 2024 12:58:15 +0100 Subject: [PATCH 17/34] test(nextjs): Fix turbopack test (#14455) --- .../e2e-tests/test-applications/nextjs-turbo/.npmrc | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/dev-packages/e2e-tests/test-applications/nextjs-turbo/.npmrc b/dev-packages/e2e-tests/test-applications/nextjs-turbo/.npmrc index 070f80f05092..c0bee06878d1 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-turbo/.npmrc +++ b/dev-packages/e2e-tests/test-applications/nextjs-turbo/.npmrc @@ -1,2 +1,8 @@ @sentry:registry=http://127.0.0.1:4873 @sentry-internal:registry=http://127.0.0.1:4873 + +# todo: check if this is still needed in upcoming versions +# Hoist all dependencies to the root level due to issues with import-in-the-middle and require-in-the-middle +# Just adding these as dependencies removed the warnings, but didn't fix the issue +shamefully-hoist=true +strict-peer-dependencies=false From 3742bd39404d0131759ce855f8cba0e3454ab63c Mon Sep 17 00:00:00 2001 From: Andrei <168741329+andreiborza@users.noreply.github.com> Date: Mon, 25 Nov 2024 13:35:41 +0100 Subject: [PATCH 18/34] feat(core): Deprecate `flatten` (#14454) Ref: https://github.com/getsentry/sentry-javascript/issues/14267 From my investigation, redis does not actually support arrays/nested arrays for keys and ioredis is wrapping thinly over redis functions, but otel has `any[]` as one of their possible values for command line arguments so I opted for inlining the implementation and adding a note to use `Array.flat` in v9. --- packages/core/src/utils-hoist/array.ts | 5 ++++- packages/core/src/utils-hoist/index.ts | 1 + packages/core/test/utils-hoist/array.test.ts | 9 +++++++++ packages/node/src/utils/redisCache.ts | 21 +++++++++++++++++++- packages/nuxt/src/vite/utils.ts | 21 +++++++++++--------- packages/utils/src/index.ts | 1 + 6 files changed, 47 insertions(+), 11 deletions(-) diff --git a/packages/core/src/utils-hoist/array.ts b/packages/core/src/utils-hoist/array.ts index 15f08ba541b8..412e22224156 100644 --- a/packages/core/src/utils-hoist/array.ts +++ b/packages/core/src/utils-hoist/array.ts @@ -1,6 +1,9 @@ export type NestedArray = Array | T>; -/** Flattens a multi-dimensional array */ +/** Flattens a multi-dimensional array + * + * @deprecated This function is deprecated and will be removed in the next major version. + */ export function flatten(input: NestedArray): T[] { const result: T[] = []; diff --git a/packages/core/src/utils-hoist/index.ts b/packages/core/src/utils-hoist/index.ts index cd5b107ce6e6..c04f314c40f2 100644 --- a/packages/core/src/utils-hoist/index.ts +++ b/packages/core/src/utils-hoist/index.ts @@ -1,4 +1,5 @@ export { applyAggregateErrorsToEvent } from './aggregate-errors'; +// eslint-disable-next-line deprecation/deprecation export { flatten } from './array'; export { getBreadcrumbLogLevelFromHttpStatusCode } from './breadcrumb-log-level'; export { getComponentName, getDomElement, getLocationHref, htmlTreeAsString } from './browser'; diff --git a/packages/core/test/utils-hoist/array.test.ts b/packages/core/test/utils-hoist/array.test.ts index fb788d48cea9..3716a6d190b1 100644 --- a/packages/core/test/utils-hoist/array.test.ts +++ b/packages/core/test/utils-hoist/array.test.ts @@ -1,16 +1,19 @@ import type { NestedArray } from '../../src/utils-hoist/array'; +// eslint-disable-next-line deprecation/deprecation import { flatten } from '../../src/utils-hoist/array'; describe('flatten', () => { it('should return the same array when input is a flat array', () => { const input = [1, 2, 3, 4]; const expected = [1, 2, 3, 4]; + // eslint-disable-next-line deprecation/deprecation expect(flatten(input)).toEqual(expected); }); it('should flatten a nested array of numbers', () => { const input = [[1, 2, [3]], 4]; const expected = [1, 2, 3, 4]; + // eslint-disable-next-line deprecation/deprecation expect(flatten(input)).toEqual(expected); }); @@ -20,6 +23,7 @@ describe('flatten', () => { ['How', 'Are', 'You'], ]; const expected = ['Hello', 'World', 'How', 'Are', 'You']; + // eslint-disable-next-line deprecation/deprecation expect(flatten(input)).toEqual(expected); }); @@ -29,30 +33,35 @@ describe('flatten', () => { [{ a: 3 }, { b: 4 }], ]; const expected = [{ a: 1 }, { b: 2 }, { a: 3 }, { b: 4 }]; + // eslint-disable-next-line deprecation/deprecation expect(flatten(input)).toEqual(expected); }); it('should flatten a mixed type array', () => { const input: NestedArray = [['a', { b: 2 }, 'c'], 'd']; const expected = ['a', { b: 2 }, 'c', 'd']; + // eslint-disable-next-line deprecation/deprecation expect(flatten(input)).toEqual(expected); }); it('should flatten a deeply nested array', () => { const input = [1, [2, [3, [4, [5]]]]]; const expected = [1, 2, 3, 4, 5]; + // eslint-disable-next-line deprecation/deprecation expect(flatten(input)).toEqual(expected); }); it('should return an empty array when input is empty', () => { const input: any[] = []; const expected: any[] = []; + // eslint-disable-next-line deprecation/deprecation expect(flatten(input)).toEqual(expected); }); it('should return the same array when input is a flat array', () => { const input = [1, 'a', { b: 2 }, 'c', 3]; const expected = [1, 'a', { b: 2 }, 'c', 3]; + // eslint-disable-next-line deprecation/deprecation expect(flatten(input)).toEqual(expected); }); }); diff --git a/packages/node/src/utils/redisCache.ts b/packages/node/src/utils/redisCache.ts index cb411f304cf7..2a963710e0d5 100644 --- a/packages/node/src/utils/redisCache.ts +++ b/packages/node/src/utils/redisCache.ts @@ -1,5 +1,4 @@ import type { CommandArgs as IORedisCommandArgs } from '@opentelemetry/instrumentation-ioredis'; -import { flatten } from '@sentry/core'; const SINGLE_ARG_COMMANDS = ['get', 'set', 'setex']; @@ -95,3 +94,23 @@ export function calculateCacheItemSize(response: unknown): number | undefined { }, 0) : getSize(response); } + +// TODO(v9): This is inlined from core so we can deprecate `flatten`. +// It's usage can be replaced with `Array.flat` in v9. +type NestedArray = Array | T>; +function flatten(input: NestedArray): T[] { + const result: T[] = []; + + const flattenHelper = (input: NestedArray): void => { + input.forEach((el: T | NestedArray) => { + if (Array.isArray(el)) { + flattenHelper(el as NestedArray); + } else { + result.push(el as T); + } + }); + }; + + flattenHelper(input); + return result; +} diff --git a/packages/nuxt/src/vite/utils.ts b/packages/nuxt/src/vite/utils.ts index d18fd1dcc484..fff676a6ede1 100644 --- a/packages/nuxt/src/vite/utils.ts +++ b/packages/nuxt/src/vite/utils.ts @@ -1,6 +1,6 @@ import * as fs from 'fs'; import * as path from 'path'; -import { consoleSandbox, flatten } from '@sentry/core'; +import { consoleSandbox } from '@sentry/core'; /** * Find the default SDK init file for the given type (client or server). @@ -92,18 +92,21 @@ export function constructWrappedFunctionExportQuery( entrypointWrappedFunctions: string[], debug?: boolean, ): string { + const functionsToExport: { wrap: string[]; reexport: string[] } = { + wrap: [], + reexport: [], + }; + // `exportedBindings` can look like this: `{ '.': [ 'handler' ] }` or `{ '.': [], './firebase-gen-1.mjs': [ 'server' ] }` // The key `.` refers to exports within the current file, while other keys show from where exports were imported first. - const functionsToExport = flatten(Object.values(exportedBindings || {})).reduce( - (functions, currFunctionName) => { - if (entrypointWrappedFunctions.includes(currFunctionName)) { - functions.wrap.push(currFunctionName); + Object.values(exportedBindings || {}).forEach(functions => + functions.forEach(fn => { + if (entrypointWrappedFunctions.includes(fn)) { + functionsToExport.wrap.push(fn); } else { - functions.reexport.push(currFunctionName); + functionsToExport.reexport.push(fn); } - return functions; - }, - { wrap: [], reexport: [] } as { wrap: string[]; reexport: string[] }, + }), ); if (debug && functionsToExport.wrap.length === 0) { diff --git a/packages/utils/src/index.ts b/packages/utils/src/index.ts index bf0963131200..28629a1b240e 100644 --- a/packages/utils/src/index.ts +++ b/packages/utils/src/index.ts @@ -610,6 +610,7 @@ export const isNodeEnv = isNodeEnv_imported; export const loadModule = loadModule_imported; /** @deprecated Import from `@sentry/core` instead. */ +// eslint-disable-next-line deprecation/deprecation export const flatten = flatten_imported; /** @deprecated Import from `@sentry/core` instead. */ From d9909e0777377444a4209cdc895441556249192b Mon Sep 17 00:00:00 2001 From: Andrei <168741329+andreiborza@users.noreply.github.com> Date: Mon, 25 Nov 2024 14:43:20 +0100 Subject: [PATCH 19/34] chore(docs): Add `flatten` deprecation to v9 migration guide (#14460) --- docs/migration/draft-v9-migration-guide.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/migration/draft-v9-migration-guide.md b/docs/migration/draft-v9-migration-guide.md index dba02dcdd509..c543009d4995 100644 --- a/docs/migration/draft-v9-migration-guide.md +++ b/docs/migration/draft-v9-migration-guide.md @@ -17,6 +17,7 @@ - Deprecated `memoBuilder`. No replacements. - Deprecated `BAGGAGE_HEADER_NAME`. No replacements. - Deprecated `makeFifoCache`. No replacements. +- Deprecated `flatten`. No replacements. ## `@sentry/core` From 1a31ec46be120e61410742a00c17bda6a3c66444 Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Mon, 25 Nov 2024 15:13:07 +0100 Subject: [PATCH 20/34] ref(browser): Avoid optional chaining (#14457) We apparently disabled the no-optional-chaining rule in browser-utils, which lead to us including this polyfill. Properly enforcing this rule should (hopefully?) reduce bundle size a bit. --- packages/browser-utils/.eslintrc.js | 4 +-- packages/browser-utils/src/metrics/cls.ts | 30 +++++++++++-------- packages/browser-utils/src/metrics/inp.ts | 12 ++++---- .../src/metrics/web-vitals/getINP.ts | 2 -- .../browser-utils/test/utils/TestClient.ts | 2 -- 5 files changed, 26 insertions(+), 24 deletions(-) diff --git a/packages/browser-utils/.eslintrc.js b/packages/browser-utils/.eslintrc.js index 9d8a86b13b96..607e5d1b7d43 100644 --- a/packages/browser-utils/.eslintrc.js +++ b/packages/browser-utils/.eslintrc.js @@ -6,9 +6,7 @@ module.exports = { overrides: [ { files: ['src/**'], - rules: { - '@sentry-internal/sdk/no-optional-chaining': 'off', - }, + rules: {}, }, { files: ['src/metrics/**'], diff --git a/packages/browser-utils/src/metrics/cls.ts b/packages/browser-utils/src/metrics/cls.ts index 1a7be525c3aa..05d2f934eb8c 100644 --- a/packages/browser-utils/src/metrics/cls.ts +++ b/packages/browser-utils/src/metrics/cls.ts @@ -67,7 +67,11 @@ export function trackClsAsStandaloneSpan(): void { setTimeout(() => { const client = getClient(); - const unsubscribeStartNavigation = client?.on('startNavigationSpan', () => { + if (!client) { + return; + } + + const unsubscribeStartNavigation = client.on('startNavigationSpan', () => { _collectClsOnce(); unsubscribeStartNavigation && unsubscribeStartNavigation(); }); @@ -84,15 +88,15 @@ export function trackClsAsStandaloneSpan(): void { function sendStandaloneClsSpan(clsValue: number, entry: LayoutShift | undefined, pageloadSpanId: string) { DEBUG_BUILD && logger.log(`Sending CLS span (${clsValue})`); - const startTime = msToSec((browserPerformanceTimeOrigin || 0) + (entry?.startTime || 0)); + const startTime = msToSec((browserPerformanceTimeOrigin || 0) + ((entry && entry.startTime) || 0)); const routeName = getCurrentScope().getScopeData().transactionName; - const name = entry ? htmlTreeAsString(entry.sources[0]?.node) : 'Layout shift'; + const name = entry ? htmlTreeAsString(entry.sources[0] && entry.sources[0].node) : 'Layout shift'; const attributes: SpanAttributes = dropUndefinedKeys({ [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.browser.cls', [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'ui.webvital.cls', - [SEMANTIC_ATTRIBUTE_EXCLUSIVE_TIME]: entry?.duration || 0, + [SEMANTIC_ATTRIBUTE_EXCLUSIVE_TIME]: (entry && entry.duration) || 0, // attach the pageload span id to the CLS span so that we can link them in the UI 'sentry.pageload.span_id': pageloadSpanId, }); @@ -104,19 +108,21 @@ function sendStandaloneClsSpan(clsValue: number, entry: LayoutShift | undefined, startTime, }); - span?.addEvent('cls', { - [SEMANTIC_ATTRIBUTE_SENTRY_MEASUREMENT_UNIT]: '', - [SEMANTIC_ATTRIBUTE_SENTRY_MEASUREMENT_VALUE]: clsValue, - }); + if (span) { + span.addEvent('cls', { + [SEMANTIC_ATTRIBUTE_SENTRY_MEASUREMENT_UNIT]: '', + [SEMANTIC_ATTRIBUTE_SENTRY_MEASUREMENT_VALUE]: clsValue, + }); - // LayoutShift performance entries always have a duration of 0, so we don't need to add `entry.duration` here - // see: https://developer.mozilla.org/en-US/docs/Web/API/PerformanceEntry/duration - span?.end(startTime); + // LayoutShift performance entries always have a duration of 0, so we don't need to add `entry.duration` here + // see: https://developer.mozilla.org/en-US/docs/Web/API/PerformanceEntry/duration + span.end(startTime); + } } function supportsLayoutShift(): boolean { try { - return PerformanceObserver.supportedEntryTypes?.includes('layout-shift'); + return PerformanceObserver.supportedEntryTypes.includes('layout-shift'); } catch { return false; } diff --git a/packages/browser-utils/src/metrics/inp.ts b/packages/browser-utils/src/metrics/inp.ts index c722e6425c3a..4e90c89619d9 100644 --- a/packages/browser-utils/src/metrics/inp.ts +++ b/packages/browser-utils/src/metrics/inp.ts @@ -112,12 +112,14 @@ function _trackINP(): () => void { startTime, }); - span?.addEvent('inp', { - [SEMANTIC_ATTRIBUTE_SENTRY_MEASUREMENT_UNIT]: 'millisecond', - [SEMANTIC_ATTRIBUTE_SENTRY_MEASUREMENT_VALUE]: metric.value, - }); + if (span) { + span.addEvent('inp', { + [SEMANTIC_ATTRIBUTE_SENTRY_MEASUREMENT_UNIT]: 'millisecond', + [SEMANTIC_ATTRIBUTE_SENTRY_MEASUREMENT_VALUE]: metric.value, + }); - span?.end(startTime + duration); + span.end(startTime + duration); + } }); } diff --git a/packages/browser-utils/src/metrics/web-vitals/getINP.ts b/packages/browser-utils/src/metrics/web-vitals/getINP.ts index 96558f2cf109..e66f17eed2a1 100644 --- a/packages/browser-utils/src/metrics/web-vitals/getINP.ts +++ b/packages/browser-utils/src/metrics/web-vitals/getINP.ts @@ -66,7 +66,6 @@ const processEntry = (entry: PerformanceEventTiming) => { // The least-long of the 10 longest interactions. const minLongestInteraction = longestInteractionList[longestInteractionList.length - 1]; - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const existingInteraction = longestInteractionMap[entry.interactionId!]; // Only process the entry if it's possibly one of the ten longest, @@ -82,7 +81,6 @@ const processEntry = (entry: PerformanceEventTiming) => { existingInteraction.latency = Math.max(existingInteraction.latency, entry.duration); } else { const interaction = { - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion id: entry.interactionId!, latency: entry.duration, entries: [entry], diff --git a/packages/browser-utils/test/utils/TestClient.ts b/packages/browser-utils/test/utils/TestClient.ts index 3bd5fff8cf76..b3e3c29cb8cd 100644 --- a/packages/browser-utils/test/utils/TestClient.ts +++ b/packages/browser-utils/test/utils/TestClient.ts @@ -20,10 +20,8 @@ export class TestClient extends BaseClient { exception: { values: [ { - /* eslint-disable @typescript-eslint/no-unsafe-member-access */ type: exception.name, value: exception.message, - /* eslint-enable @typescript-eslint/no-unsafe-member-access */ }, ], }, From b271bc831983ccfc0025e618847fc92ec54a6024 Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Mon, 25 Nov 2024 17:26:50 +0100 Subject: [PATCH 21/34] feat(core): Update & deprecate `undefined` option handling (#14450) This PR updates & streamlines our handling of certain `undefined` options: First, we do not want to differentiate anymore between options being unset and set to `undefined` - the resulting behavior should be the same. This especially applies to the tracing options `tracesSampleRate`, `tracesSampler` and `enableTracing` - if any of those is set to `undefined`, `hasEnabledTracing()` will be true and certain things will happen. In v9, we want to change this so that `undefined` will also result in `hasEnabledTracing() === false`. We'll warn if we encounter such a scenario. Another related thing this PR does is streamline how we handle falsy values for `environment`, `release` and `dist` on an event. Today, we go out of our way to check if the properties are set and only update them accordingly. However, fasly values do not make sense for these fields anyhow, so we can streamline this a bit and simply check for truthiness to determine if we want to use event, option, or default values. Closes https://github.com/getsentry/sentry-javascript/issues/14261 --- .size-limit.js | 8 +- docs/migration/draft-v9-migration-guide.md | 16 +++ packages/core/src/baseclient.ts | 14 +- packages/core/src/utils/prepareEvent.ts | 21 +-- .../lib/{base.test.ts => baseclient.test.ts} | 62 +++++++- packages/core/test/lib/prepareEvent.test.ts | 134 ++++++++++++++++++ .../core/test/lib/tracing/sentrySpan.test.ts | 8 ++ 7 files changed, 247 insertions(+), 16 deletions(-) rename packages/core/test/lib/{base.test.ts => baseclient.test.ts} (96%) diff --git a/.size-limit.js b/.size-limit.js index e97e15f6cd4e..355e484c5bd1 100644 --- a/.size-limit.js +++ b/.size-limit.js @@ -40,7 +40,7 @@ module.exports = [ path: 'packages/browser/build/npm/esm/index.js', import: createImport('init', 'browserTracingIntegration'), gzip: true, - limit: '36.5 KB', + limit: '37.5 KB', }, { name: '@sentry/browser (incl. Tracing, Replay)', @@ -124,7 +124,7 @@ module.exports = [ import: createImport('init', 'ErrorBoundary', 'reactRouterV6BrowserTracingIntegration'), ignore: ['react/jsx-runtime'], gzip: true, - limit: '39.5 KB', + limit: '40.5 KB', }, // Vue SDK (ESM) { @@ -139,7 +139,7 @@ module.exports = [ path: 'packages/vue/build/esm/index.js', import: createImport('init', 'browserTracingIntegration'), gzip: true, - limit: '38.5 KB', + limit: '39.5 KB', }, // Svelte SDK (ESM) { @@ -219,7 +219,7 @@ module.exports = [ import: createImport('init'), ignore: ['$app/stores'], gzip: true, - limit: '37 KB', + limit: '38 KB', }, // Node SDK (ESM) { diff --git a/docs/migration/draft-v9-migration-guide.md b/docs/migration/draft-v9-migration-guide.md index c543009d4995..763d0765cff5 100644 --- a/docs/migration/draft-v9-migration-guide.md +++ b/docs/migration/draft-v9-migration-guide.md @@ -2,6 +2,22 @@ # Deprecations +## General + +- **Passing `undefined` to `tracesSampleRate` / `tracesSampler` / `enableTracing` will be handled differently in v9** + +In v8, a setup like the following: + +```ts +Sentry.init({ + tracesSampleRate: undefined, +}); +``` + +Will result in tracing being _enabled_, although no spans will be generated. +In v9, we will streamline this behavior so that passing `undefined` will result in tracing being disabled, the same as not passing the option at all. +If you are relying on `undefined` being passed in and having tracing enabled because of this, you should update your config to set e.g. `tracesSampleRate: 0` instead, which will also enable tracing in v9. + ## `@sentry/utils` - **The `@sentry/utils` package has been deprecated. Import everything from `@sentry/core` instead.** diff --git a/packages/core/src/baseclient.ts b/packages/core/src/baseclient.ts index 2387523d46ee..d86f170ca25d 100644 --- a/packages/core/src/baseclient.ts +++ b/packages/core/src/baseclient.ts @@ -46,7 +46,7 @@ import { dsnToString, makeDsn } from './utils-hoist/dsn'; import { addItemToEnvelope, createAttachmentEnvelopeItem } from './utils-hoist/envelope'; import { SentryError } from './utils-hoist/error'; import { isParameterizedString, isPlainObject, isPrimitive, isThenable } from './utils-hoist/is'; -import { logger } from './utils-hoist/logger'; +import { consoleSandbox, logger } from './utils-hoist/logger'; import { checkOrSetAlreadyCaught, uuid4 } from './utils-hoist/misc'; import { dropUndefinedKeys } from './utils-hoist/object'; import { SyncPromise, rejectedSyncPromise, resolvedSyncPromise } from './utils-hoist/syncpromise'; @@ -142,6 +142,18 @@ export abstract class BaseClient implements Client { url, }); } + + // TODO(v9): Remove this deprecation warning + const tracingOptions = ['enableTracing', 'tracesSampleRate', 'tracesSampler'] as const; + const undefinedOption = tracingOptions.find(option => option in options && options[option] == undefined); + if (undefinedOption) { + consoleSandbox(() => { + // eslint-disable-next-line no-console + console.warn( + `[Sentry] Deprecation warning: \`${undefinedOption}\` is set to undefined, which leads to tracing being enabled. In v9, a value of \`undefined\` will result in tracing being disabled.`, + ); + }); + } } /** diff --git a/packages/core/src/utils/prepareEvent.ts b/packages/core/src/utils/prepareEvent.ts index bf23a0e3ee79..98f64adbdd62 100644 --- a/packages/core/src/utils/prepareEvent.ts +++ b/packages/core/src/utils/prepareEvent.ts @@ -129,23 +129,26 @@ export function prepareEvent( } /** - * Enhances event using the client configuration. - * It takes care of all "static" values like environment, release and `dist`, - * as well as truncating overly long values. + * Enhances event using the client configuration. + * It takes care of all "static" values like environment, release and `dist`, + * as well as truncating overly long values. + * + * Only exported for tests. + * * @param event event instance to be enhanced */ -function applyClientOptions(event: Event, options: ClientOptions): void { +export function applyClientOptions(event: Event, options: ClientOptions): void { const { environment, release, dist, maxValueLength = 250 } = options; - if (!('environment' in event)) { - event.environment = 'environment' in options ? environment : DEFAULT_ENVIRONMENT; - } + // empty strings do not make sense for environment, release, and dist + // so we handle them the same as if they were not provided + event.environment = event.environment || environment || DEFAULT_ENVIRONMENT; - if (event.release === undefined && release !== undefined) { + if (!event.release && release) { event.release = release; } - if (event.dist === undefined && dist !== undefined) { + if (!event.dist && dist) { event.dist = dist; } diff --git a/packages/core/test/lib/base.test.ts b/packages/core/test/lib/baseclient.test.ts similarity index 96% rename from packages/core/test/lib/base.test.ts rename to packages/core/test/lib/baseclient.test.ts index 296a496d2d40..4e2bcdb334ef 100644 --- a/packages/core/test/lib/base.test.ts +++ b/packages/core/test/lib/baseclient.test.ts @@ -77,6 +77,56 @@ describe('BaseClient', () => { }); }); + describe('constructor() / warnings', () => { + test('does not warn for defaults', () => { + const consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation(() => undefined); + + const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN }); + new TestClient(options); + + expect(consoleWarnSpy).toHaveBeenCalledTimes(0); + consoleWarnSpy.mockRestore(); + }); + + describe.each(['tracesSampleRate', 'tracesSampler', 'enableTracing'])('%s', key => { + it('warns when set to undefined', () => { + const consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation(() => undefined); + + const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN, [key]: undefined }); + new TestClient(options); + + expect(consoleWarnSpy).toHaveBeenCalledTimes(1); + expect(consoleWarnSpy).toBeCalledWith( + `[Sentry] Deprecation warning: \`${key}\` is set to undefined, which leads to tracing being enabled. In v9, a value of \`undefined\` will result in tracing being disabled.`, + ); + consoleWarnSpy.mockRestore(); + }); + + it('warns when set to null', () => { + const consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation(() => undefined); + + const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN, [key]: null }); + new TestClient(options); + + expect(consoleWarnSpy).toHaveBeenCalledTimes(1); + expect(consoleWarnSpy).toBeCalledWith( + `[Sentry] Deprecation warning: \`${key}\` is set to undefined, which leads to tracing being enabled. In v9, a value of \`undefined\` will result in tracing being disabled.`, + ); + consoleWarnSpy.mockRestore(); + }); + + it('does not warn when set to 0', () => { + const consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation(() => undefined); + + const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN, [key]: 0 }); + new TestClient(options); + + expect(consoleWarnSpy).toHaveBeenCalledTimes(0); + consoleWarnSpy.mockRestore(); + }); + }); + }); + describe('getOptions()', () => { test('returns the options', () => { expect.assertions(1); @@ -552,7 +602,7 @@ describe('BaseClient', () => { ); }); - test('allows for environment to be explicitly set to falsy value', () => { + test('uses default environment when set to falsy value', () => { expect.assertions(1); const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN, environment: undefined }); @@ -563,7 +613,7 @@ describe('BaseClient', () => { expect(TestClient.instance!.event!).toEqual( expect.objectContaining({ - environment: undefined, + environment: 'production', event_id: '42', message: 'message', timestamp: 2020, @@ -1122,6 +1172,8 @@ describe('BaseClient', () => { }); test('calls `beforeSendSpan` and discards the span', () => { + const consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation(() => undefined); + const beforeSendSpan = jest.fn(() => null); const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN, beforeSendSpan }); const client = new TestClient(options); @@ -1150,6 +1202,12 @@ describe('BaseClient', () => { const capturedEvent = TestClient.instance!.event!; expect(capturedEvent.spans).toHaveLength(0); expect(client['_outcomes']).toEqual({ 'before_send:span': 2 }); + + expect(consoleWarnSpy).toHaveBeenCalledTimes(1); + expect(consoleWarnSpy).toBeCalledWith( + '[Sentry] Deprecation warning: Returning null from `beforeSendSpan` will be disallowed from SDK version 9.0.0 onwards. The callback will only support mutating spans. To drop certain spans, configure the respective integrations directly.', + ); + consoleWarnSpy.mockRestore(); }); test('calls `beforeSend` and logs info about invalid return value', () => { diff --git a/packages/core/test/lib/prepareEvent.test.ts b/packages/core/test/lib/prepareEvent.test.ts index a905d948475a..0322afb7e7a8 100644 --- a/packages/core/test/lib/prepareEvent.test.ts +++ b/packages/core/test/lib/prepareEvent.test.ts @@ -12,6 +12,7 @@ import { GLOBAL_OBJ, createStackParser, getGlobalScope, getIsolationScope } from import { Scope } from '../../src/scope'; import { + applyClientOptions, applyDebugIds, applyDebugMeta, parseEventHintOrCaptureContext, @@ -518,3 +519,136 @@ describe('prepareEvent', () => { }); }); }); + +describe('applyClientOptions', () => { + it('works with defaults', () => { + const event: Event = {}; + const options = {} as ClientOptions; + + applyClientOptions(event, options); + + expect(event).toEqual({ + environment: 'production', + }); + + // These should not be set at all on the event + expect('release' in event).toBe(false); + expect('dist' in event).toBe(false); + }); + + it('works with event data and no options', () => { + const event: Event = { + environment: 'blub', + release: 'blab', + dist: 'blib', + }; + const options = {} as ClientOptions; + + applyClientOptions(event, options); + + expect(event).toEqual({ + environment: 'blub', + release: 'blab', + dist: 'blib', + }); + }); + + it('event data has precedence over options', () => { + const event: Event = { + environment: 'blub', + release: 'blab', + dist: 'blib', + }; + const options = { + environment: 'blub2', + release: 'blab2', + dist: 'blib2', + } as ClientOptions; + + applyClientOptions(event, options); + + expect(event).toEqual({ + environment: 'blub', + release: 'blab', + dist: 'blib', + }); + }); + + it('option data is used if no event data exists', () => { + const event: Event = {}; + const options = { + environment: 'blub2', + release: 'blab2', + dist: 'blib2', + } as ClientOptions; + + applyClientOptions(event, options); + + expect(event).toEqual({ + environment: 'blub2', + release: 'blab2', + dist: 'blib2', + }); + }); + + it('option data is ignored if empty string', () => { + const event: Event = {}; + const options = { + environment: '', + release: '', + dist: '', + } as ClientOptions; + + applyClientOptions(event, options); + + expect(event).toEqual({ + environment: 'production', + }); + + // These should not be set at all on the event + expect('release' in event).toBe(false); + expect('dist' in event).toBe(false); + }); + + it('option data is used if event data is undefined', () => { + const event: Event = { + environment: undefined, + release: undefined, + dist: undefined, + }; + const options = { + environment: 'blub2', + release: 'blab2', + dist: 'blib2', + } as ClientOptions; + + applyClientOptions(event, options); + + expect(event).toEqual({ + environment: 'blub2', + release: 'blab2', + dist: 'blib2', + }); + }); + + it('option data is used if event data is empty string', () => { + const event: Event = { + environment: '', + release: '', + dist: '', + }; + const options = { + environment: 'blub2', + release: 'blab2', + dist: 'blib2', + } as ClientOptions; + + applyClientOptions(event, options); + + expect(event).toEqual({ + environment: 'blub2', + release: 'blab2', + dist: 'blib2', + }); + }); +}); diff --git a/packages/core/test/lib/tracing/sentrySpan.test.ts b/packages/core/test/lib/tracing/sentrySpan.test.ts index 447e42328a2e..9a1c9f69255c 100644 --- a/packages/core/test/lib/tracing/sentrySpan.test.ts +++ b/packages/core/test/lib/tracing/sentrySpan.test.ts @@ -161,6 +161,8 @@ describe('SentrySpan', () => { }); test('does not send the span if `beforeSendSpan` drops the span', () => { + const consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation(() => undefined); + const beforeSendSpan = jest.fn(() => null); const client = new TestClient( getDefaultTestClientOptions({ @@ -185,6 +187,12 @@ describe('SentrySpan', () => { expect(mockSend).not.toHaveBeenCalled(); expect(recordDroppedEventSpy).toHaveBeenCalledWith('before_send', 'span'); + + expect(consoleWarnSpy).toHaveBeenCalledTimes(1); + expect(consoleWarnSpy).toBeCalledWith( + '[Sentry] Deprecation warning: Returning null from `beforeSendSpan` will be disallowed from SDK version 9.0.0 onwards. The callback will only support mutating spans. To drop certain spans, configure the respective integrations directly.', + ); + consoleWarnSpy.mockRestore(); }); }); From fc1d986aff3d5376b11ef58fc4ab256b9f9b4900 Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Tue, 26 Nov 2024 09:05:23 +0100 Subject: [PATCH 22/34] ref: Remove & streamline some debug logs (#14456) Streamline some debug logs, esp. also removing some debug logs around browser metrics, in favor of just having a generic log when we add measurements. --- .../src/metrics/browserMetrics.ts | 14 +------------ .../browser/src/integrations/httpclient.ts | 12 +++++------ .../src/tracing/browserTracingIntegration.ts | 21 +++++++++---------- packages/core/src/tracing/measurement.ts | 3 +++ 4 files changed, 20 insertions(+), 30 deletions(-) diff --git a/packages/browser-utils/src/metrics/browserMetrics.ts b/packages/browser-utils/src/metrics/browserMetrics.ts index d64bba34509a..4352fe3d5f27 100644 --- a/packages/browser-utils/src/metrics/browserMetrics.ts +++ b/packages/browser-utils/src/metrics/browserMetrics.ts @@ -1,11 +1,10 @@ /* eslint-disable max-lines */ import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, getActiveSpan } from '@sentry/core'; import { setMeasurement } from '@sentry/core'; -import { browserPerformanceTimeOrigin, getComponentName, htmlTreeAsString, logger, parseUrl } from '@sentry/core'; +import { browserPerformanceTimeOrigin, getComponentName, htmlTreeAsString, parseUrl } from '@sentry/core'; import type { Measurements, Span, SpanAttributes, StartSpanOptions } from '@sentry/types'; import { spanToJSON } from '@sentry/core'; -import { DEBUG_BUILD } from '../debug-build'; import { WINDOW } from '../types'; import { trackClsAsStandaloneSpan } from './cls'; import { @@ -241,7 +240,6 @@ function _trackCLS(): () => void { if (!entry) { return; } - DEBUG_BUILD && logger.log(`[Measurements] Adding CLS ${metric.value}`); _measurements['cls'] = { value: metric.value, unit: '' }; _clsEntry = entry; }, true); @@ -255,7 +253,6 @@ function _trackLCP(): () => void { return; } - DEBUG_BUILD && logger.log('[Measurements] Adding LCP'); _measurements['lcp'] = { value: metric.value, unit: 'millisecond' }; _lcpEntry = entry as LargestContentfulPaint; }, true); @@ -271,7 +268,6 @@ function _trackFID(): () => void { const timeOrigin = msToSec(browserPerformanceTimeOrigin as number); const startTime = msToSec(entry.startTime); - DEBUG_BUILD && logger.log('[Measurements] Adding FID'); _measurements['fid'] = { value: metric.value, unit: 'millisecond' }; _measurements['mark.fid'] = { value: timeOrigin + startTime, unit: 'second' }; }); @@ -284,7 +280,6 @@ function _trackTtfb(): () => void { return; } - DEBUG_BUILD && logger.log('[Measurements] Adding TTFB'); _measurements['ttfb'] = { value: metric.value, unit: 'millisecond' }; }); } @@ -305,7 +300,6 @@ export function addPerformanceEntries(span: Span, options: AddPerformanceEntries return; } - DEBUG_BUILD && logger.log('[Tracing] Adding & adjusting spans using Performance API'); const timeOrigin = msToSec(browserPerformanceTimeOrigin); const performanceEntries = performance.getEntries(); @@ -343,11 +337,9 @@ export function addPerformanceEntries(span: Span, options: AddPerformanceEntries const shouldRecord = entry.startTime < firstHidden.firstHiddenTime; if (entry.name === 'first-paint' && shouldRecord) { - DEBUG_BUILD && logger.log('[Measurements] Adding FP'); _measurements['fp'] = { value: entry.startTime, unit: 'millisecond' }; } if (entry.name === 'first-contentful-paint' && shouldRecord) { - DEBUG_BUILD && logger.log('[Measurements] Adding FCP'); _measurements['fcp'] = { value: entry.startTime, unit: 'millisecond' }; } break; @@ -618,8 +610,6 @@ function _trackNavigator(span: Span): void { /** Add LCP / CLS data to span to allow debugging */ function _setWebVitalAttributes(span: Span): void { if (_lcpEntry) { - DEBUG_BUILD && logger.log('[Measurements] Adding LCP Data'); - // Capture Properties of the LCP element that contributes to the LCP. if (_lcpEntry.element) { @@ -652,7 +642,6 @@ function _setWebVitalAttributes(span: Span): void { // See: https://developer.mozilla.org/en-US/docs/Web/API/LayoutShift if (_clsEntry && _clsEntry.sources) { - DEBUG_BUILD && logger.log('[Measurements] Adding CLS Data'); _clsEntry.sources.forEach((source, index) => span.setAttribute(`cls.source.${index + 1}`, htmlTreeAsString(source.node)), ); @@ -685,7 +674,6 @@ function _addTtfbRequestTimeToMeasurements(_measurements: Measurements): void { const { responseStart, requestStart } = navEntry; if (requestStart <= responseStart) { - DEBUG_BUILD && logger.log('[Measurements] Adding TTFB Request Time'); _measurements['ttfb.requestTime'] = { value: responseStart - requestStart, unit: 'millisecond', diff --git a/packages/browser/src/integrations/httpclient.ts b/packages/browser/src/integrations/httpclient.ts index 439941c97faf..88c1418ab4af 100644 --- a/packages/browser/src/integrations/httpclient.ts +++ b/packages/browser/src/integrations/httpclient.ts @@ -108,8 +108,8 @@ function _parseCookieHeaders( if (cookieString) { cookies = _parseCookieString(cookieString); } - } catch (e) { - DEBUG_BUILD && logger.log(`Could not extract cookies from header ${cookieHeader}`); + } catch { + // ignore it if parsing fails } return [headers, cookies]; @@ -138,14 +138,14 @@ function _xhrResponseHandler( if (cookieString) { responseCookies = _parseCookieString(cookieString); } - } catch (e) { - DEBUG_BUILD && logger.log('Could not extract cookies from response headers'); + } catch { + // ignore it if parsing fails } try { responseHeaders = _getXHRResponseHeaders(xhr); - } catch (e) { - DEBUG_BUILD && logger.log('Could not extract headers from response'); + } catch { + // ignore it if parsing fails } requestHeaders = headers; diff --git a/packages/browser/src/tracing/browserTracingIntegration.ts b/packages/browser/src/tracing/browserTracingIntegration.ts index a59706edb8fb..37d3b532129c 100644 --- a/packages/browser/src/tracing/browserTracingIntegration.ts +++ b/packages/browser/src/tracing/browserTracingIntegration.ts @@ -299,16 +299,20 @@ export const browserTracingIntegration = ((_options: Partial { if (getClient() !== client) { return; } - if (activeSpan && !spanToJSON(activeSpan).timestamp) { - DEBUG_BUILD && logger.log(`[Tracing] Finishing current root span with op: ${spanToJSON(activeSpan).op}`); - // If there's an open transaction on the scope, we need to finish it before creating an new one. - activeSpan.end(); - } + maybeEndActiveSpan(); activeSpan = _createRouteSpan(client, { op: 'navigation', @@ -320,12 +324,7 @@ export const browserTracingIntegration = ((_options: Partial Date: Tue, 26 Nov 2024 10:37:01 +0100 Subject: [PATCH 23/34] feat(core/utils): Deprecate `getNumberOfUrlSegments` (#14458) --- docs/migration/draft-v9-migration-guide.md | 1 + packages/core/src/utils-hoist/index.ts | 1 + packages/core/src/utils-hoist/url.ts | 3 +++ packages/core/test/utils-hoist/url.test.ts | 1 + packages/react/src/reactrouterv6.tsx | 2 ++ packages/utils/src/index.ts | 1 + 6 files changed, 9 insertions(+) diff --git a/docs/migration/draft-v9-migration-guide.md b/docs/migration/draft-v9-migration-guide.md index 763d0765cff5..6fc1f5e60f0e 100644 --- a/docs/migration/draft-v9-migration-guide.md +++ b/docs/migration/draft-v9-migration-guide.md @@ -31,6 +31,7 @@ If you are relying on `undefined` being passed in and having tracing enabled bec - Deprecated `extractRequestData`. Instead manually extract relevant data off request. - Deprecated `arrayify`. No replacements. - Deprecated `memoBuilder`. No replacements. +- Deprecated `getNumberOfUrlSegments`. No replacements. - Deprecated `BAGGAGE_HEADER_NAME`. No replacements. - Deprecated `makeFifoCache`. No replacements. - Deprecated `flatten`. No replacements. diff --git a/packages/core/src/utils-hoist/index.ts b/packages/core/src/utils-hoist/index.ts index c04f314c40f2..ce6be00849df 100644 --- a/packages/core/src/utils-hoist/index.ts +++ b/packages/core/src/utils-hoist/index.ts @@ -159,6 +159,7 @@ export { parseBaggageHeader, } from './baggage'; +// eslint-disable-next-line deprecation/deprecation export { getNumberOfUrlSegments, getSanitizedUrlString, parseUrl, stripUrlQueryAndFragment } from './url'; // eslint-disable-next-line deprecation/deprecation export { makeFifoCache } from './cache'; diff --git a/packages/core/src/utils-hoist/url.ts b/packages/core/src/utils-hoist/url.ts index e324f41f82a3..44dc669da93a 100644 --- a/packages/core/src/utils-hoist/url.ts +++ b/packages/core/src/utils-hoist/url.ts @@ -50,7 +50,10 @@ export function stripUrlQueryAndFragment(urlPath: string): string { /** * Returns number of URL segments of a passed string URL. + * + * @deprecated This function will be removed in the next major version. */ +// TODO(v9): Hoist this function into the places where we use it. (as it stands only react router v6 instrumentation) export function getNumberOfUrlSegments(url: string): number { // split at '/' or at '\/' to split regex urls correctly return url.split(/\\?\//).filter(s => s.length > 0 && s !== ',').length; diff --git a/packages/core/test/utils-hoist/url.test.ts b/packages/core/test/utils-hoist/url.test.ts index fd8b861516ab..cd793e1d3d0c 100644 --- a/packages/core/test/utils-hoist/url.test.ts +++ b/packages/core/test/utils-hoist/url.test.ts @@ -33,6 +33,7 @@ describe('getNumberOfUrlSegments', () => { ['multi param parameterized path', '/stores/:storeId/products/:productId', 4], ['regex path', String(/\/api\/post[0-9]/), 2], ])('%s', (_: string, input, output) => { + // eslint-disable-next-line deprecation/deprecation expect(getNumberOfUrlSegments(input)).toEqual(output); }); }); diff --git a/packages/react/src/reactrouterv6.tsx b/packages/react/src/reactrouterv6.tsx index b209391f1dda..341470b0002a 100644 --- a/packages/react/src/reactrouterv6.tsx +++ b/packages/react/src/reactrouterv6.tsx @@ -197,6 +197,8 @@ function getNormalizedName( // If the route defined on the element is something like // Product} /> // We should check against the branch.pathname for the number of / separators + // TODO(v9): Put the implementation of `getNumberOfUrlSegments` in this file + // eslint-disable-next-line deprecation/deprecation getNumberOfUrlSegments(pathBuilder) !== getNumberOfUrlSegments(branch.pathname) && // We should not count wildcard operators in the url segments calculation pathBuilder.slice(-2) !== '/*' diff --git a/packages/utils/src/index.ts b/packages/utils/src/index.ts index 28629a1b240e..0a3a5a88f23f 100644 --- a/packages/utils/src/index.ts +++ b/packages/utils/src/index.ts @@ -666,6 +666,7 @@ export const _optionalChainDelete = _optionalChainDelete_imported; export const BAGGAGE_HEADER_NAME = BAGGAGE_HEADER_NAME_imported; /** @deprecated Import from `@sentry/core` instead. */ +// eslint-disable-next-line deprecation/deprecation export const getNumberOfUrlSegments = getNumberOfUrlSegments_imported; /** @deprecated Import from `@sentry/core` instead. */ From cf368b14b0e1e49ad32c316859862295bbd980d5 Mon Sep 17 00:00:00 2001 From: Luca Forstner Date: Tue, 26 Nov 2024 11:06:44 +0100 Subject: [PATCH 24/34] meta: Backfill v9 migration guide items (#14459) Backfills items to the draft migration guide for v9 that we missed - https://github.com/getsentry/sentry-javascript/pull/14433 - https://github.com/getsentry/sentry-javascript/pull/14454 --- docs/migration/draft-v9-migration-guide.md | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/docs/migration/draft-v9-migration-guide.md b/docs/migration/draft-v9-migration-guide.md index 6fc1f5e60f0e..7965633f4512 100644 --- a/docs/migration/draft-v9-migration-guide.md +++ b/docs/migration/draft-v9-migration-guide.md @@ -4,19 +4,20 @@ ## General +- **Returning `null` from `beforeSendSpan` span is deprecated.** - **Passing `undefined` to `tracesSampleRate` / `tracesSampler` / `enableTracing` will be handled differently in v9** -In v8, a setup like the following: + In v8, a setup like the following: -```ts -Sentry.init({ - tracesSampleRate: undefined, -}); -``` + ```ts + Sentry.init({ + tracesSampleRate: undefined, + }); + ``` -Will result in tracing being _enabled_, although no spans will be generated. -In v9, we will streamline this behavior so that passing `undefined` will result in tracing being disabled, the same as not passing the option at all. -If you are relying on `undefined` being passed in and having tracing enabled because of this, you should update your config to set e.g. `tracesSampleRate: 0` instead, which will also enable tracing in v9. + Will result in tracing being _enabled_, although no spans will be generated. + In v9, we will streamline this behavior so that passing `undefined` will result in tracing being disabled, the same as not passing the option at all. + If you are relying on `undefined` being passed in and having tracing enabled because of this, you should update your config to set e.g. `tracesSampleRate: 0` instead, which will also enable tracing in v9. ## `@sentry/utils` From b59ce07a744d62536e43b6b2284571f99929b273 Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Tue, 26 Nov 2024 11:27:39 +0100 Subject: [PATCH 25/34] feat: Streamline `sentry-trace`, `baggage` and DSC handling (#14364) As a first step for https://github.com/getsentry/sentry-javascript/issues/12385, I looked though our current implementations, and tried to streamline this. We have a bunch of somewhat duplicate code there that handles sentry-trace & baggage headers _mostly_ the same but not _fully_ the same, as well as relatedly the DSC. To streamline this, I added some new methods in core: * `getTraceData` already exists, but was extended to also allow to pass a specific `span` to it. I updated some code to use this method that hasn't used it before, and also added some more tests - also around the ACS and the otel-specific implementation for this. * For this and edge cases, there are new primitives `getDynamicSamplingContextFromScope` and `getTraceContextFromScope` which handle picking these things from given scope etc. While working on this, I also noticed that `captureCheckIn` in ServerRuntimeClient was actually not properly working in Node (OTEL), as it relied on the core way of scope-span coupling. So I also updated this to actually work as expected. --- .size-limit.js | 2 +- .../ContextLines/noAddedLines/test.ts | 1 + docs/migration/draft-v9-migration-guide.md | 1 + packages/browser/src/tracing/request.ts | 40 +-- packages/core/src/baseclient.ts | 35 +- packages/core/src/currentScopes.ts | 20 +- packages/core/src/fetch.ts | 125 +++---- packages/core/src/index.ts | 7 +- packages/core/src/server-runtime-client.ts | 29 +- .../src/tracing/dynamicSamplingContext.ts | 13 +- packages/core/src/tracing/index.ts | 1 + packages/core/src/utils-hoist/tracing.ts | 30 +- packages/core/src/utils/traceData.ts | 42 +-- .../core/test/lib/utils/traceData.test.ts | 326 ++++++++++++------ packages/node/src/sdk/client.ts | 18 +- packages/opentelemetry/src/index.ts | 9 +- packages/opentelemetry/src/propagator.ts | 40 +-- packages/opentelemetry/src/trace.ts | 24 +- .../opentelemetry/src/utils/getTraceData.ts | 32 +- .../test/utils/getTraceData.test.ts | 93 +++++ packages/remix/src/utils/instrumentServer.ts | 23 +- .../test/client/root-loader.test.ts | 32 +- 22 files changed, 588 insertions(+), 355 deletions(-) create mode 100644 packages/opentelemetry/test/utils/getTraceData.test.ts diff --git a/.size-limit.js b/.size-limit.js index 355e484c5bd1..dc2b7af8128f 100644 --- a/.size-limit.js +++ b/.size-limit.js @@ -132,7 +132,7 @@ module.exports = [ path: 'packages/vue/build/esm/index.js', import: createImport('init'), gzip: true, - limit: '28 KB', + limit: '29 KB', }, { name: '@sentry/vue (incl. Tracing)', diff --git a/dev-packages/browser-integration-tests/suites/integrations/ContextLines/noAddedLines/test.ts b/dev-packages/browser-integration-tests/suites/integrations/ContextLines/noAddedLines/test.ts index 8fa8ec16cddd..a3c8614bfb2b 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/ContextLines/noAddedLines/test.ts +++ b/dev-packages/browser-integration-tests/suites/integrations/ContextLines/noAddedLines/test.ts @@ -7,6 +7,7 @@ sentryTest('should not add source context lines to errors from script files', as const url = await getLocalTestUrl({ testDir: __dirname }); const eventReqPromise = waitForErrorRequestOnUrl(page, url); + await page.waitForFunction('window.Sentry'); const clickPromise = page.locator('#script-error-btn').click(); diff --git a/docs/migration/draft-v9-migration-guide.md b/docs/migration/draft-v9-migration-guide.md index 7965633f4512..967fa0d5c6bb 100644 --- a/docs/migration/draft-v9-migration-guide.md +++ b/docs/migration/draft-v9-migration-guide.md @@ -42,6 +42,7 @@ - Deprecated `transactionNamingScheme` option in `requestDataIntegration`. - Deprecated `debugIntegration`. To log outgoing events, use [Hook Options](https://docs.sentry.io/platforms/javascript/configuration/options/#hooks) (`beforeSend`, `beforeSendTransaction`, ...). - Deprecated `sessionTimingIntegration`. To capture session durations alongside events, use [Context](https://docs.sentry.io/platforms/javascript/enriching-events/context/) (`Sentry.setContext()`). +- Deprecated `addTracingHeadersToFetchRequest` method - this was only meant for internal use and is not needed anymore. ## `@sentry/nestjs` diff --git a/packages/browser/src/tracing/request.ts b/packages/browser/src/tracing/request.ts index 225903d32a4b..63b0c1f7049b 100644 --- a/packages/browser/src/tracing/request.ts +++ b/packages/browser/src/tracing/request.ts @@ -9,24 +9,17 @@ import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SentryNonRecordingSpan, getActiveSpan, - getClient, - getCurrentScope, - getDynamicSamplingContextFromClient, - getDynamicSamplingContextFromSpan, - getIsolationScope, + getTraceData, hasTracingEnabled, instrumentFetchRequest, setHttpStatus, spanToJSON, - spanToTraceHeader, startInactiveSpan, } from '@sentry/core'; import { addFetchEndInstrumentationHandler, addFetchInstrumentationHandler, browserPerformanceTimeOrigin, - dynamicSamplingContextToSentryBaggageHeader, - generateSentryTraceHeader, parseUrl, stringMatchesSomePattern, } from '@sentry/core'; @@ -76,7 +69,9 @@ export interface RequestInstrumentationOptions { * * Default: true */ - traceXHR: boolean /** + traceXHR: boolean; + + /** * Flag to disable tracking of long-lived streams, like server-sent events (SSE) via fetch. * Do not enable this in case you have live streams or very long running streams. * @@ -84,7 +79,7 @@ export interface RequestInstrumentationOptions { * (https://github.com/getsentry/sentry-javascript/issues/13950) * * Default: false - */; + */ trackFetchStreamPerformance: boolean; /** @@ -401,12 +396,9 @@ export function xhrCallback( xhr.__sentry_xhr_span_id__ = span.spanContext().spanId; spans[xhr.__sentry_xhr_span_id__] = span; - const client = getClient(); - - if (xhr.setRequestHeader && shouldAttachHeaders(sentryXhrData.url) && client) { + if (shouldAttachHeaders(sentryXhrData.url)) { addTracingHeadersToXhrRequest( xhr, - client, // If performance is disabled (TWP) or there's no active root span (pageload/navigation/interaction), // we do not want to use the span as base for the trace headers, // which means that the headers will be generated from the scope and the sampling decision is deferred @@ -417,22 +409,12 @@ export function xhrCallback( return span; } -function addTracingHeadersToXhrRequest(xhr: SentryWrappedXMLHttpRequest, client: Client, span?: Span): void { - const scope = getCurrentScope(); - const isolationScope = getIsolationScope(); - const { traceId, spanId, sampled, dsc } = { - ...isolationScope.getPropagationContext(), - ...scope.getPropagationContext(), - }; +function addTracingHeadersToXhrRequest(xhr: SentryWrappedXMLHttpRequest, span?: Span): void { + const { 'sentry-trace': sentryTrace, baggage } = getTraceData({ span }); - const sentryTraceHeader = - span && hasTracingEnabled() ? spanToTraceHeader(span) : generateSentryTraceHeader(traceId, spanId, sampled); - - const sentryBaggageHeader = dynamicSamplingContextToSentryBaggageHeader( - dsc || (span ? getDynamicSamplingContextFromSpan(span) : getDynamicSamplingContextFromClient(traceId, client)), - ); - - setHeaderOnXhr(xhr, sentryTraceHeader, sentryBaggageHeader); + if (sentryTrace) { + setHeaderOnXhr(xhr, sentryTrace, baggage); + } } function setHeaderOnXhr( diff --git a/packages/core/src/baseclient.ts b/packages/core/src/baseclient.ts index d86f170ca25d..23010505a7dc 100644 --- a/packages/core/src/baseclient.ts +++ b/packages/core/src/baseclient.ts @@ -32,7 +32,7 @@ import type { } from '@sentry/types'; import { getEnvelopeEndpointWithUrlEncodedAuth } from './api'; -import { getIsolationScope } from './currentScopes'; +import { getCurrentScope, getIsolationScope, getTraceContextFromScope } from './currentScopes'; import { DEBUG_BUILD } from './debug-build'; import { createEventEnvelope, createSessionEnvelope } from './envelope'; import type { IntegrationIndex } from './integration'; @@ -40,7 +40,7 @@ import { afterSetupIntegrations } from './integration'; import { setupIntegration, setupIntegrations } from './integration'; import type { Scope } from './scope'; import { updateSession } from './session'; -import { getDynamicSamplingContextFromClient } from './tracing/dynamicSamplingContext'; +import { getDynamicSamplingContextFromScope } from './tracing/dynamicSamplingContext'; import { createClientReportEnvelope } from './utils-hoist/clientreport'; import { dsnToString, makeDsn } from './utils-hoist/dsn'; import { addItemToEnvelope, createAttachmentEnvelopeItem } from './utils-hoist/envelope'; @@ -48,7 +48,6 @@ import { SentryError } from './utils-hoist/error'; import { isParameterizedString, isPlainObject, isPrimitive, isThenable } from './utils-hoist/is'; import { consoleSandbox, logger } from './utils-hoist/logger'; import { checkOrSetAlreadyCaught, uuid4 } from './utils-hoist/misc'; -import { dropUndefinedKeys } from './utils-hoist/object'; import { SyncPromise, rejectedSyncPromise, resolvedSyncPromise } from './utils-hoist/syncpromise'; import { parseSampleRate } from './utils/parseSampleRate'; import { prepareEvent } from './utils/prepareEvent'; @@ -672,7 +671,7 @@ export abstract class BaseClient implements Client { protected _prepareEvent( event: Event, hint: EventHint, - currentScope?: Scope, + currentScope = getCurrentScope(), isolationScope = getIsolationScope(), ): PromiseLike { const options = this.getOptions(); @@ -692,30 +691,18 @@ export abstract class BaseClient implements Client { return evt; } - const propagationContext = { - ...isolationScope.getPropagationContext(), - ...(currentScope ? currentScope.getPropagationContext() : undefined), + evt.contexts = { + trace: getTraceContextFromScope(currentScope), + ...evt.contexts, }; - const trace = evt.contexts && evt.contexts.trace; - if (!trace && propagationContext) { - const { traceId: trace_id, spanId, parentSpanId, dsc } = propagationContext; - evt.contexts = { - trace: dropUndefinedKeys({ - trace_id, - span_id: spanId, - parent_span_id: parentSpanId, - }), - ...evt.contexts, - }; + const dynamicSamplingContext = getDynamicSamplingContextFromScope(this, currentScope); - const dynamicSamplingContext = dsc ? dsc : getDynamicSamplingContextFromClient(trace_id, this); + evt.sdkProcessingMetadata = { + dynamicSamplingContext, + ...evt.sdkProcessingMetadata, + }; - evt.sdkProcessingMetadata = { - dynamicSamplingContext, - ...evt.sdkProcessingMetadata, - }; - } return evt; }); } diff --git a/packages/core/src/currentScopes.ts b/packages/core/src/currentScopes.ts index 77e8aa70a02a..825092fb2d5d 100644 --- a/packages/core/src/currentScopes.ts +++ b/packages/core/src/currentScopes.ts @@ -1,8 +1,9 @@ -import type { Scope } from '@sentry/types'; +import type { Scope, TraceContext } from '@sentry/types'; import type { Client } from '@sentry/types'; import { getAsyncContextStrategy } from './asyncContext'; import { getMainCarrier } from './carrier'; import { Scope as ScopeClass } from './scope'; +import { dropUndefinedKeys } from './utils-hoist/object'; import { getGlobalSingleton } from './utils-hoist/worldwide'; /** @@ -120,3 +121,20 @@ export function withIsolationScope( export function getClient(): C | undefined { return getCurrentScope().getClient(); } + +/** + * Get a trace context for the given scope. + */ +export function getTraceContextFromScope(scope: Scope): TraceContext { + const propagationContext = scope.getPropagationContext(); + + const { traceId, spanId, parentSpanId } = propagationContext; + + const traceContext: TraceContext = dropUndefinedKeys({ + trace_id: traceId, + span_id: spanId, + parent_span_id: parentSpanId, + }); + + return traceContext; +} diff --git a/packages/core/src/fetch.ts b/packages/core/src/fetch.ts index 65629d1291ac..1dbb84423678 100644 --- a/packages/core/src/fetch.ts +++ b/packages/core/src/fetch.ts @@ -1,20 +1,13 @@ import type { Client, HandlerDataFetch, Scope, Span, SpanOrigin } from '@sentry/types'; -import { getClient, getCurrentScope, getIsolationScope } from './currentScopes'; import { SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from './semanticAttributes'; -import { - SPAN_STATUS_ERROR, - getDynamicSamplingContextFromClient, - getDynamicSamplingContextFromSpan, - setHttpStatus, - startInactiveSpan, -} from './tracing'; +import { SPAN_STATUS_ERROR, setHttpStatus, startInactiveSpan } from './tracing'; import { SentryNonRecordingSpan } from './tracing/sentryNonRecordingSpan'; -import { SENTRY_BAGGAGE_KEY_PREFIX, dynamicSamplingContextToSentryBaggageHeader } from './utils-hoist/baggage'; +import { SENTRY_BAGGAGE_KEY_PREFIX } from './utils-hoist/baggage'; import { isInstanceOf } from './utils-hoist/is'; -import { generateSentryTraceHeader } from './utils-hoist/tracing'; import { parseUrl } from './utils-hoist/url'; import { hasTracingEnabled } from './utils/hasTracingEnabled'; -import { getActiveSpan, spanToTraceHeader } from './utils/spanUtils'; +import { getActiveSpan } from './utils/spanUtils'; +import { getTraceData } from './utils/traceData'; type PolymorphicRequestHeaders = | Record @@ -59,9 +52,6 @@ export function instrumentFetchRequest( return undefined; } - const scope = getCurrentScope(); - const client = getClient(); - const { method, url } = handlerData.fetchData; const fullUrl = getFullURL(url); @@ -88,37 +78,34 @@ export function instrumentFetchRequest( handlerData.fetchData.__span = span.spanContext().spanId; spans[span.spanContext().spanId] = span; - if (shouldAttachHeaders(handlerData.fetchData.url) && client) { + if (shouldAttachHeaders(handlerData.fetchData.url)) { const request: string | Request = handlerData.args[0]; - // In case the user hasn't set the second argument of a fetch call we default it to `{}`. - handlerData.args[1] = handlerData.args[1] || {}; - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const options: { [key: string]: any } = handlerData.args[1]; + const options: { [key: string]: unknown } = handlerData.args[1] || {}; - options.headers = addTracingHeadersToFetchRequest( + const headers = _addTracingHeadersToFetchRequest( request, - client, - scope, options, // If performance is disabled (TWP) or there's no active root span (pageload/navigation/interaction), // we do not want to use the span as base for the trace headers, // which means that the headers will be generated from the scope and the sampling decision is deferred hasTracingEnabled() && hasParent ? span : undefined, ); + if (headers) { + // Ensure this is actually set, if no options have been passed previously + handlerData.args[1] = options; + options.headers = headers; + } } return span; } /** - * Adds sentry-trace and baggage headers to the various forms of fetch headers + * Adds sentry-trace and baggage headers to the various forms of fetch headers. */ -export function addTracingHeadersToFetchRequest( - request: string | unknown, // unknown is actually type Request but we can't export DOM types from this package, - client: Client, - scope: Scope, +function _addTracingHeadersToFetchRequest( + request: string | Request, fetchOptionsObj: { headers?: | { @@ -128,31 +115,24 @@ export function addTracingHeadersToFetchRequest( }, span?: Span, ): PolymorphicRequestHeaders | undefined { - const isolationScope = getIsolationScope(); + const traceHeaders = getTraceData({ span }); + const sentryTrace = traceHeaders['sentry-trace']; + const baggage = traceHeaders.baggage; - const { traceId, spanId, sampled, dsc } = { - ...isolationScope.getPropagationContext(), - ...scope.getPropagationContext(), - }; - - const sentryTraceHeader = span ? spanToTraceHeader(span) : generateSentryTraceHeader(traceId, spanId, sampled); - - const sentryBaggageHeader = dynamicSamplingContextToSentryBaggageHeader( - dsc || (span ? getDynamicSamplingContextFromSpan(span) : getDynamicSamplingContextFromClient(traceId, client)), - ); + // Nothing to do, when we return undefined here, the original headers will be used + if (!sentryTrace) { + return undefined; + } - const headers = - fetchOptionsObj.headers || - (typeof Request !== 'undefined' && isInstanceOf(request, Request) ? (request as Request).headers : undefined); + const headers = fetchOptionsObj.headers || (isRequest(request) ? request.headers : undefined); if (!headers) { - return { 'sentry-trace': sentryTraceHeader, baggage: sentryBaggageHeader }; - } else if (typeof Headers !== 'undefined' && isInstanceOf(headers, Headers)) { - const newHeaders = new Headers(headers as Headers); - - newHeaders.set('sentry-trace', sentryTraceHeader); + return { ...traceHeaders }; + } else if (isHeaders(headers)) { + const newHeaders = new Headers(headers); + newHeaders.set('sentry-trace', sentryTrace); - if (sentryBaggageHeader) { + if (baggage) { const prevBaggageHeader = newHeaders.get('baggage'); if (prevBaggageHeader) { const prevHeaderStrippedFromSentryBaggage = stripBaggageHeaderOfSentryBaggageValues(prevBaggageHeader); @@ -160,16 +140,14 @@ export function addTracingHeadersToFetchRequest( 'baggage', // If there are non-sentry entries (i.e. if the stripped string is non-empty/truthy) combine the stripped header and sentry baggage header // otherwise just set the sentry baggage header - prevHeaderStrippedFromSentryBaggage - ? `${prevHeaderStrippedFromSentryBaggage},${sentryBaggageHeader}` - : sentryBaggageHeader, + prevHeaderStrippedFromSentryBaggage ? `${prevHeaderStrippedFromSentryBaggage},${baggage}` : baggage, ); } else { - newHeaders.set('baggage', sentryBaggageHeader); + newHeaders.set('baggage', baggage); } } - return newHeaders as PolymorphicRequestHeaders; + return newHeaders; } else if (Array.isArray(headers)) { const newHeaders = [ ...headers @@ -187,13 +165,13 @@ export function addTracingHeadersToFetchRequest( } }), // Attach the new sentry-trace header - ['sentry-trace', sentryTraceHeader], + ['sentry-trace', sentryTrace], ]; - if (sentryBaggageHeader) { + if (baggage) { // If there are multiple entries with the same key, the browser will merge the values into a single request header. // Its therefore safe to simply push a "baggage" entry, even though there might already be another baggage header. - newHeaders.push(['baggage', sentryBaggageHeader]); + newHeaders.push(['baggage', baggage]); } return newHeaders as PolymorphicRequestHeaders; @@ -211,18 +189,39 @@ export function addTracingHeadersToFetchRequest( newBaggageHeaders.push(stripBaggageHeaderOfSentryBaggageValues(existingBaggageHeader)); } - if (sentryBaggageHeader) { - newBaggageHeaders.push(sentryBaggageHeader); + if (baggage) { + newBaggageHeaders.push(baggage); } return { ...(headers as Exclude), - 'sentry-trace': sentryTraceHeader, + 'sentry-trace': sentryTrace, baggage: newBaggageHeaders.length > 0 ? newBaggageHeaders.join(',') : undefined, }; } } +/** + * Adds sentry-trace and baggage headers to the various forms of fetch headers. + * + * @deprecated This function will not be exported anymore in v9. + */ +export function addTracingHeadersToFetchRequest( + request: string | unknown, + _client: Client | undefined, + _scope: Scope | undefined, + fetchOptionsObj: { + headers?: + | { + [key: string]: string[] | string | undefined; + } + | PolymorphicRequestHeaders; + }, + span?: Span, +): PolymorphicRequestHeaders | undefined { + return _addTracingHeadersToFetchRequest(request as Request, fetchOptionsObj, span); +} + function getFullURL(url: string): string | undefined { try { const parsed = new URL(url); @@ -260,3 +259,11 @@ function stripBaggageHeaderOfSentryBaggageValues(baggageHeader: string): string .join(',') ); } + +function isRequest(request: unknown): request is Request { + return typeof Request !== 'undefined' && isInstanceOf(request, Request); +} + +function isHeaders(headers: unknown): headers is Headers { + return typeof Headers !== 'undefined' && isInstanceOf(headers, Headers); +} diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index c56c568d092f..eed487e961ba 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -40,6 +40,7 @@ export { withScope, withIsolationScope, getClient, + getTraceContextFromScope, } from './currentScopes'; export { getDefaultCurrentScope, @@ -111,7 +112,11 @@ export type { MetricData } from '@sentry/types'; export { metricsDefault } from './metrics/exports-default'; export { BrowserMetricsAggregator } from './metrics/browser-aggregator'; export { getMetricSummaryJsonForSpan } from './metrics/metric-summary'; -export { addTracingHeadersToFetchRequest, instrumentFetchRequest } from './fetch'; +export { + // eslint-disable-next-line deprecation/deprecation + addTracingHeadersToFetchRequest, + instrumentFetchRequest, +} from './fetch'; export { trpcMiddleware } from './trpc'; export { captureFeedback } from './feedback'; diff --git a/packages/core/src/server-runtime-client.ts b/packages/core/src/server-runtime-client.ts index 93c1051ed6ec..c044c1be7bc1 100644 --- a/packages/core/src/server-runtime-client.ts +++ b/packages/core/src/server-runtime-client.ts @@ -14,12 +14,12 @@ import type { import { BaseClient } from './baseclient'; import { createCheckInEnvelope } from './checkin'; -import { getIsolationScope } from './currentScopes'; +import { getIsolationScope, getTraceContextFromScope } from './currentScopes'; import { DEBUG_BUILD } from './debug-build'; import type { Scope } from './scope'; import { SessionFlusher } from './sessionflusher'; import { - getDynamicSamplingContextFromClient, + getDynamicSamplingContextFromScope, getDynamicSamplingContextFromSpan, registerSpanErrorInstrumentation, } from './tracing'; @@ -28,7 +28,7 @@ import { logger } from './utils-hoist/logger'; import { uuid4 } from './utils-hoist/misc'; import { resolvedSyncPromise } from './utils-hoist/syncpromise'; import { _getSpanForScope } from './utils/spanOnScope'; -import { getRootSpan, spanToTraceContext } from './utils/spanUtils'; +import { spanToTraceContext } from './utils/spanUtils'; export interface ServerRuntimeClientOptions extends ClientOptions { platform?: string; @@ -251,7 +251,7 @@ export class ServerRuntimeClient< } /** Extract trace information from scope */ - private _getTraceInfoFromScope( + protected _getTraceInfoFromScope( scope: Scope | undefined, ): [dynamicSamplingContext: Partial | undefined, traceContext: TraceContext | undefined] { if (!scope) { @@ -259,22 +259,11 @@ export class ServerRuntimeClient< } const span = _getSpanForScope(scope); - if (span) { - const rootSpan = getRootSpan(span); - const samplingContext = getDynamicSamplingContextFromSpan(rootSpan); - return [samplingContext, spanToTraceContext(rootSpan)]; - } - - const { traceId, spanId, parentSpanId, dsc } = scope.getPropagationContext(); - const traceContext: TraceContext = { - trace_id: traceId, - span_id: spanId, - parent_span_id: parentSpanId, - }; - if (dsc) { - return [dsc, traceContext]; - } - return [getDynamicSamplingContextFromClient(traceId, this), traceContext]; + const traceContext = span ? spanToTraceContext(span) : getTraceContextFromScope(scope); + const dynamicSamplingContext = span + ? getDynamicSamplingContextFromSpan(span) + : getDynamicSamplingContextFromScope(this, scope); + return [dynamicSamplingContext, traceContext]; } } diff --git a/packages/core/src/tracing/dynamicSamplingContext.ts b/packages/core/src/tracing/dynamicSamplingContext.ts index 1e8ca0448b3b..a1bb008a2572 100644 --- a/packages/core/src/tracing/dynamicSamplingContext.ts +++ b/packages/core/src/tracing/dynamicSamplingContext.ts @@ -1,4 +1,4 @@ -import type { Client, DynamicSamplingContext, Span } from '@sentry/types'; +import type { Client, DynamicSamplingContext, Scope, Span } from '@sentry/types'; import { DEFAULT_ENVIRONMENT } from '../constants'; import { getClient } from '../currentScopes'; @@ -51,6 +51,14 @@ export function getDynamicSamplingContextFromClient(trace_id: string, client: Cl return dsc; } +/** + * Get the dynamic sampling context for the currently active scopes. + */ +export function getDynamicSamplingContextFromScope(client: Client, scope: Scope): Partial { + const propagationContext = scope.getPropagationContext(); + return propagationContext.dsc || getDynamicSamplingContextFromClient(propagationContext.traceId, client); +} + /** * Creates a dynamic sampling context from a span (and client and scope) * @@ -64,8 +72,6 @@ export function getDynamicSamplingContextFromSpan(span: Span): Readonly): Client { + getCurrentScope().setPropagationContext({ + traceId: SCOPE_TRACE_ID, + spanId: SCOPE_SPAN_ID, + }); -const mockedScope = { - getPropagationContext: () => ({ - traceId: '123', - }), -} as any; + const options = getDefaultTestClientOptions({ + dsn, + tracesSampleRate: 1, + ...opts, + }); + const client = new TestClient(options); + setCurrentClient(client); + client.init(); + + return client; +} describe('getTraceData', () => { beforeEach(() => { - jest.spyOn(SentryCoreExports, 'isEnabled').mockReturnValue(true); + setAsyncContextStrategy(undefined); + getCurrentScope().clear(); + getIsolationScope().clear(); + getGlobalScope().clear(); + getCurrentScope().setClient(undefined); }); afterEach(() => { jest.clearAllMocks(); }); - it('returns the tracing data from the span, if a span is available', () => { - { - jest.spyOn(SentryCoreTracing, 'getDynamicSamplingContextFromSpan').mockReturnValueOnce({ - environment: 'production', - }); - jest.spyOn(SentryCoreSpanUtils, 'getActiveSpan').mockImplementationOnce(() => mockedSpan); - jest.spyOn(SentryCoreCurrentScopes, 'getCurrentScope').mockImplementationOnce(() => mockedScope); + it('uses the ACS implementation, if available', () => { + setupClient(); + + const carrier = getMainCarrier(); + + const customFn = jest.fn((options?: { span?: Span }) => { + expect(options).toEqual({ span: undefined }); + return { + 'sentry-trace': 'abc', + baggage: 'xyz', + }; + }) as typeof getTraceData; + + const acs = { + ...getAsyncContextStrategy(carrier), + getTraceData: customFn, + }; + setAsyncContextStrategy(acs); + + const span = new SentrySpan({ + traceId: '12345678901234567890123456789012', + spanId: '1234567890123456', + sampled: true, + }); + withActiveSpan(span, () => { const data = getTraceData(); expect(data).toEqual({ - 'sentry-trace': '12345678901234567890123456789012-1234567890123456-1', - baggage: 'sentry-environment=production', + 'sentry-trace': 'abc', + baggage: 'xyz', }); - } + }); }); - it('returns propagationContext DSC data if no span is available', () => { - jest.spyOn(SentryCoreSpanUtils, 'getActiveSpan').mockImplementationOnce(() => undefined); - jest.spyOn(SentryCoreCurrentScopes, 'getCurrentScope').mockImplementationOnce( - () => - ({ - getPropagationContext: () => ({ - traceId: '12345678901234567890123456789012', - sampled: true, - spanId: '1234567890123456', - dsc: { - environment: 'staging', - public_key: 'key', - trace_id: '12345678901234567890123456789012', - }, - }), - }) as any, - ); - jest.spyOn(SentryCoreCurrentScopes, 'getClient').mockImplementationOnce(() => mockedClient); + it('passes span to ACS implementation, if available', () => { + setupClient(); - const traceData = getTraceData(); + const carrier = getMainCarrier(); - expect(traceData).toEqual({ - 'sentry-trace': expect.stringMatching(/12345678901234567890123456789012-(.{16})-1/), - baggage: 'sentry-environment=staging,sentry-public_key=key,sentry-trace_id=12345678901234567890123456789012', + const span = new SentrySpan({ + traceId: '12345678901234567890123456789012', + spanId: '1234567890123456', + sampled: true, }); - }); - it('returns only the `sentry-trace` value if no DSC is available', () => { - jest.spyOn(SentryCoreTracing, 'getDynamicSamplingContextFromClient').mockReturnValueOnce({ - trace_id: '', - public_key: undefined, + const customFn = jest.fn((options?: { span?: Span }) => { + expect(options).toEqual({ span }); + return { + 'sentry-trace': 'abc', + baggage: 'xyz', + }; + }) as typeof getTraceData; + + const acs = { + ...getAsyncContextStrategy(carrier), + getTraceData: customFn, + }; + setAsyncContextStrategy(acs); + + const data = getTraceData({ span }); + + expect(data).toEqual({ + 'sentry-trace': 'abc', + baggage: 'xyz', }); + }); - // @ts-expect-error - we don't need to provide all the properties - jest.spyOn(SentryCoreSpanUtils, 'getActiveSpan').mockImplementationOnce(() => ({ - isRecording: () => true, - spanContext: () => { - return { - traceId: '12345678901234567890123456789012', - spanId: '1234567890123456', - traceFlags: TRACE_FLAG_SAMPLED, - }; - }, - })); + it('returns the tracing data from the span, if a span is available', () => { + setupClient(); - jest.spyOn(SentryCoreCurrentScopes, 'getCurrentScope').mockImplementationOnce(() => mockedScope); - jest.spyOn(SentryCoreCurrentScopes, 'getClient').mockImplementationOnce(() => mockedClient); + const span = new SentrySpan({ + traceId: '12345678901234567890123456789012', + spanId: '1234567890123456', + sampled: true, + }); - const traceData = getTraceData(); + withActiveSpan(span, () => { + const data = getTraceData(); - expect(traceData).toEqual({ - 'sentry-trace': '12345678901234567890123456789012-1234567890123456-1', + expect(data).toEqual({ + 'sentry-trace': '12345678901234567890123456789012-1234567890123456-1', + baggage: + 'sentry-environment=production,sentry-public_key=123,sentry-trace_id=12345678901234567890123456789012,sentry-sampled=true', + }); }); }); - it('returns only the `sentry-trace` tag if no DSC is available without a client', () => { - jest.spyOn(SentryCoreTracing, 'getDynamicSamplingContextFromClient').mockReturnValueOnce({ - trace_id: '', - public_key: undefined, + it('allows to pass a span directly', () => { + setupClient(); + + const span = new SentrySpan({ + traceId: '12345678901234567890123456789012', + spanId: '1234567890123456', + sampled: true, + }); + + const data = getTraceData({ span }); + + expect(data).toEqual({ + 'sentry-trace': '12345678901234567890123456789012-1234567890123456-1', + baggage: + 'sentry-environment=production,sentry-public_key=123,sentry-trace_id=12345678901234567890123456789012,sentry-sampled=true', }); + }); - // @ts-expect-error - we don't need to provide all the properties - jest.spyOn(SentryCoreSpanUtils, 'getActiveSpan').mockImplementationOnce(() => ({ - isRecording: () => true, - spanContext: () => { - return { - traceId: '12345678901234567890123456789012', - spanId: '1234567890123456', - traceFlags: TRACE_FLAG_SAMPLED, - }; + it('returns propagationContext DSC data if no span is available', () => { + setupClient(); + + getCurrentScope().setPropagationContext({ + traceId: '12345678901234567890123456789012', + sampled: true, + spanId: '1234567890123456', + dsc: { + environment: 'staging', + public_key: 'key', + trace_id: '12345678901234567890123456789012', }, - })); - jest.spyOn(SentryCoreCurrentScopes, 'getCurrentScope').mockImplementationOnce(() => mockedScope); - jest.spyOn(SentryCoreCurrentScopes, 'getClient').mockImplementationOnce(() => undefined); + }); const traceData = getTraceData(); expect(traceData).toEqual({ 'sentry-trace': '12345678901234567890123456789012-1234567890123456-1', + baggage: 'sentry-environment=staging,sentry-public_key=key,sentry-trace_id=12345678901234567890123456789012', }); - expect('baggage' in traceData).toBe(false); }); - it('returns an empty object if the `sentry-trace` value is invalid', () => { - // @ts-expect-error - we don't need to provide all the properties - jest.spyOn(SentryCoreSpanUtils, 'getActiveSpan').mockImplementationOnce(() => ({ - isRecording: () => true, - spanContext: () => { - return { - traceId: '1234567890123456789012345678901+', - spanId: '1234567890123456', - traceFlags: TRACE_FLAG_SAMPLED, - }; - }, - })); + it('returns frozen DSC from SentrySpan if available', () => { + setupClient(); + + const span = new SentrySpan({ + traceId: '12345678901234567890123456789012', + spanId: '1234567890123456', + sampled: true, + }); + + freezeDscOnSpan(span, { + environment: 'test-dev', + public_key: '456', + trace_id: '12345678901234567890123456789088', + }); + + withActiveSpan(span, () => { + const data = getTraceData(); + + expect(data).toEqual({ + 'sentry-trace': '12345678901234567890123456789012-1234567890123456-1', + baggage: 'sentry-environment=test-dev,sentry-public_key=456,sentry-trace_id=12345678901234567890123456789088', + }); + }); + }); + + it('works with an OTEL span with frozen DSC in traceState', () => { + setupClient(); + + const traceId = '12345678901234567890123456789099'; + const spanId = '1234567890123499'; + const span = new SentrySpan({ + traceId, + spanId, + sampled: true, + }); + + span.spanContext = () => { + const traceState = { + set: () => traceState, + unset: () => traceState, + get: (key: string) => { + if (key === 'sentry.dsc') { + return 'sentry-environment=test-dev,sentry-public_key=456,sentry-trace_id=12345678901234567890123456789088'; + } + return undefined; + }, + serialize: () => '', + }; + + return { + traceId, + spanId, + sampled: true, + traceFlags: 1, + traceState, + }; + }; + + withActiveSpan(span, () => { + const data = getTraceData(); + + expect(data).toEqual({ + 'sentry-trace': '12345678901234567890123456789099-1234567890123499-1', + baggage: 'sentry-environment=test-dev,sentry-public_key=456,sentry-trace_id=12345678901234567890123456789088', + }); + }); + }); + + it('returns empty object without a client', () => { const traceData = getTraceData(); expect(traceData).toEqual({}); }); + it('returns an empty object if the `sentry-trace` value is invalid', () => { + // Invalid traceID + const traceId = '1234567890123456789012345678901+'; + const spanId = '1234567890123499'; + + const span = new SentrySpan({ + traceId, + spanId, + sampled: true, + }); + + withActiveSpan(span, () => { + const data = getTraceData(); + expect(data).toEqual({}); + }); + }); + it('returns an empty object if the SDK is disabled', () => { - jest.spyOn(SentryCoreExports, 'isEnabled').mockReturnValueOnce(false); + setupClient({ dsn: undefined }); const traceData = getTraceData(); diff --git a/packages/node/src/sdk/client.ts b/packages/node/src/sdk/client.ts index 8179d40e2819..b4730ac0f07c 100644 --- a/packages/node/src/sdk/client.ts +++ b/packages/node/src/sdk/client.ts @@ -2,9 +2,10 @@ import * as os from 'node:os'; import type { Tracer } from '@opentelemetry/api'; import { trace } from '@opentelemetry/api'; import type { BasicTracerProvider } from '@opentelemetry/sdk-trace-base'; -import type { ServerRuntimeClientOptions } from '@sentry/core'; -import { SDK_VERSION, ServerRuntimeClient, applySdkMetadata } from '@sentry/core'; -import { logger } from '@sentry/core'; +import type { Scope, ServerRuntimeClientOptions } from '@sentry/core'; +import { SDK_VERSION, ServerRuntimeClient, applySdkMetadata, logger } from '@sentry/core'; +import { getTraceContextForScope } from '@sentry/opentelemetry'; +import type { DynamicSamplingContext, TraceContext } from '@sentry/types'; import { isMainThread, threadId } from 'worker_threads'; import { DEBUG_BUILD } from '../debug-build'; import type { NodeClientOptions } from '../types'; @@ -115,4 +116,15 @@ export class NodeClient extends ServerRuntimeClient { process.on('beforeExit', this._clientReportOnExitFlushListener); } } + + /** Custom implementation for OTEL, so we can handle scope-span linking. */ + protected _getTraceInfoFromScope( + scope: Scope | undefined, + ): [dynamicSamplingContext: Partial | undefined, traceContext: TraceContext | undefined] { + if (!scope) { + return [undefined, undefined]; + } + + return getTraceContextForScope(this, scope); + } } diff --git a/packages/opentelemetry/src/index.ts b/packages/opentelemetry/src/index.ts index 98460b575c8d..55f657061989 100644 --- a/packages/opentelemetry/src/index.ts +++ b/packages/opentelemetry/src/index.ts @@ -27,7 +27,14 @@ export { enhanceDscWithOpenTelemetryRootSpanName } from './utils/enhanceDscWithO export { generateSpanContextForPropagationContext } from './utils/generateSpanContextForPropagationContext'; export { getActiveSpan } from './utils/getActiveSpan'; -export { startSpan, startSpanManual, startInactiveSpan, withActiveSpan, continueTrace } from './trace'; +export { + startSpan, + startSpanManual, + startInactiveSpan, + withActiveSpan, + continueTrace, + getTraceContextForScope, +} from './trace'; export { suppressTracing } from './utils/suppressTracing'; diff --git a/packages/opentelemetry/src/propagator.ts b/packages/opentelemetry/src/propagator.ts index 054a348fd7b5..6c4009888416 100644 --- a/packages/opentelemetry/src/propagator.ts +++ b/packages/opentelemetry/src/propagator.ts @@ -5,15 +5,10 @@ import { propagation, trace } from '@opentelemetry/api'; import { W3CBaggagePropagator, isTracingSuppressed } from '@opentelemetry/core'; import { ATTR_URL_FULL, SEMATTRS_HTTP_URL } from '@opentelemetry/semantic-conventions'; import type { continueTrace } from '@sentry/core'; +import { getDynamicSamplingContextFromScope } from '@sentry/core'; import { getRootSpan } from '@sentry/core'; import { spanToJSON } from '@sentry/core'; -import { - getClient, - getCurrentScope, - getDynamicSamplingContextFromClient, - getDynamicSamplingContextFromSpan, - getIsolationScope, -} from '@sentry/core'; +import { getClient, getCurrentScope, getDynamicSamplingContextFromSpan, getIsolationScope } from '@sentry/core'; import { LRUMap, SENTRY_BAGGAGE_KEY_PREFIX, @@ -191,7 +186,10 @@ export class SentryPropagator extends W3CBaggagePropagator { } } -function getInjectionData(context: Context): { +/** + * Get propagation injection data for the given context. + */ +export function getInjectionData(context: Context): { dynamicSamplingContext: Partial | undefined; traceId: string | undefined; spanId: string | undefined; @@ -204,8 +202,7 @@ function getInjectionData(context: Context): { if (span && !spanIsRemote) { const spanContext = span.spanContext(); - const propagationContext = getPropagationContextFromSpan(span); - const dynamicSamplingContext = getDynamicSamplingContext(propagationContext, spanContext.traceId); + const dynamicSamplingContext = getDynamicSamplingContextFromSpan(span); return { dynamicSamplingContext, traceId: spanContext.traceId, @@ -216,9 +213,10 @@ function getInjectionData(context: Context): { // Else we try to use the propagation context from the scope const scope = getScopesFromContext(context)?.scope || getCurrentScope(); + const client = getClient(); const propagationContext = scope.getPropagationContext(); - const dynamicSamplingContext = getDynamicSamplingContext(propagationContext, propagationContext.traceId); + const dynamicSamplingContext = client ? getDynamicSamplingContextFromScope(client, scope) : undefined; return { dynamicSamplingContext, traceId: propagationContext.traceId, @@ -227,26 +225,6 @@ function getInjectionData(context: Context): { }; } -/** Get the DSC from a context, or fall back to use the one from the client. */ -function getDynamicSamplingContext( - propagationContext: PropagationContext, - traceId: string | undefined, -): Partial | undefined { - // If we have a DSC on the propagation context, we just use it - if (propagationContext?.dsc) { - return propagationContext.dsc; - } - - // Else, we try to generate a new one - const client = getClient(); - - if (client) { - return getDynamicSamplingContextFromClient(traceId || propagationContext.traceId, client); - } - - return undefined; -} - function getContextWithRemoteActiveSpan( ctx: Context, { sentryTrace, baggage }: Parameters[0], diff --git a/packages/opentelemetry/src/trace.ts b/packages/opentelemetry/src/trace.ts index 1ed0fa6a1322..3fa994a74de8 100644 --- a/packages/opentelemetry/src/trace.ts +++ b/packages/opentelemetry/src/trace.ts @@ -7,12 +7,15 @@ import { continueTrace as baseContinueTrace, getClient, getCurrentScope, + getDynamicSamplingContextFromScope, getDynamicSamplingContextFromSpan, getRootSpan, + getTraceContextFromScope, handleCallbackErrors, spanToJSON, + spanToTraceContext, } from '@sentry/core'; -import type { Client, Scope, Span as SentrySpan } from '@sentry/types'; +import type { Client, DynamicSamplingContext, Scope, Span as SentrySpan, TraceContext } from '@sentry/types'; import { continueTraceAsRemoteSpan } from './propagator'; import type { OpenTelemetryClient, OpenTelemetrySpanContext } from './types'; @@ -279,6 +282,25 @@ export function continueTrace(options: Parameters[0 }); } +/** + * Get the trace context for a given scope. + * We have a custom implemention here because we need an OTEL-specific way to get the span from a scope. + */ +export function getTraceContextForScope( + client: Client, + scope: Scope, +): [dynamicSamplingContext: Partial, traceContext: TraceContext] { + const ctx = getContextFromScope(scope); + const span = ctx && trace.getSpan(ctx); + + const traceContext = span ? spanToTraceContext(span) : getTraceContextFromScope(scope); + + const dynamicSamplingContext = span + ? getDynamicSamplingContextFromSpan(span) + : getDynamicSamplingContextFromScope(client, scope); + return [dynamicSamplingContext, traceContext]; +} + function getActiveSpanWrapper(parentSpan: Span | SentrySpan | undefined | null): (callback: () => T) => T { return parentSpan !== undefined ? (callback: () => T) => { diff --git a/packages/opentelemetry/src/utils/getTraceData.ts b/packages/opentelemetry/src/utils/getTraceData.ts index 8d91c74bd294..c79fc2a6e957 100644 --- a/packages/opentelemetry/src/utils/getTraceData.ts +++ b/packages/opentelemetry/src/utils/getTraceData.ts @@ -1,22 +1,30 @@ import * as api from '@opentelemetry/api'; -import { dropUndefinedKeys } from '@sentry/core'; -import type { SerializedTraceData } from '@sentry/types'; +import { + dynamicSamplingContextToSentryBaggageHeader, + generateSentryTraceHeader, + getCapturedScopesOnSpan, +} from '@sentry/core'; +import type { SerializedTraceData, Span } from '@sentry/types'; +import { getInjectionData } from '../propagator'; +import { getContextFromScope } from './contextData'; /** * Otel-specific implementation of `getTraceData`. * @see `@sentry/core` version of `getTraceData` for more information */ -export function getTraceData(): SerializedTraceData { - const headersObject: Record = {}; +export function getTraceData({ span }: { span?: Span } = {}): SerializedTraceData { + let ctx = api.context.active(); - api.propagation.inject(api.context.active(), headersObject); - - if (!headersObject['sentry-trace']) { - return {}; + if (span) { + const { scope } = getCapturedScopesOnSpan(span); + // fall back to current context if for whatever reason we can't find the one of the span + ctx = (scope && getContextFromScope(scope)) || api.trace.setSpan(api.context.active(), span); } - return dropUndefinedKeys({ - 'sentry-trace': headersObject['sentry-trace'], - baggage: headersObject.baggage, - }); + const { traceId, spanId, sampled, dynamicSamplingContext } = getInjectionData(ctx); + + return { + 'sentry-trace': generateSentryTraceHeader(traceId, spanId, sampled), + baggage: dynamicSamplingContextToSentryBaggageHeader(dynamicSamplingContext), + }; } diff --git a/packages/opentelemetry/test/utils/getTraceData.test.ts b/packages/opentelemetry/test/utils/getTraceData.test.ts new file mode 100644 index 000000000000..e0f2270d8e22 --- /dev/null +++ b/packages/opentelemetry/test/utils/getTraceData.test.ts @@ -0,0 +1,93 @@ +import { context, trace } from '@opentelemetry/api'; +import { getCurrentScope, setAsyncContextStrategy } from '@sentry/core'; +import { getTraceData } from '../../src/utils/getTraceData'; +import { makeTraceState } from '../../src/utils/makeTraceState'; +import { cleanupOtel, mockSdkInit } from '../helpers/mockSdkInit'; + +describe('getTraceData', () => { + beforeEach(() => { + setAsyncContextStrategy(undefined); + mockSdkInit(); + }); + + afterEach(() => { + cleanupOtel(); + jest.clearAllMocks(); + }); + + it('returns the tracing data from the span, if a span is available', () => { + const ctx = trace.setSpanContext(context.active(), { + traceId: '12345678901234567890123456789012', + spanId: '1234567890123456', + traceFlags: 1, + }); + + context.with(ctx, () => { + const data = getTraceData(); + + expect(data).toEqual({ + 'sentry-trace': '12345678901234567890123456789012-1234567890123456-1', + baggage: + 'sentry-environment=production,sentry-public_key=username,sentry-trace_id=12345678901234567890123456789012,sentry-sampled=true', + }); + }); + }); + + it('allows to pass a span directly', () => { + const ctx = trace.setSpanContext(context.active(), { + traceId: '12345678901234567890123456789012', + spanId: '1234567890123456', + traceFlags: 1, + }); + + const span = trace.getSpan(ctx)!; + + const data = getTraceData({ span }); + + expect(data).toEqual({ + 'sentry-trace': '12345678901234567890123456789012-1234567890123456-1', + baggage: + 'sentry-environment=production,sentry-public_key=username,sentry-trace_id=12345678901234567890123456789012,sentry-sampled=true', + }); + }); + + it('returns propagationContext DSC data if no span is available', () => { + getCurrentScope().setPropagationContext({ + traceId: '12345678901234567890123456789012', + sampled: true, + spanId: '1234567890123456', + dsc: { + environment: 'staging', + public_key: 'key', + trace_id: '12345678901234567890123456789012', + }, + }); + + const traceData = getTraceData(); + + expect(traceData).toEqual({ + 'sentry-trace': '12345678901234567890123456789012-1234567890123456-1', + baggage: 'sentry-environment=staging,sentry-public_key=key,sentry-trace_id=12345678901234567890123456789012', + }); + }); + + it('works with an span with frozen DSC in traceState', () => { + const ctx = trace.setSpanContext(context.active(), { + traceId: '12345678901234567890123456789012', + spanId: '1234567890123456', + traceFlags: 1, + traceState: makeTraceState({ + dsc: { environment: 'test-dev', public_key: '456', trace_id: '12345678901234567890123456789088' }, + }), + }); + + context.with(ctx, () => { + const data = getTraceData(); + + expect(data).toEqual({ + 'sentry-trace': '12345678901234567890123456789012-1234567890123456-1', + baggage: 'sentry-environment=test-dev,sentry-public_key=456,sentry-trace_id=12345678901234567890123456789088', + }); + }); + }); +}); diff --git a/packages/remix/src/utils/instrumentServer.ts b/packages/remix/src/utils/instrumentServer.ts index ac1382bc5e00..fbd874a12df9 100644 --- a/packages/remix/src/utils/instrumentServer.ts +++ b/packages/remix/src/utils/instrumentServer.ts @@ -6,16 +6,16 @@ import { getActiveSpan, getClient, getRootSpan, + getTraceData, hasTracingEnabled, setHttpStatus, spanToJSON, - spanToTraceHeader, startSpan, winterCGRequestToRequestData, withIsolationScope, } from '@sentry/core'; -import { dynamicSamplingContextToSentryBaggageHeader, fill, isNodeEnv, loadModule, logger } from '@sentry/core'; -import { continueTrace, getDynamicSamplingContextFromSpan } from '@sentry/opentelemetry'; +import { fill, isNodeEnv, loadModule, logger } from '@sentry/core'; +import { continueTrace } from '@sentry/opentelemetry'; import type { RequestEventData, TransactionSource, WrappedFunction } from '@sentry/types'; import type { Span } from '@sentry/types'; @@ -204,18 +204,13 @@ function getTraceAndBaggage(): { sentryTrace?: string; sentryBaggage?: string; } { - if (isNodeEnv() && hasTracingEnabled()) { - const span = getActiveSpan(); - const rootSpan = span && getRootSpan(span); + if (isNodeEnv()) { + const traceData = getTraceData(); - if (rootSpan) { - const dynamicSamplingContext = getDynamicSamplingContextFromSpan(rootSpan); - - return { - sentryTrace: spanToTraceHeader(span), - sentryBaggage: dynamicSamplingContextToSentryBaggageHeader(dynamicSamplingContext), - }; - } + return { + sentryTrace: traceData['sentry-trace'], + sentryBaggage: traceData.baggage, + }; } return {}; diff --git a/packages/remix/test/integration/test/client/root-loader.test.ts b/packages/remix/test/integration/test/client/root-loader.test.ts index 53b7648756e5..431195e8eab7 100644 --- a/packages/remix/test/integration/test/client/root-loader.test.ts +++ b/packages/remix/test/integration/test/client/root-loader.test.ts @@ -25,8 +25,8 @@ test('should inject `sentry-trace` and `baggage` into root loader returning an e const { sentryTrace, sentryBaggage } = await extractTraceAndBaggageFromMeta(page); - expect(sentryTrace).toEqual(expect.any(String)); - expect(sentryBaggage).toEqual(expect.any(String)); + expect(sentryTrace).toMatch(/.+/); + expect(sentryBaggage).toMatch(/.+/); const rootData = (await getRouteData(page))['root']; @@ -41,8 +41,8 @@ test('should inject `sentry-trace` and `baggage` into root loader returning a pl const { sentryTrace, sentryBaggage } = await extractTraceAndBaggageFromMeta(page); - expect(sentryTrace).toEqual(expect.any(String)); - expect(sentryBaggage).toEqual(expect.any(String)); + expect(sentryTrace).toMatch(/.+/); + expect(sentryBaggage).toMatch(/.+/); const rootData = (await getRouteData(page))['root']; @@ -59,8 +59,8 @@ test('should inject `sentry-trace` and `baggage` into root loader returning a `J const { sentryTrace, sentryBaggage } = await extractTraceAndBaggageFromMeta(page); - expect(sentryTrace).toEqual(expect.any(String)); - expect(sentryBaggage).toEqual(expect.any(String)); + expect(sentryTrace).toMatch(/.+/); + expect(sentryBaggage).toMatch(/.+/); const rootData = (await getRouteData(page))['root']; @@ -77,8 +77,8 @@ test('should inject `sentry-trace` and `baggage` into root loader returning a de const { sentryTrace, sentryBaggage } = await extractTraceAndBaggageFromMeta(page); - expect(sentryTrace).toEqual(expect.any(String)); - expect(sentryBaggage).toEqual(expect.any(String)); + expect(sentryTrace).toMatch(/.+/); + expect(sentryBaggage).toMatch(/.+/); const rootData = (await getRouteData(page))['root']; @@ -93,8 +93,8 @@ test('should inject `sentry-trace` and `baggage` into root loader returning `nul const { sentryTrace, sentryBaggage } = await extractTraceAndBaggageFromMeta(page); - expect(sentryTrace).toEqual(expect.any(String)); - expect(sentryBaggage).toEqual(expect.any(String)); + expect(sentryTrace).toMatch(/.+/); + expect(sentryBaggage).toMatch(/.+/); const rootData = (await getRouteData(page))['root']; @@ -109,8 +109,8 @@ test('should inject `sentry-trace` and `baggage` into root loader returning `und const { sentryTrace, sentryBaggage } = await extractTraceAndBaggageFromMeta(page); - expect(sentryTrace).toEqual(expect.any(String)); - expect(sentryBaggage).toEqual(expect.any(String)); + expect(sentryTrace).toMatch(/.+/); + expect(sentryBaggage).toMatch(/.+/); const rootData = (await getRouteData(page))['root']; @@ -130,8 +130,8 @@ test('should inject `sentry-trace` and `baggage` into root loader throwing a red const { sentryTrace, sentryBaggage } = await extractTraceAndBaggageFromMeta(page); - expect(sentryTrace).toEqual(expect.any(String)); - expect(sentryBaggage).toEqual(expect.any(String)); + expect(sentryTrace).toMatch(/.+/); + expect(sentryBaggage).toMatch(/.+/); const rootData = (await getRouteData(page))['root']; @@ -151,8 +151,8 @@ test('should inject `sentry-trace` and `baggage` into root loader returning a re const { sentryTrace, sentryBaggage } = await extractTraceAndBaggageFromMeta(page); - expect(sentryTrace).toEqual(expect.any(String)); - expect(sentryBaggage).toEqual(expect.any(String)); + expect(sentryTrace).toMatch(/.+/); + expect(sentryBaggage).toMatch(/.+/); const rootData = (await getRouteData(page))['root']; From 1d323740b82e4043404362697872bde04b97cb36 Mon Sep 17 00:00:00 2001 From: Andrei <168741329+andreiborza@users.noreply.github.com> Date: Tue, 26 Nov 2024 14:12:31 +0100 Subject: [PATCH 26/34] chore(nuxt): Remove nft override to ensure we are testing with latest nitro (#14480) Noticed that our e2e tests are not actually using nitro 2.10 despite being on the latest nuxt version: https://github.com/getsentry/sentry-javascript/actions/runs/12028119465/job/33531969920#step:13:196 This fixes it. --- dev-packages/e2e-tests/test-applications/nuxt-3/package.json | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/dev-packages/e2e-tests/test-applications/nuxt-3/package.json b/dev-packages/e2e-tests/test-applications/nuxt-3/package.json index 0b9654108d48..8cc66d2d408e 100644 --- a/dev-packages/e2e-tests/test-applications/nuxt-3/package.json +++ b/dev-packages/e2e-tests/test-applications/nuxt-3/package.json @@ -15,14 +15,11 @@ }, "dependencies": { "@sentry/nuxt": "latest || *", - "nuxt": "^3.13.1" + "nuxt": "^3.14.0" }, "devDependencies": { "@nuxt/test-utils": "^3.14.1", "@playwright/test": "^1.44.1", "@sentry-internal/test-utils": "link:../../../test-utils" - }, - "overrides": { - "@vercel/nft": "0.27.4" } } From 973ef9c356f4d547fe0204e919ae32c480a1fdb7 Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Tue, 26 Nov 2024 16:09:35 +0100 Subject: [PATCH 27/34] chore: Add GHSA entry for nuxt e2e test (#14490) --- .github/dependency-review-config.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/dependency-review-config.yml b/.github/dependency-review-config.yml index 99deb0e2677c..3becba39719e 100644 --- a/.github/dependency-review-config.yml +++ b/.github/dependency-review-config.yml @@ -1,7 +1,9 @@ fail-on-severity: 'high' allow-ghsas: # dependency review does not allow specific file exclusions - # we use an older version of NextJS in our tests and thus need to + # we use an older version of NextJS in our tests and thus need to # exclude this # once our minimum supported version is over 14.1.1 this can be removed - GHSA-fr5h-rqp8-mj6g + # we need this for an E2E test for the minimum required version of Nuxt 3.7.0 + - GHSA-v784-fjjh-f8r4 From 23e378314f3342a62f779623b877f4ab55943e9e Mon Sep 17 00:00:00 2001 From: Sigrid Huemer <32902192+s1gr1d@users.noreply.github.com> Date: Tue, 26 Nov 2024 19:52:33 +0100 Subject: [PATCH 28/34] meta(nuxt): Require minimum Nuxt v3.7.0 (#14473) This PR adds a minimum required version for the Nuxt SDK. Version 3.7.0 was selected because this version adds the possibility to set client source maps to `'hidden'` ([PR here](https://github.com/nuxt/nuxt/pull/22787)). However, as the SDK relies on other versions as well (see below, at the bottom), those packages need to be manually bumped (in case the lock-file is not updated yet). **Nuxt 3.7.0** at least supports the correct version range: ["nitropack": "^2.6.1"](https://github.com/nuxt/nuxt/blob/v3.7.0/packages/nuxt/package.json#L84) ["ofetch": "^1.3.3"](https://github.com/nuxt/nuxt/blob/v3.7.0/packages/nuxt/package.json#L87) Above **Nuxt v3.14.0**, everything works out of the box, as the versions are already updated: ["nitropack": "^2.10.2"](https://github.com/nuxt/nuxt/blob/v3.14.0/packages/nuxt/package.json#L97) ["ofetch": "^1.4.1"](https://github.com/nuxt/nuxt/blob/v3.14.0/packages/nuxt/package.json#L100) #### Minimum versions, the SDK relies on: - `nitropack` 2.10.0: for the correct peerDependency of `@vercel/nft` - `ofetch` 1.4.0: for being able to patch `$fetch` --- .../e2e-tests/test-applications/nuxt-3-min/package.json | 8 ++++---- packages/nuxt/package.json | 2 +- packages/nuxt/src/module.ts | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/dev-packages/e2e-tests/test-applications/nuxt-3-min/package.json b/dev-packages/e2e-tests/test-applications/nuxt-3-min/package.json index 18f798f89246..34180346b252 100644 --- a/dev-packages/e2e-tests/test-applications/nuxt-3-min/package.json +++ b/dev-packages/e2e-tests/test-applications/nuxt-3-min/package.json @@ -1,6 +1,6 @@ { "name": "nuxt-3-min", - "description": "E2E test app for the minimum nuxt 3 version our nuxt SDK supports.", + "description": "E2E test app for the minimum Nuxt 3 version our Nuxt SDK supports.", "private": true, "type": "module", "scripts": { @@ -16,7 +16,7 @@ }, "dependencies": { "@sentry/nuxt": "latest || *", - "nuxt": "3.13.2" + "nuxt": "3.7.0" }, "devDependencies": { "@nuxt/test-utils": "^3.14.1", @@ -24,7 +24,7 @@ "@sentry-internal/test-utils": "link:../../../test-utils" }, "overrides": { - "nitropack": "2.9.7", - "@vercel/nft": "^0.27.4" + "nitropack": "2.10.0", + "ofetch": "1.4.0" } } diff --git a/packages/nuxt/package.json b/packages/nuxt/package.json index df5f2b285ddd..225517786c03 100644 --- a/packages/nuxt/package.json +++ b/packages/nuxt/package.json @@ -39,7 +39,7 @@ "access": "public" }, "peerDependencies": { - "nuxt": "3.x" + "nuxt": ">=3.7.0 || 4.x" }, "dependencies": { "@nuxt/kit": "^3.13.2", diff --git a/packages/nuxt/src/module.ts b/packages/nuxt/src/module.ts index 5a684998da5a..bd6cb96122de 100644 --- a/packages/nuxt/src/module.ts +++ b/packages/nuxt/src/module.ts @@ -13,7 +13,7 @@ export default defineNuxtModule({ name: '@sentry/nuxt/module', configKey: 'sentry', compatibility: { - nuxt: '^3.0.0', + nuxt: '>=3.7.0', }, }, defaults: {}, From 09a31d11976462154444cccc6fb6dc74f777103d Mon Sep 17 00:00:00 2001 From: Luca Forstner Date: Wed, 27 Nov 2024 08:54:44 +0100 Subject: [PATCH 29/34] feat(node): Add `openTelemetryInstrumentations` option (#14484) --- packages/node/src/sdk/client.ts | 7 +++++++ packages/node/src/types.ts | 10 +++++++++- packages/node/test/sdk/client.test.ts | 16 ++++++++++++++++ 3 files changed, 32 insertions(+), 1 deletion(-) diff --git a/packages/node/src/sdk/client.ts b/packages/node/src/sdk/client.ts index b4730ac0f07c..f8a775c15600 100644 --- a/packages/node/src/sdk/client.ts +++ b/packages/node/src/sdk/client.ts @@ -1,6 +1,7 @@ import * as os from 'node:os'; import type { Tracer } from '@opentelemetry/api'; import { trace } from '@opentelemetry/api'; +import { registerInstrumentations } from '@opentelemetry/instrumentation'; import type { BasicTracerProvider } from '@opentelemetry/sdk-trace-base'; import type { Scope, ServerRuntimeClientOptions } from '@sentry/core'; import { SDK_VERSION, ServerRuntimeClient, applySdkMetadata, logger } from '@sentry/core'; @@ -27,6 +28,12 @@ export class NodeClient extends ServerRuntimeClient { serverName: options.serverName || global.process.env.SENTRY_NAME || os.hostname(), }; + if (options.openTelemetryInstrumentations) { + registerInstrumentations({ + instrumentations: options.openTelemetryInstrumentations, + }); + } + applySdkMetadata(clientOptions, 'node'); logger.log( diff --git a/packages/node/src/types.ts b/packages/node/src/types.ts index 7235e2057c34..15df14d59847 100644 --- a/packages/node/src/types.ts +++ b/packages/node/src/types.ts @@ -1,4 +1,5 @@ import type { Span as WriteableSpan } from '@opentelemetry/api'; +import type { Instrumentation } from '@opentelemetry/instrumentation'; import type { ReadableSpan } from '@opentelemetry/sdk-trace-base'; import type { ClientOptions, Options, SamplingContext, Scope, Span, TracePropagationTargets } from '@sentry/types'; @@ -90,6 +91,13 @@ export interface BaseNodeOptions { */ skipOpenTelemetrySetup?: boolean; + /** + * Provide an array of OpenTelemetry Instrumentations that should be registered. + * + * Use this option if you want to register OpenTelemetry instrumentation that the Sentry SDK does not yet have support for. + */ + openTelemetryInstrumentations?: Instrumentation[]; + /** * The max. duration in seconds that the SDK will wait for parent spans to be finished before discarding a span. * The SDK will automatically clean up spans that have no finished parent after this duration. @@ -156,7 +164,7 @@ export interface CurrentScopes { * The base `Span` type is basically a `WriteableSpan`. * There are places where we basically want to allow passing _any_ span, * so in these cases we type this as `AbstractSpan` which could be either a regular `Span` or a `ReadableSpan`. - * You'll have to make sur to check revelant fields before accessing them. + * You'll have to make sur to check relevant fields before accessing them. * * Note that technically, the `Span` exported from `@opentelemetry/sdk-trace-base` matches this, * but we cannot be 100% sure that we are actually getting such a span, so this type is more defensive. diff --git a/packages/node/test/sdk/client.test.ts b/packages/node/test/sdk/client.test.ts index 1aa4cf14fdff..8f85c375592f 100644 --- a/packages/node/test/sdk/client.test.ts +++ b/packages/node/test/sdk/client.test.ts @@ -1,5 +1,6 @@ import * as os from 'os'; import { ProxyTracer } from '@opentelemetry/api'; +import * as opentelemetryInstrumentationPackage from '@opentelemetry/instrumentation'; import { SDK_VERSION, SessionFlusher, @@ -495,6 +496,21 @@ describe('NodeClient', () => { expect(sendEnvelopeSpy).toHaveBeenCalledTimes(0); }); }); + + it('registers instrumentations provided with `openTelemetryInstrumentations`', () => { + const registerInstrumentationsSpy = jest + .spyOn(opentelemetryInstrumentationPackage, 'registerInstrumentations') + .mockImplementationOnce(() => () => undefined); + const instrumentationsArray = ['foobar'] as unknown as opentelemetryInstrumentationPackage.Instrumentation[]; + + new NodeClient(getDefaultNodeClientOptions({ openTelemetryInstrumentations: instrumentationsArray })); + + expect(registerInstrumentationsSpy).toHaveBeenCalledWith( + expect.objectContaining({ + instrumentations: instrumentationsArray, + }), + ); + }); }); describe('flush/close', () => { From de65590b0e6fff42548be9b07b67df0067eb444d Mon Sep 17 00:00:00 2001 From: Tim Fish Date: Wed, 27 Nov 2024 00:17:27 -0800 Subject: [PATCH 30/34] feat(core): Further optimize debug ID parsing (#14365) --- packages/core/src/utils-hoist/debug-ids.ts | 63 +++++++++++++-------- packages/core/src/utils/prepareEvent.ts | 2 +- packages/node/src/integrations/anr/index.ts | 16 +----- 3 files changed, 42 insertions(+), 39 deletions(-) diff --git a/packages/core/src/utils-hoist/debug-ids.ts b/packages/core/src/utils-hoist/debug-ids.ts index 4802b9356965..d407e6176e45 100644 --- a/packages/core/src/utils-hoist/debug-ids.ts +++ b/packages/core/src/utils-hoist/debug-ids.ts @@ -1,7 +1,12 @@ -import type { DebugImage, StackFrame, StackParser } from '@sentry/types'; +import type { DebugImage, StackParser } from '@sentry/types'; import { GLOBAL_OBJ } from './worldwide'; -const debugIdStackParserCache = new WeakMap>(); +type StackString = string; +type CachedResult = [string, string]; + +let parsedStackResults: Record | undefined; +let lastKeysCount: number | undefined; +let cachedFilenameDebugIds: Record | undefined; /** * Returns a map of filenames to debug identifiers. @@ -12,38 +17,46 @@ export function getFilenameToDebugIdMap(stackParser: StackParser): Record; - const cachedDebugIdStackFrameCache = debugIdStackParserCache.get(stackParser); - if (cachedDebugIdStackFrameCache) { - debugIdStackFramesCache = cachedDebugIdStackFrameCache; - } else { - debugIdStackFramesCache = new Map(); - debugIdStackParserCache.set(stackParser, debugIdStackFramesCache); + const debugIdKeys = Object.keys(debugIdMap); + + // If the count of registered globals hasn't changed since the last call, we + // can just return the cached result. + if (cachedFilenameDebugIds && debugIdKeys.length === lastKeysCount) { + return cachedFilenameDebugIds; } + lastKeysCount = debugIdKeys.length; + // Build a map of filename -> debug_id. - return Object.keys(debugIdMap).reduce>((acc, debugIdStackTrace) => { - let parsedStack: StackFrame[]; + cachedFilenameDebugIds = debugIdKeys.reduce>((acc, stackKey) => { + if (!parsedStackResults) { + parsedStackResults = {}; + } + + const result = parsedStackResults[stackKey]; - const cachedParsedStack = debugIdStackFramesCache.get(debugIdStackTrace); - if (cachedParsedStack) { - parsedStack = cachedParsedStack; + if (result) { + acc[result[0]] = result[1]; } else { - parsedStack = stackParser(debugIdStackTrace); - debugIdStackFramesCache.set(debugIdStackTrace, parsedStack); - } + const parsedStack = stackParser(stackKey); - for (let i = parsedStack.length - 1; i >= 0; i--) { - const stackFrame = parsedStack[i]; - const file = stackFrame && stackFrame.filename; + for (let i = parsedStack.length - 1; i >= 0; i--) { + const stackFrame = parsedStack[i]; + const filename = stackFrame && stackFrame.filename; + const debugId = debugIdMap[stackKey]; - if (stackFrame && file) { - acc[file] = debugIdMap[debugIdStackTrace] as string; - break; + if (filename && debugId) { + acc[filename] = debugId; + parsedStackResults[stackKey] = [filename, debugId]; + break; + } } } + return acc; }, {}); + + return cachedFilenameDebugIds; } /** @@ -55,6 +68,10 @@ export function getDebugImagesForResources( ): DebugImage[] { const filenameDebugIdMap = getFilenameToDebugIdMap(stackParser); + if (!filenameDebugIdMap) { + return []; + } + const images: DebugImage[] = []; for (const path of resource_paths) { if (path && filenameDebugIdMap[path]) { diff --git a/packages/core/src/utils/prepareEvent.ts b/packages/core/src/utils/prepareEvent.ts index 98f64adbdd62..14358d4e3101 100644 --- a/packages/core/src/utils/prepareEvent.ts +++ b/packages/core/src/utils/prepareEvent.ts @@ -179,7 +179,7 @@ export function applyDebugIds(event: Event, stackParser: StackParser): void { event!.exception!.values!.forEach(exception => { // eslint-disable-next-line @typescript-eslint/no-non-null-assertion exception.stacktrace!.frames!.forEach(frame => { - if (frame.filename) { + if (filenameDebugIdMap && frame.filename) { frame.debug_id = filenameDebugIdMap[frame.filename]; } }); diff --git a/packages/node/src/integrations/anr/index.ts b/packages/node/src/integrations/anr/index.ts index 1fa2218d5879..0777940cc211 100644 --- a/packages/node/src/integrations/anr/index.ts +++ b/packages/node/src/integrations/anr/index.ts @@ -1,4 +1,3 @@ -import * as diagnosticsChannel from 'node:diagnostics_channel'; import { Worker } from 'node:worker_threads'; import { defineIntegration, getCurrentScope, getGlobalScope, getIsolationScope, mergeScopeData } from '@sentry/core'; import { GLOBAL_OBJ, getFilenameToDebugIdMap, logger } from '@sentry/core'; @@ -101,13 +100,6 @@ type AnrReturn = (options?: Partial) => Integration & Anr export const anrIntegration = defineIntegration(_anrIntegration) as AnrReturn; -function onModuleLoad(callback: () => void): void { - // eslint-disable-next-line deprecation/deprecation - diagnosticsChannel.channel('module.require.end').subscribe(() => callback()); - // eslint-disable-next-line deprecation/deprecation - diagnosticsChannel.channel('module.import.asyncEnd').subscribe(() => callback()); -} - /** * Starts the ANR worker thread * @@ -161,12 +153,6 @@ async function _startWorker( } } - let debugImages: Record = getFilenameToDebugIdMap(initOptions.stackParser); - - onModuleLoad(() => { - debugImages = getFilenameToDebugIdMap(initOptions.stackParser); - }); - const worker = new Worker(new URL(`data:application/javascript;base64,${base64WorkerScript}`), { workerData: options, // We don't want any Node args to be passed to the worker @@ -185,7 +171,7 @@ async function _startWorker( // 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, debugImages }); + worker.postMessage({ session, debugImages: getFilenameToDebugIdMap(initOptions.stackParser) }); } catch (_) { // } From 1e0cb046f029085b09d0f3193a325f4adeaec3f4 Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Wed, 27 Nov 2024 09:57:19 +0100 Subject: [PATCH 31/34] ref(core): Do not check baggage validity (#14479) This PR drops the validation for baggage content. We didn't do this for browser previously, only for node, but it adds bundle size and does not appear too important. I left the trace header validation in for now, we may also drop this but it is smaller and I guess also more important to us...? --- packages/core/src/utils/traceData.ts | 29 +------- .../core/test/lib/utils/traceData.test.ts | 73 ------------------- 2 files changed, 1 insertion(+), 101 deletions(-) diff --git a/packages/core/src/utils/traceData.ts b/packages/core/src/utils/traceData.ts index bd93948d7953..4892e558661a 100644 --- a/packages/core/src/utils/traceData.ts +++ b/packages/core/src/utils/traceData.ts @@ -44,39 +44,12 @@ export function getTraceData(options: { span?: Span } = {}): SerializedTraceData return {}; } - const validBaggage = isValidBaggageString(baggage); - if (!validBaggage) { - logger.warn('Invalid baggage data. Not returning "baggage" value'); - } - return { 'sentry-trace': sentryTrace, - ...(validBaggage && { baggage }), + baggage, }; } -/** - * Tests string against baggage spec as defined in: - * - * - W3C Baggage grammar: https://www.w3.org/TR/baggage/#definition - * - RFC7230 token definition: https://datatracker.ietf.org/doc/html/rfc7230#section-3.2.6 - * - * exported for testing - */ -export function isValidBaggageString(baggage?: string): boolean { - if (!baggage || !baggage.length) { - return false; - } - const keyRegex = "[-!#$%&'*+.^_`|~A-Za-z0-9]+"; - const valueRegex = '[!#-+-./0-9:<=>?@A-Z\\[\\]a-z{-}]+'; - const spaces = '\\s*'; - // eslint-disable-next-line @sentry-internal/sdk/no-regexp-constructor -- RegExp for readability, no user input - const baggageRegex = new RegExp( - `^${keyRegex}${spaces}=${spaces}${valueRegex}(${spaces},${spaces}${keyRegex}${spaces}=${spaces}${valueRegex})*$`, - ); - return baggageRegex.test(baggage); -} - /** * Get a sentry-trace header value for the given scope. */ diff --git a/packages/core/test/lib/utils/traceData.test.ts b/packages/core/test/lib/utils/traceData.test.ts index c6ef21101830..d73ab091d8eb 100644 --- a/packages/core/test/lib/utils/traceData.test.ts +++ b/packages/core/test/lib/utils/traceData.test.ts @@ -13,7 +13,6 @@ import { import { getAsyncContextStrategy } from '../../../src/asyncContext'; import { freezeDscOnSpan } from '../../../src/tracing/dynamicSamplingContext'; -import { isValidBaggageString } from '../../../src/utils/traceData'; import type { TestClientOptions } from '../../mocks/client'; import { TestClient, getDefaultTestClientOptions } from '../../mocks/client'; @@ -281,75 +280,3 @@ describe('getTraceData', () => { expect(traceData).toEqual({}); }); }); - -describe('isValidBaggageString', () => { - it.each([ - 'sentry-environment=production', - 'sentry-environment=staging,sentry-public_key=key,sentry-trace_id=abc', - // @ is allowed in values - 'sentry-release=project@1.0.0', - // spaces are allowed around the delimiters - 'sentry-environment=staging , sentry-public_key=key ,sentry-release=myproject@1.0.0', - 'sentry-environment=staging , thirdparty=value ,sentry-release=myproject@1.0.0', - // these characters are explicitly allowed for keys in the baggage spec: - "!#$%&'*+-.^_`|~1234567890abcxyzABCXYZ=true", - // special characters in values are fine (except for ",;\ - see other test) - 'key=(value)', - 'key=[{(value)}]', - 'key=some$value', - 'key=more#value', - 'key=max&value', - 'key=max:value', - 'key=x=value', - ])('returns true if the baggage string is valid (%s)', baggageString => { - expect(isValidBaggageString(baggageString)).toBe(true); - }); - - it.each([ - // baggage spec doesn't permit leading spaces - ' sentry-environment=production,sentry-publickey=key,sentry-trace_id=abc', - // no spaces in keys or values - 'sentry-public key=key', - 'sentry-publickey=my key', - // no delimiters ("(),/:;<=>?@[\]{}") in keys - 'asdf(x=value', - 'asdf)x=value', - 'asdf,x=value', - 'asdf/x=value', - 'asdf:x=value', - 'asdf;x=value', - 'asdfx=value', - 'asdf?x=value', - 'asdf@x=value', - 'asdf[x=value', - 'asdf]x=value', - 'asdf\\x=value', - 'asdf{x=value', - 'asdf}x=value', - // no ,;\" in values - 'key=va,lue', - 'key=va;lue', - 'key=va\\lue', - 'key=va"lue"', - // baggage headers can have properties but we currently don't support them - 'sentry-environment=production;prop1=foo;prop2=bar,nextkey=value', - // no fishy stuff - 'absolutely not a valid baggage string', - 'val"/>', - 'something"/>', - '', - '/>', - '" onblur="alert("xss")', - ])('returns false if the baggage string is invalid (%s)', baggageString => { - expect(isValidBaggageString(baggageString)).toBe(false); - }); - - it('returns false if the baggage string is empty', () => { - expect(isValidBaggageString('')).toBe(false); - }); - - it('returns false if the baggage string is empty', () => { - expect(isValidBaggageString(undefined)).toBe(false); - }); -}); From 3e7969fba45442017b1232533723b490c2072a49 Mon Sep 17 00:00:00 2001 From: Luca Forstner Date: Wed, 27 Nov 2024 10:36:41 +0100 Subject: [PATCH 32/34] feat: Deprecate `registerEsmLoaderHooks.include` and `registerEsmLoaderHooks.exclude` (#14486) --- .../suites/esm/warn-esm/test.ts | 2 +- docs/migration/draft-v9-migration-guide.md | 2 ++ packages/node/src/sdk/initOtel.ts | 4 ++- packages/node/src/types.ts | 31 +++++++++++++++++-- packages/nuxt/src/server/sdk.ts | 7 +++-- 5 files changed, 40 insertions(+), 6 deletions(-) diff --git a/dev-packages/node-integration-tests/suites/esm/warn-esm/test.ts b/dev-packages/node-integration-tests/suites/esm/warn-esm/test.ts index 25f90460e2b9..f8d752497d46 100644 --- a/dev-packages/node-integration-tests/suites/esm/warn-esm/test.ts +++ b/dev-packages/node-integration-tests/suites/esm/warn-esm/test.ts @@ -5,7 +5,7 @@ afterAll(() => { }); const esmWarning = - '[Sentry] You are using Node.js in ESM mode ("import syntax"). The Sentry Node.js SDK is not compatible with ESM in Node.js versions before 18.19.0 or before 20.6.0. Please either build your application with CommonJS ("require() syntax"), or use version 7.x of the Sentry Node.js SDK.'; + '[Sentry] You are using Node.js in ESM mode ("import syntax"). The Sentry Node.js SDK is not compatible with ESM in Node.js versions before 18.19.0 or before 20.6.0. Please either build your application with CommonJS ("require() syntax"), or upgrade your Node.js version.'; test("warns if using ESM on Node.js versions that don't support `register()`", async () => { const nodeMajorVersion = Number(process.versions.node.split('.')[0]); diff --git a/docs/migration/draft-v9-migration-guide.md b/docs/migration/draft-v9-migration-guide.md index 967fa0d5c6bb..4d289eb56204 100644 --- a/docs/migration/draft-v9-migration-guide.md +++ b/docs/migration/draft-v9-migration-guide.md @@ -90,3 +90,5 @@ - Deprecated `processThreadBreadcrumbIntegration` in favor of `childProcessIntegration`. Functionally they are the same. - Deprecated `nestIntegration`. Use the NestJS SDK (`@sentry/nestjs`) instead. - Deprecated `setupNestErrorHandler`. Use the NestJS SDK (`@sentry/nestjs`) instead. +- Deprecated `registerEsmLoaderHooks.include` and `registerEsmLoaderHooks.exclude`. Set `onlyIncludeInstrumentedModules: true` instead. +- `registerEsmLoaderHooks` will only accept `true | false | undefined` in the future. The SDK will default to wrapping modules that are used as part of OpenTelemetry Instrumentation. diff --git a/packages/node/src/sdk/initOtel.ts b/packages/node/src/sdk/initOtel.ts index fd37e5ef477f..b731ecd8a332 100644 --- a/packages/node/src/sdk/initOtel.ts +++ b/packages/node/src/sdk/initOtel.ts @@ -42,10 +42,12 @@ interface RegisterOptions { } function getRegisterOptions(esmHookConfig?: EsmLoaderHookOptions): RegisterOptions { + // TODO(v9): Make onlyIncludeInstrumentedModules: true the default behavior. if (esmHookConfig?.onlyIncludeInstrumentedModules) { const { addHookMessagePort } = createAddHookMessageChannel(); // If the user supplied include, we need to use that as a starting point or use an empty array to ensure no modules // are wrapped if they are not hooked + // eslint-disable-next-line deprecation/deprecation return { data: { addHookMessagePort, include: esmHookConfig.include || [] }, transferList: [addHookMessagePort] }; } @@ -75,7 +77,7 @@ export function maybeInitializeEsmLoader(esmHookConfig?: EsmLoaderHookOptions): consoleSandbox(() => { // eslint-disable-next-line no-console console.warn( - '[Sentry] You are using Node.js in ESM mode ("import syntax"). The Sentry Node.js SDK is not compatible with ESM in Node.js versions before 18.19.0 or before 20.6.0. Please either build your application with CommonJS ("require() syntax"), or use version 7.x of the Sentry Node.js SDK.', + '[Sentry] You are using Node.js in ESM mode ("import syntax"). The Sentry Node.js SDK is not compatible with ESM in Node.js versions before 18.19.0 or before 20.6.0. Please either build your application with CommonJS ("require() syntax"), or upgrade your Node.js version.', ); }); } diff --git a/packages/node/src/types.ts b/packages/node/src/types.ts index 15df14d59847..512e6c164727 100644 --- a/packages/node/src/types.ts +++ b/packages/node/src/types.ts @@ -5,9 +5,26 @@ import type { ClientOptions, Options, SamplingContext, Scope, Span, TracePropaga import type { NodeTransportOptions } from './transports'; +/** + * Note: In the next major version of the Sentry SDK this interface will be removed and the SDK will by default only wrap + * ESM modules that are required to be wrapped by OpenTelemetry Instrumentation. + */ export interface EsmLoaderHookOptions { + /** + * Provide a list of modules to wrap with `import-in-the-middle`. + * + * @deprecated It is recommended to use `onlyIncludeInstrumentedModules: true` instead of manually defining modules to include and exclude. + */ include?: Array; - exclude?: Array /** + + /** + * Provide a list of modules to prevent them from being wrapped with `import-in-the-middle`. + * + * @deprecated It is recommended to use `onlyIncludeInstrumentedModules: true` instead of manually defining modules to include and exclude. + */ + exclude?: Array; + + /** * When set to `true`, `import-in-the-middle` will only wrap ESM modules that are specifically instrumented by * OpenTelemetry plugins. This is useful to avoid issues where `import-in-the-middle` is not compatible with some of * your dependencies. @@ -17,7 +34,11 @@ export interface EsmLoaderHookOptions { * `Sentry.init()`. * * Defaults to `false`. - */; + * + * Note: In the next major version of the Sentry SDK this option will be removed and the SDK will by default only wrap + * ESM modules that are required to be wrapped by OpenTelemetry Instrumentation. + */ + // TODO(v9): Make `onlyIncludeInstrumentedModules: true` the default behavior. onlyIncludeInstrumentedModules?: boolean; } @@ -88,6 +109,8 @@ export interface BaseNodeOptions { * * The `SentryPropagator` * * The `SentryContextManager` * * The `SentrySampler` + * + * If you are registering your own OpenTelemetry Loader Hooks (or `import-in-the-middle` hooks), it is also recommended to set the `registerEsmLoaderHooks` option to false. */ skipOpenTelemetrySetup?: boolean; @@ -125,7 +148,11 @@ export interface BaseNodeOptions { * ``` * * Defaults to `true`. + * + * Note: In the next major version of the SDK, the possibility to provide fine-grained control will be removed from this option. + * This means that it will only be possible to pass `true` or `false`. The default value will continue to be `true`. */ + // TODO(v9): Only accept true | false | undefined. registerEsmLoaderHooks?: boolean | EsmLoaderHookOptions; /** diff --git a/packages/nuxt/src/server/sdk.ts b/packages/nuxt/src/server/sdk.ts index 6c4dd4712a1a..ccef93ea3350 100644 --- a/packages/nuxt/src/server/sdk.ts +++ b/packages/nuxt/src/server/sdk.ts @@ -100,9 +100,12 @@ export function mergeRegisterEsmLoaderHooks( ): SentryNuxtServerOptions['registerEsmLoaderHooks'] { if (typeof options.registerEsmLoaderHooks === 'object' && options.registerEsmLoaderHooks !== null) { return { + // eslint-disable-next-line deprecation/deprecation exclude: Array.isArray(options.registerEsmLoaderHooks.exclude) - ? [...options.registerEsmLoaderHooks.exclude, /vue/] - : options.registerEsmLoaderHooks.exclude ?? [/vue/], + ? // eslint-disable-next-line deprecation/deprecation + [...options.registerEsmLoaderHooks.exclude, /vue/] + : // eslint-disable-next-line deprecation/deprecation + options.registerEsmLoaderHooks.exclude ?? [/vue/], }; } return options.registerEsmLoaderHooks ?? { exclude: [/vue/] }; From a4138e93ca563ec50f6e969a903327063fce3e86 Mon Sep 17 00:00:00 2001 From: Fabio Moretti Date: Wed, 27 Nov 2024 11:51:08 +0100 Subject: [PATCH 33/34] perf(opentelemetry): Bucket spans for cleanup (#14154) Co-authored-by: Luca Forstner --- .../test/integration/transactions.test.ts | 35 ++-- packages/opentelemetry/src/spanExporter.ts | 162 +++++++++++------- .../test/integration/transactions.test.ts | 53 +++--- 3 files changed, 151 insertions(+), 99 deletions(-) diff --git a/packages/node/test/integration/transactions.test.ts b/packages/node/test/integration/transactions.test.ts index e13d239821d3..a9d524ea0285 100644 --- a/packages/node/test/integration/transactions.test.ts +++ b/packages/node/test/integration/transactions.test.ts @@ -577,17 +577,9 @@ describe('Integration | Transactions', () => { throw new Error('No exporter found, aborting test...'); } - let innerSpan1Id: string | undefined; - let innerSpan2Id: string | undefined; - void Sentry.startSpan({ name: 'test name' }, async () => { - const subSpan = Sentry.startInactiveSpan({ name: 'inner span 1' }); - innerSpan1Id = subSpan.spanContext().spanId; - subSpan.end(); - - Sentry.startSpan({ name: 'inner span 2' }, innerSpan => { - innerSpan2Id = innerSpan.spanContext().spanId; - }); + Sentry.startInactiveSpan({ name: 'inner span 1' }).end(); + Sentry.startInactiveSpan({ name: 'inner span 2' }).end(); // Pretend this is pending for 10 minutes await new Promise(resolve => setTimeout(resolve, 10 * 60 * 1000)); @@ -596,7 +588,13 @@ describe('Integration | Transactions', () => { jest.advanceTimersByTime(1); // Child-spans have been added to the exporter, but they are pending since they are waiting for their parent - expect(exporter['_finishedSpans'].length).toBe(2); + const finishedSpans1 = []; + exporter['_finishedSpanBuckets'].forEach((bucket: any) => { + if (bucket) { + finishedSpans1.push(...bucket.spans); + } + }); + expect(finishedSpans1.length).toBe(2); expect(beforeSendTransaction).toHaveBeenCalledTimes(0); // Now wait for 5 mins @@ -608,18 +606,21 @@ describe('Integration | Transactions', () => { jest.advanceTimersByTime(1); // Old spans have been cleared away - expect(exporter['_finishedSpans'].length).toBe(0); + const finishedSpans2 = []; + exporter['_finishedSpanBuckets'].forEach((bucket: any) => { + if (bucket) { + finishedSpans2.push(...bucket.spans); + } + }); + expect(finishedSpans2.length).toBe(0); // Called once for the 'other span' expect(beforeSendTransaction).toHaveBeenCalledTimes(1); expect(logs).toEqual( expect.arrayContaining([ - 'SpanExporter has 1 unsent spans remaining', - 'SpanExporter has 2 unsent spans remaining', - 'SpanExporter exported 1 spans, 2 unsent spans remaining', - `SpanExporter dropping span inner span 1 (${innerSpan1Id}) because it is pending for more than 5 minutes.`, - `SpanExporter dropping span inner span 2 (${innerSpan2Id}) because it is pending for more than 5 minutes.`, + 'SpanExporter dropped 2 spans because they were pending for more than 300 seconds.', + 'SpanExporter exported 1 spans, 0 unsent spans remaining', ]), ); }); diff --git a/packages/opentelemetry/src/spanExporter.ts b/packages/opentelemetry/src/spanExporter.ts index 11d822f66ec1..91a8390100df 100644 --- a/packages/opentelemetry/src/spanExporter.ts +++ b/packages/opentelemetry/src/spanExporter.ts @@ -35,60 +35,121 @@ type SpanNodeCompleted = SpanNode & { span: ReadableSpan }; const MAX_SPAN_COUNT = 1000; const DEFAULT_TIMEOUT = 300; // 5 min +interface FinishedSpanBucket { + timestampInS: number; + spans: Set; +} + /** * A Sentry-specific exporter that converts OpenTelemetry Spans to Sentry Spans & Transactions. */ export class SentrySpanExporter { private _flushTimeout: ReturnType | undefined; - private _finishedSpans: ReadableSpan[]; - private _timeout: number; - public constructor(options?: { timeout?: number }) { - this._finishedSpans = []; - this._timeout = options?.timeout || DEFAULT_TIMEOUT; + /* + * A quick explanation on the buckets: We do bucketing of finished spans for efficiency. This span exporter is + * accumulating spans until a root span is encountered and then it flushes all the spans that are descendants of that + * root span. Because it is totally in the realm of possibilities that root spans are never finished, and we don't + * want to accumulate spans indefinitely in memory, we need to periodically evacuate spans. Naively we could simply + * store the spans in an array and each time a new span comes in we could iterate through the entire array and + * evacuate all spans that have an end-timestamp that is older than our limit. This could get quite expensive because + * we would have to iterate a potentially large number of spans every time we evacuate. We want to avoid these large + * bursts of computation. + * + * Instead we go for a bucketing approach and put spans into buckets, based on what second + * (modulo the time limit) the span was put into the exporter. With buckets, when we decide to evacuate, we can + * iterate through the bucket entries instead, which have an upper bound of items, making the evacuation much more + * efficient. Cleaning up also becomes much more efficient since it simply involves de-referencing a bucket within the + * bucket array, and letting garbage collection take care of the rest. + */ + private _finishedSpanBuckets: (FinishedSpanBucket | undefined)[]; + private _finishedSpanBucketSize: number; + private _spansToBucketEntry: WeakMap; + private _lastCleanupTimestampInS: number; + + public constructor(options?: { + /** Lower bound of time in seconds until spans that are buffered but have not been sent as part of a transaction get cleared from memory. */ + timeout?: number; + }) { + this._finishedSpanBucketSize = options?.timeout || DEFAULT_TIMEOUT; + this._finishedSpanBuckets = new Array(this._finishedSpanBucketSize).fill(undefined); + this._lastCleanupTimestampInS = Math.floor(Date.now() / 1000); + this._spansToBucketEntry = new WeakMap(); } /** Export a single span. */ public export(span: ReadableSpan): void { - this._finishedSpans.push(span); - - // If the span has a local parent ID, we don't need to export anything just yet - if (getLocalParentId(span)) { - const openSpanCount = this._finishedSpans.length; - DEBUG_BUILD && logger.log(`SpanExporter has ${openSpanCount} unsent spans remaining`); - this._cleanupOldSpans(); - return; + const currentTimestampInS = Math.floor(Date.now() / 1000); + + if (this._lastCleanupTimestampInS !== currentTimestampInS) { + let droppedSpanCount = 0; + this._finishedSpanBuckets.forEach((bucket, i) => { + if (bucket && bucket.timestampInS <= currentTimestampInS - this._finishedSpanBucketSize) { + droppedSpanCount += bucket.spans.size; + this._finishedSpanBuckets[i] = undefined; + } + }); + if (droppedSpanCount > 0) { + DEBUG_BUILD && + logger.log( + `SpanExporter dropped ${droppedSpanCount} spans because they were pending for more than ${this._finishedSpanBucketSize} seconds.`, + ); + } + this._lastCleanupTimestampInS = currentTimestampInS; } - this._clearTimeout(); - - // If we got a parent span, we try to send the span tree - // Wait a tick for this, to ensure we avoid race conditions - this._flushTimeout = setTimeout(() => { - this.flush(); - }, 1); + const currentBucketIndex = currentTimestampInS % this._finishedSpanBucketSize; + const currentBucket = this._finishedSpanBuckets[currentBucketIndex] || { + timestampInS: currentTimestampInS, + spans: new Set(), + }; + this._finishedSpanBuckets[currentBucketIndex] = currentBucket; + currentBucket.spans.add(span); + this._spansToBucketEntry.set(span, currentBucket); + + // If the span doesn't have a local parent ID (it's a root span), we're gonna flush all the ended spans + if (!getLocalParentId(span)) { + this._clearTimeout(); + + // If we got a parent span, we try to send the span tree + // Wait a tick for this, to ensure we avoid race conditions + this._flushTimeout = setTimeout(() => { + this.flush(); + }, 1); + } } /** Try to flush any pending spans immediately. */ public flush(): void { this._clearTimeout(); - const openSpanCount = this._finishedSpans.length; + const finishedSpans: ReadableSpan[] = []; + this._finishedSpanBuckets.forEach(bucket => { + if (bucket) { + finishedSpans.push(...bucket.spans); + } + }); + + const sentSpans = maybeSend(finishedSpans); - const remainingSpans = maybeSend(this._finishedSpans); + const sentSpanCount = sentSpans.size; - const remainingOpenSpanCount = remainingSpans.length; - const sentSpanCount = openSpanCount - remainingOpenSpanCount; + const remainingOpenSpanCount = finishedSpans.length - sentSpanCount; DEBUG_BUILD && logger.log(`SpanExporter exported ${sentSpanCount} spans, ${remainingOpenSpanCount} unsent spans remaining`); - this._cleanupOldSpans(remainingSpans); + sentSpans.forEach(span => { + const bucketEntry = this._spansToBucketEntry.get(span); + if (bucketEntry) { + bucketEntry.spans.delete(span); + } + }); } /** Clear the exporter. */ public clear(): void { - this._finishedSpans = []; + this._finishedSpanBuckets = this._finishedSpanBuckets.fill(undefined); this._clearTimeout(); } @@ -99,52 +160,33 @@ export class SentrySpanExporter { this._flushTimeout = undefined; } } - - /** - * Remove any span that is older than 5min. - * We do this to avoid leaking memory. - */ - private _cleanupOldSpans(spans = this._finishedSpans): void { - const currentTimeSeconds = Date.now() / 1000; - this._finishedSpans = spans.filter(span => { - const shouldDrop = shouldCleanupSpan(span, currentTimeSeconds, this._timeout); - DEBUG_BUILD && - shouldDrop && - logger.log( - `SpanExporter dropping span ${span.name} (${ - span.spanContext().spanId - }) because it is pending for more than 5 minutes.`, - ); - return !shouldDrop; - }); - } } /** * Send the given spans, but only if they are part of a finished transaction. * - * Returns the unsent spans. + * Returns the sent spans. * Spans remain unsent when their parent span is not yet finished. * This will happen regularly, as child spans are generally finished before their parents. * But it _could_ also happen because, for whatever reason, a parent span was lost. * In this case, we'll eventually need to clean this up. */ -function maybeSend(spans: ReadableSpan[]): ReadableSpan[] { +function maybeSend(spans: ReadableSpan[]): Set { const grouped = groupSpansWithParents(spans); - const remaining = new Set(grouped); + const sentSpans = new Set(); const rootNodes = getCompletedRootNodes(grouped); rootNodes.forEach(root => { - remaining.delete(root); const span = root.span; + sentSpans.add(span); const transactionEvent = createTransactionForOtelSpan(span); // We'll recursively add all the child spans to this array const spans = transactionEvent.spans || []; root.children.forEach(child => { - createAndFinishSpanForOtelSpan(child, spans, remaining); + createAndFinishSpanForOtelSpan(child, spans, sentSpans); }); // spans.sort() mutates the array, but we do not use this anymore after this point @@ -162,9 +204,7 @@ function maybeSend(spans: ReadableSpan[]): ReadableSpan[] { captureEvent(transactionEvent); }); - return Array.from(remaining) - .map(node => node.span) - .filter((span): span is ReadableSpan => !!span); + return sentSpans; } function nodeIsCompletedRootNode(node: SpanNode): node is SpanNodeCompleted { @@ -175,11 +215,6 @@ function getCompletedRootNodes(nodes: SpanNode[]): SpanNodeCompleted[] { return nodes.filter(nodeIsCompletedRootNode); } -function shouldCleanupSpan(span: ReadableSpan, currentTimeSeconds: number, maxStartTimeOffsetSeconds: number): boolean { - const cutoff = currentTimeSeconds - maxStartTimeOffsetSeconds; - return spanTimeInputToSeconds(span.startTime) < cutoff; -} - function parseSpan(span: ReadableSpan): { op?: string; origin?: SpanOrigin; source?: TransactionSource } { const attributes = span.attributes; @@ -260,16 +295,19 @@ function createTransactionForOtelSpan(span: ReadableSpan): TransactionEvent { return transactionEvent; } -function createAndFinishSpanForOtelSpan(node: SpanNode, spans: SpanJSON[], remaining: Set): void { - remaining.delete(node); +function createAndFinishSpanForOtelSpan(node: SpanNode, spans: SpanJSON[], sentSpans: Set): void { const span = node.span; + if (span) { + sentSpans.add(span); + } + const shouldDrop = !span; // If this span should be dropped, we still want to create spans for the children of this if (shouldDrop) { node.children.forEach(child => { - createAndFinishSpanForOtelSpan(child, spans, remaining); + createAndFinishSpanForOtelSpan(child, spans, sentSpans); }); return; } @@ -308,7 +346,7 @@ function createAndFinishSpanForOtelSpan(node: SpanNode, spans: SpanJSON[], remai spans.push(spanJSON); node.children.forEach(child => { - createAndFinishSpanForOtelSpan(child, spans, remaining); + createAndFinishSpanForOtelSpan(child, spans, sentSpans); }); } diff --git a/packages/opentelemetry/test/integration/transactions.test.ts b/packages/opentelemetry/test/integration/transactions.test.ts index b66147a413d7..8dacab4412c0 100644 --- a/packages/opentelemetry/test/integration/transactions.test.ts +++ b/packages/opentelemetry/test/integration/transactions.test.ts @@ -460,24 +460,22 @@ describe('Integration | Transactions', () => { throw new Error('No exporter found, aborting test...'); } - let innerSpan1Id: string | undefined; - let innerSpan2Id: string | undefined; - void startSpan({ name: 'test name' }, async () => { - const subSpan = startInactiveSpan({ name: 'inner span 1' }); - innerSpan1Id = subSpan.spanContext().spanId; - subSpan.end(); - - startSpan({ name: 'inner span 2' }, innerSpan => { - innerSpan2Id = innerSpan.spanContext().spanId; - }); + startInactiveSpan({ name: 'inner span 1' }).end(); + startInactiveSpan({ name: 'inner span 2' }).end(); // Pretend this is pending for 10 minutes await new Promise(resolve => setTimeout(resolve, 10 * 60 * 1000)); }); // Child-spans have been added to the exporter, but they are pending since they are waiting for their parent - expect(exporter['_finishedSpans'].length).toBe(2); + const finishedSpans1 = []; + exporter['_finishedSpanBuckets'].forEach(bucket => { + if (bucket) { + finishedSpans1.push(...bucket.spans); + } + }); + expect(finishedSpans1.length).toBe(2); expect(beforeSendTransaction).toHaveBeenCalledTimes(0); // Now wait for 5 mins @@ -489,18 +487,21 @@ describe('Integration | Transactions', () => { jest.advanceTimersByTime(1); // Old spans have been cleared away - expect(exporter['_finishedSpans'].length).toBe(0); + const finishedSpans2 = []; + exporter['_finishedSpanBuckets'].forEach(bucket => { + if (bucket) { + finishedSpans2.push(...bucket.spans); + } + }); + expect(finishedSpans2.length).toBe(0); // Called once for the 'other span' expect(beforeSendTransaction).toHaveBeenCalledTimes(1); expect(logs).toEqual( expect.arrayContaining([ - 'SpanExporter has 1 unsent spans remaining', - 'SpanExporter has 2 unsent spans remaining', - 'SpanExporter exported 1 spans, 2 unsent spans remaining', - `SpanExporter dropping span inner span 1 (${innerSpan1Id}) because it is pending for more than 5 minutes.`, - `SpanExporter dropping span inner span 2 (${innerSpan2Id}) because it is pending for more than 5 minutes.`, + 'SpanExporter dropped 2 spans because they were pending for more than 300 seconds.', + 'SpanExporter exported 1 spans, 0 unsent spans remaining', ]), ); }); @@ -553,7 +554,13 @@ describe('Integration | Transactions', () => { expect(transactions[0]?.spans).toHaveLength(2); // No spans are pending - expect(exporter['_finishedSpans'].length).toBe(0); + const finishedSpans = []; + exporter['_finishedSpanBuckets'].forEach(bucket => { + if (bucket) { + finishedSpans.push(...bucket.spans); + } + }); + expect(finishedSpans.length).toBe(0); }); it('discards child spans that are finished after their parent span', async () => { @@ -607,8 +614,14 @@ describe('Integration | Transactions', () => { expect(transactions[0]?.spans).toHaveLength(1); // subSpan2 is pending (and will eventually be cleaned up) - expect(exporter['_finishedSpans'].length).toBe(1); - expect(exporter['_finishedSpans'][0]?.name).toBe('inner span 2'); + const finishedSpans: any = []; + exporter['_finishedSpanBuckets'].forEach(bucket => { + if (bucket) { + finishedSpans.push(...bucket.spans); + } + }); + expect(finishedSpans.length).toBe(1); + expect(finishedSpans[0]?.name).toBe('inner span 2'); }); it('uses & inherits DSC on span trace state', async () => { From f7289c4af6538c1ef678c14112910b77278cfcca Mon Sep 17 00:00:00 2001 From: Luca Forstner Date: Wed, 27 Nov 2024 11:04:55 +0000 Subject: [PATCH 34/34] meta(changelog): Update changelog for 8.41.0 --- CHANGELOG.md | 94 +++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 93 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bfacfe82c2a4..09f035ce0315 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,7 +10,99 @@ - "You miss 100 percent of the chances you don't take. — Wayne Gretzky" — Michael Scott -Work in this release was contributed by @NEKOYASAN. Thank you for your contribution! +## 8.41.0 + +### Important Changes + +- **meta(nuxt): Require minimum Nuxt v3.7.0 ([#14473](https://github.com/getsentry/sentry-javascript/pull/14473))** + + We formalized that the Nuxt SDK is at minimum compatible with Nuxt version 3.7.0 and above. + Additionally, the SDK requires the implicit `nitropack` dependency to satisfy version `^2.6.1` and `ofetch` to satisfy `^1.3.3`. + It is recommended to check your lock-files and manually upgrade these dependencies if they don't match the version ranges. + +### Deprecations + +We are deprecating a few APIs which will be removed in the next major. + +The following deprecations will _potentially_ affect you: + +- **feat(core): Update & deprecate `undefined` option handling ([#14450](https://github.com/getsentry/sentry-javascript/pull/14450))** + + In the next major version we will change how passing `undefined` to `tracesSampleRate` / `tracesSampler` / `enableTracing` will behave. + + Currently, doing the following: + + ```ts + Sentry.init({ + tracesSampleRate: undefined, + }); + ``` + + Will result in tracing being _enabled_ (although no spans will be generated) because the `tracesSampleRate` key is present in the options object. + In the next major version, this behavior will be changed so that passing `undefined` (or rather having a `tracesSampleRate` key) will result in tracing being disabled, the same as not passing the option at all. + If you are currently relying on `undefined` being passed, and and thus have tracing enabled, it is recommended to update your config to set e.g. `tracesSampleRate: 0` instead, which will also enable tracing in v9. + + The same applies to `tracesSampler` and `enableTracing`. + +- **feat(core): Log warnings when returning `null` in `beforeSendSpan` ([#14433](https://github.com/getsentry/sentry-javascript/pull/14433))** + + Currently, the `beforeSendSpan` option in `Sentry.init()` allows you to drop individual spans from a trace by returning `null` from the hook. + Since this API lends itself to creating "gaps" inside traces, we decided to change how this API will work in the next major version. + + With the next major version the `beforeSendSpan` API can only be used to mutate spans, but no longer to drop them. + With this release the SDK will warn you if you are using this API to drop spans. + Instead, it is recommended to configure instrumentation (i.e. integrations) directly to control what spans are created. + + Additionally, with the next major version, root spans will also be passed to `beforeSendSpan`. + +- **feat(utils): Deprecate `@sentry/utils` ([#14431](https://github.com/getsentry/sentry-javascript/pull/14431))** + + With the next major version the `@sentry/utils` package will be merged into the `@sentry/core` package. + It is therefore no longer recommended to use the `@sentry/utils` package. + +- **feat(vue): Deprecate configuring Vue tracing options anywhere else other than through the `vueIntegration`'s `tracingOptions` option ([#14385](https://github.com/getsentry/sentry-javascript/pull/14385))** + + Currently it is possible to configure tracing options in various places in the Sentry Vue SDK: + + - In `Sentry.init()` + - Inside `tracingOptions` in `Sentry.init()` + - In the `vueIntegration()` options + - Inside `tracingOptions` in the `vueIntegration()` options + + Because this is a bit messy and confusing to document, the only recommended way to configure tracing options going forward is through the `tracingOptions` in the `vueIntegration()`. + The other means of configuration will be removed in the next major version of the SDK. + +- **feat: Deprecate `registerEsmLoaderHooks.include` and `registerEsmLoaderHooks.exclude` ([#14486](https://github.com/getsentry/sentry-javascript/pull/14486))** + + Currently it is possible to define `registerEsmLoaderHooks.include` and `registerEsmLoaderHooks.exclude` options in `Sentry.init()` to only apply ESM loader hooks to a subset of modules. + This API served as an escape hatch in case certain modules are incompatible with ESM loader hooks. + + Since this API was introduced, a way was found to only wrap modules that there exists instrumentation for (meaning a vetted list). + To only wrap modules that have instrumentation, it is recommended to instead set `registerEsmLoaderHooks.onlyIncludeInstrumentedModules` to `true`. + + Note that `onlyIncludeInstrumentedModules: true` will become the default behavior in the next major version and the `registerEsmLoaderHooks` will no longer accept fine-grained options. + +The following deprecations will _most likely_ not affect you unless you are building an SDK yourself: + +- feat(core): Deprecate `arrayify` ([#14405](https://github.com/getsentry/sentry-javascript/pull/14405)) +- feat(core): Deprecate `flatten` ([#14454](https://github.com/getsentry/sentry-javascript/pull/14454)) +- feat(core): Deprecate `urlEncode` ([#14406](https://github.com/getsentry/sentry-javascript/pull/14406)) +- feat(core): Deprecate `validSeverityLevels` ([#14407](https://github.com/getsentry/sentry-javascript/pull/14407)) +- feat(core/utils): Deprecate `getNumberOfUrlSegments` ([#14458](https://github.com/getsentry/sentry-javascript/pull/14458)) +- feat(utils): Deprecate `memoBuilder`, `BAGGAGE_HEADER_NAME`, and `makeFifoCache` ([#14434](https://github.com/getsentry/sentry-javascript/pull/14434)) +- feat(utils/core): Deprecate `addRequestDataToEvent` and `extractRequestData` ([#14430](https://github.com/getsentry/sentry-javascript/pull/14430)) + +### Other Changes + +- feat: Streamline `sentry-trace`, `baggage` and DSC handling ([#14364](https://github.com/getsentry/sentry-javascript/pull/14364)) +- feat(core): Further optimize debug ID parsing ([#14365](https://github.com/getsentry/sentry-javascript/pull/14365)) +- feat(node): Add `openTelemetryInstrumentations` option ([#14484](https://github.com/getsentry/sentry-javascript/pull/14484)) +- feat(nuxt): Add filter for not found source maps (devtools) ([#14437](https://github.com/getsentry/sentry-javascript/pull/14437)) +- feat(nuxt): Only delete public source maps ([#14438](https://github.com/getsentry/sentry-javascript/pull/14438)) +- fix(nextjs): Don't report `NEXT_REDIRECT` from browser ([#14440](https://github.com/getsentry/sentry-javascript/pull/14440)) +- perf(opentelemetry): Bucket spans for cleanup ([#14154](https://github.com/getsentry/sentry-javascript/pull/14154)) + +Work in this release was contributed by @NEKOYASAN and @fmorett. Thank you for your contributions! ## 8.40.0