From 8e99ba961c6684ba50896dcfab6d5f3dca38d976 Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Wed, 6 Sep 2023 13:53:39 +0200 Subject: [PATCH 01/35] ref(core): Avoid unnecessary breadcrumbs array mutations (#8957) This is a micro improvement, but maybe worth it as we can have a lot of breadcrumbs. This ensures we only create a new breadcrumbs array if we exceed the limit. In contrast, currently we'll always create two copies of the breadcrumbs for each added breadcrumb (first for the array spread, then we copy it through the slice), even if we don't really need to do this. --- packages/core/src/scope.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/core/src/scope.ts b/packages/core/src/scope.ts index 8875f1c996f2..b2342169a510 100644 --- a/packages/core/src/scope.ts +++ b/packages/core/src/scope.ts @@ -419,7 +419,11 @@ export class Scope implements ScopeInterface { timestamp: dateTimestampInSeconds(), ...breadcrumb, }; - this._breadcrumbs = [...this._breadcrumbs, mergedBreadcrumb].slice(-maxCrumbs); + + const breadcrumbs = this._breadcrumbs; + breadcrumbs.push(mergedBreadcrumb); + this._breadcrumbs = breadcrumbs.length > maxCrumbs ? breadcrumbs.slice(-maxCrumbs) : breadcrumbs; + this._notifyScopeListeners(); return this; From 3076e09759b832134bab230f642e618c36ef0e21 Mon Sep 17 00:00:00 2001 From: Luca Forstner Date: Wed, 6 Sep 2023 13:28:16 +0100 Subject: [PATCH 02/35] fix(nextjs): Request for no HSTS in tunnel route endpoint (#8936) --- packages/nextjs/src/config/withSentryConfig.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/nextjs/src/config/withSentryConfig.ts b/packages/nextjs/src/config/withSentryConfig.ts index b58603d82056..06f5808642ca 100644 --- a/packages/nextjs/src/config/withSentryConfig.ts +++ b/packages/nextjs/src/config/withSentryConfig.ts @@ -112,7 +112,7 @@ function setUpTunnelRewriteRules(userNextConfig: NextConfigObject, tunnelPath: s value: '(?.*)', }, ], - destination: 'https://o:orgid.ingest.sentry.io/api/:projectid/envelope/', + destination: 'https://o:orgid.ingest.sentry.io/api/:projectid/envelope/?hsts=0', }; if (typeof originalRewrites !== 'function') { From d3fe4734acb85ca5dbf7d5366e2399bcb92b82d5 Mon Sep 17 00:00:00 2001 From: Tim Fish Date: Wed, 6 Sep 2023 16:52:19 +0200 Subject: [PATCH 03/35] feat(nextjs): Remove `EdgeClient` and use `ServerRuntimeClient` (#8932) This PR removes the `EdgeClient` and duplicate `eventbuilder` functions. --- packages/nextjs/src/edge/edgeclient.ts | 174 ------------------- packages/nextjs/src/edge/eventbuilder.ts | 130 -------------- packages/nextjs/src/edge/index.ts | 30 +++- packages/nextjs/test/edge/edgeclient.test.ts | 57 ------ 4 files changed, 26 insertions(+), 365 deletions(-) delete mode 100644 packages/nextjs/src/edge/edgeclient.ts delete mode 100644 packages/nextjs/src/edge/eventbuilder.ts delete mode 100644 packages/nextjs/test/edge/edgeclient.test.ts diff --git a/packages/nextjs/src/edge/edgeclient.ts b/packages/nextjs/src/edge/edgeclient.ts deleted file mode 100644 index 39b795c81a53..000000000000 --- a/packages/nextjs/src/edge/edgeclient.ts +++ /dev/null @@ -1,174 +0,0 @@ -import type { Scope } from '@sentry/core'; -import { - addTracingExtensions, - BaseClient, - createCheckInEnvelope, - getDynamicSamplingContextFromClient, - SDK_VERSION, -} from '@sentry/core'; -import type { - CheckIn, - ClientOptions, - DynamicSamplingContext, - Event, - EventHint, - MonitorConfig, - SerializedCheckIn, - Severity, - SeverityLevel, - TraceContext, -} from '@sentry/types'; -import { logger, uuid4 } from '@sentry/utils'; - -import { eventFromMessage, eventFromUnknownInput } from './eventbuilder'; -import type { EdgeTransportOptions } from './transport'; - -export type EdgeClientOptions = ClientOptions; - -/** - * The Sentry Edge SDK Client. - */ -export class EdgeClient extends BaseClient { - /** - * Creates a new Edge SDK instance. - * @param options Configuration options for this SDK. - */ - public constructor(options: EdgeClientOptions) { - options._metadata = options._metadata || {}; - options._metadata.sdk = options._metadata.sdk || { - name: 'sentry.javascript.nextjs', - packages: [ - { - name: 'npm:@sentry/nextjs', - version: SDK_VERSION, - }, - ], - version: SDK_VERSION, - }; - - // The Edge client always supports tracing - addTracingExtensions(); - - super(options); - } - - /** - * @inheritDoc - */ - public eventFromException(exception: unknown, hint?: EventHint): PromiseLike { - return Promise.resolve(eventFromUnknownInput(this._options.stackParser, exception, hint)); - } - - /** - * @inheritDoc - */ - public eventFromMessage( - message: string, - // eslint-disable-next-line deprecation/deprecation - level: Severity | SeverityLevel = 'info', - hint?: EventHint, - ): PromiseLike { - return Promise.resolve( - eventFromMessage(this._options.stackParser, message, level, hint, this._options.attachStacktrace), - ); - } - - /** - * Create a cron monitor check in and send it to Sentry. - * - * @param checkIn An object that describes a check in. - * @param upsertMonitorConfig An optional object that describes a monitor config. Use this if you want - * to create a monitor automatically when sending a check in. - */ - public captureCheckIn(checkIn: CheckIn, monitorConfig?: MonitorConfig, scope?: Scope): string { - const id = checkIn.status !== 'in_progress' && checkIn.checkInId ? checkIn.checkInId : uuid4(); - if (!this._isEnabled()) { - __DEBUG_BUILD__ && logger.warn('SDK not enabled, will not capture checkin.'); - return id; - } - - const options = this.getOptions(); - const { release, environment, tunnel } = options; - - const serializedCheckIn: SerializedCheckIn = { - check_in_id: id, - monitor_slug: checkIn.monitorSlug, - status: checkIn.status, - release, - environment, - }; - - if (checkIn.status !== 'in_progress') { - serializedCheckIn.duration = checkIn.duration; - } - - if (monitorConfig) { - serializedCheckIn.monitor_config = { - schedule: monitorConfig.schedule, - checkin_margin: monitorConfig.checkinMargin, - max_runtime: monitorConfig.maxRuntime, - timezone: monitorConfig.timezone, - }; - } - - const [dynamicSamplingContext, traceContext] = this._getTraceInfoFromScope(scope); - if (traceContext) { - serializedCheckIn.contexts = { - trace: traceContext, - }; - } - - const envelope = createCheckInEnvelope( - serializedCheckIn, - dynamicSamplingContext, - this.getSdkMetadata(), - tunnel, - this.getDsn(), - ); - - __DEBUG_BUILD__ && logger.info('Sending checkin:', checkIn.monitorSlug, checkIn.status); - void this._sendEnvelope(envelope); - return id; - } - - /** - * @inheritDoc - */ - protected _prepareEvent(event: Event, hint: EventHint, scope?: Scope): PromiseLike { - event.platform = event.platform || 'edge'; - event.contexts = { - ...event.contexts, - runtime: event.contexts?.runtime || { - name: 'edge', - }, - }; - event.server_name = event.server_name || process.env.SENTRY_NAME; - return super._prepareEvent(event, hint, scope); - } - - /** Extract trace information from scope */ - private _getTraceInfoFromScope( - scope: Scope | undefined, - ): [dynamicSamplingContext: Partial | undefined, traceContext: TraceContext | undefined] { - if (!scope) { - return [undefined, undefined]; - } - - const span = scope.getSpan(); - if (span) { - return [span?.transaction?.getDynamicSamplingContext(), span?.getTraceContext()]; - } - - 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, scope), traceContext]; - } -} diff --git a/packages/nextjs/src/edge/eventbuilder.ts b/packages/nextjs/src/edge/eventbuilder.ts deleted file mode 100644 index 4e483fce3ff7..000000000000 --- a/packages/nextjs/src/edge/eventbuilder.ts +++ /dev/null @@ -1,130 +0,0 @@ -import { getCurrentHub } from '@sentry/core'; -import type { - Event, - EventHint, - Exception, - Mechanism, - Severity, - SeverityLevel, - StackFrame, - StackParser, -} from '@sentry/types'; -import { - addExceptionMechanism, - addExceptionTypeValue, - extractExceptionKeysForMessage, - isError, - isPlainObject, - normalizeToSize, -} from '@sentry/utils'; - -/** - * Extracts stack frames from the error.stack string - */ -export function parseStackFrames(stackParser: StackParser, error: Error): StackFrame[] { - return stackParser(error.stack || '', 1); -} - -/** - * Extracts stack frames from the error and builds a Sentry Exception - */ -export function exceptionFromError(stackParser: StackParser, error: Error): Exception { - const exception: Exception = { - type: error.name || error.constructor.name, - value: error.message, - }; - - const frames = parseStackFrames(stackParser, error); - if (frames.length) { - exception.stacktrace = { frames }; - } - - return exception; -} - -/** - * Builds and Event from a Exception - * @hidden - */ -export function eventFromUnknownInput(stackParser: StackParser, exception: unknown, hint?: EventHint): Event { - let ex: unknown = exception; - const providedMechanism: Mechanism | undefined = - hint && hint.data && (hint.data as { mechanism: Mechanism }).mechanism; - const mechanism: Mechanism = providedMechanism || { - handled: true, - type: 'generic', - }; - - if (!isError(exception)) { - if (isPlainObject(exception)) { - // This will allow us to group events based on top-level keys - // which is much better than creating new group when any key/value change - const message = `Non-Error exception captured with keys: ${extractExceptionKeysForMessage(exception)}`; - - const hub = getCurrentHub(); - const client = hub.getClient(); - const normalizeDepth = client && client.getOptions().normalizeDepth; - hub.configureScope(scope => { - scope.setExtra('__serialized__', normalizeToSize(exception, normalizeDepth)); - }); - - ex = (hint && hint.syntheticException) || new Error(message); - (ex as Error).message = message; - } else { - // This handles when someone does: `throw "something awesome";` - // We use synthesized Error here so we can extract a (rough) stack trace. - ex = (hint && hint.syntheticException) || new Error(exception as string); - (ex as Error).message = exception as string; - } - mechanism.synthetic = true; - } - - const event = { - exception: { - values: [exceptionFromError(stackParser, ex as Error)], - }, - }; - - addExceptionTypeValue(event, undefined, undefined); - addExceptionMechanism(event, mechanism); - - return { - ...event, - event_id: hint && hint.event_id, - }; -} - -/** - * Builds and Event from a Message - * @hidden - */ -export function eventFromMessage( - stackParser: StackParser, - message: string, - // eslint-disable-next-line deprecation/deprecation - level: Severity | SeverityLevel = 'info', - hint?: EventHint, - attachStacktrace?: boolean, -): Event { - const event: Event = { - event_id: hint && hint.event_id, - level, - message, - }; - - if (attachStacktrace && hint && hint.syntheticException) { - const frames = parseStackFrames(stackParser, hint.syntheticException); - if (frames.length) { - event.exception = { - values: [ - { - value: message, - stacktrace: { frames }, - }, - ], - }; - } - } - - return event; -} diff --git a/packages/nextjs/src/edge/index.ts b/packages/nextjs/src/edge/index.ts index 6c2967d30f9b..281dd215001f 100644 --- a/packages/nextjs/src/edge/index.ts +++ b/packages/nextjs/src/edge/index.ts @@ -1,10 +1,16 @@ -import { getIntegrationsToSetup, initAndBind, Integrations as CoreIntegrations } from '@sentry/core'; +import type { ServerRuntimeClientOptions } from '@sentry/core'; +import { + getIntegrationsToSetup, + initAndBind, + Integrations as CoreIntegrations, + SDK_VERSION, + ServerRuntimeClient, +} from '@sentry/core'; import type { Options } from '@sentry/types'; import { createStackParser, GLOBAL_OBJ, nodeStackLineParser, stackParserFromStackParserOptions } from '@sentry/utils'; import { getVercelEnv } from '../common/getVercelEnv'; import { setAsyncLocalStorageAsyncContextStrategy } from './asyncLocalStorageAsyncContextStrategy'; -import { EdgeClient } from './edgeclient'; import { makeEdgeTransport } from './transport'; const nodeStackParser = createStackParser(nodeStackLineParser()); @@ -53,14 +59,30 @@ export function init(options: EdgeOptions = {}): void { options.instrumenter = 'sentry'; } - const clientOptions = { + const clientOptions: ServerRuntimeClientOptions = { ...options, stackParser: stackParserFromStackParserOptions(options.stackParser || nodeStackParser), integrations: getIntegrationsToSetup(options), transport: options.transport || makeEdgeTransport, }; - initAndBind(EdgeClient, clientOptions); + clientOptions._metadata = clientOptions._metadata || {}; + clientOptions._metadata.sdk = clientOptions._metadata.sdk || { + name: 'sentry.javascript.nextjs', + packages: [ + { + name: 'npm:@sentry/nextjs', + version: SDK_VERSION, + }, + ], + version: SDK_VERSION, + }; + + clientOptions.platform = 'edge'; + clientOptions.runtime = { name: 'edge' }; + clientOptions.serverName = process.env.SENTRY_NAME; + + initAndBind(ServerRuntimeClient, clientOptions); // TODO?: Sessiontracking } diff --git a/packages/nextjs/test/edge/edgeclient.test.ts b/packages/nextjs/test/edge/edgeclient.test.ts deleted file mode 100644 index cba4a751c71e..000000000000 --- a/packages/nextjs/test/edge/edgeclient.test.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { createTransport } from '@sentry/core'; -import type { Event, EventHint } from '@sentry/types'; - -import type { EdgeClientOptions } from '../../src/edge/edgeclient'; -import { EdgeClient } from '../../src/edge/edgeclient'; - -const PUBLIC_DSN = 'https://username@domain/123'; - -function getDefaultEdgeClientOptions(options: Partial = {}): EdgeClientOptions { - return { - integrations: [], - transport: () => createTransport({ recordDroppedEvent: () => undefined }, _ => Promise.resolve({})), - stackParser: () => [], - instrumenter: 'sentry', - ...options, - }; -} - -describe('NodeClient', () => { - describe('_prepareEvent', () => { - test('adds platform to event', () => { - const options = getDefaultEdgeClientOptions({ dsn: PUBLIC_DSN }); - const client = new EdgeClient(options); - - const event: Event = {}; - const hint: EventHint = {}; - (client as any)._prepareEvent(event, hint); - - expect(event.platform).toEqual('edge'); - }); - - test('adds runtime context to event', () => { - const options = getDefaultEdgeClientOptions({ dsn: PUBLIC_DSN }); - const client = new EdgeClient(options); - - const event: Event = {}; - const hint: EventHint = {}; - (client as any)._prepareEvent(event, hint); - - expect(event.contexts?.runtime).toEqual({ - name: 'edge', - }); - }); - - test("doesn't clobber existing runtime data", () => { - const options = getDefaultEdgeClientOptions({ dsn: PUBLIC_DSN }); - const client = new EdgeClient(options); - - const event: Event = { contexts: { runtime: { name: 'foo', version: '1.2.3' } } }; - const hint: EventHint = {}; - (client as any)._prepareEvent(event, hint); - - expect(event.contexts?.runtime).toEqual({ name: 'foo', version: '1.2.3' }); - expect(event.contexts?.runtime).not.toEqual({ name: 'edge' }); - }); - }); -}); From dad475ca832c35abb88cfbba1fac218cd8c86534 Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Thu, 7 Sep 2023 09:33:50 +0200 Subject: [PATCH 04/35] fix(node-experimental): Use Sentry logger as Otel logger (#8960) So the logs are properly hidden from breadcrumbs etc. --- packages/node-experimental/src/sdk/initOtel.ts | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/packages/node-experimental/src/sdk/initOtel.ts b/packages/node-experimental/src/sdk/initOtel.ts index 92cf794c8b29..318bba138837 100644 --- a/packages/node-experimental/src/sdk/initOtel.ts +++ b/packages/node-experimental/src/sdk/initOtel.ts @@ -1,7 +1,8 @@ -import { diag, DiagConsoleLogger, DiagLogLevel } from '@opentelemetry/api'; +import { diag, DiagLogLevel } from '@opentelemetry/api'; import { AlwaysOnSampler, NodeTracerProvider } from '@opentelemetry/sdk-trace-node'; import { getCurrentHub } from '@sentry/core'; import { SentryPropagator, SentrySpanProcessor } from '@sentry/opentelemetry-node'; +import { logger } from '@sentry/utils'; import type { NodeExperimentalClient } from './client'; import { SentryContextManager } from './otelContextManager'; @@ -14,7 +15,14 @@ export function initOtel(): () => void { const client = getCurrentHub().getClient(); if (client?.getOptions().debug) { - diag.setLogger(new DiagConsoleLogger(), DiagLogLevel.DEBUG); + const otelLogger = new Proxy(logger as typeof logger & { verbose: (typeof logger)['debug'] }, { + get(target, prop, receiver) { + const actualProp = prop === 'verbose' ? 'debug' : prop; + return Reflect.get(target, actualProp, receiver); + }, + }); + + diag.setLogger(otelLogger, DiagLogLevel.DEBUG); } // Create and configure NodeTracerProvider From fbe1b78a107c04f2761eb76d763b60366e4cee7f Mon Sep 17 00:00:00 2001 From: Daniel Griesser Date: Thu, 7 Sep 2023 11:49:40 +0200 Subject: [PATCH 05/35] fix: uuidv4 fix for cloudflare (#8968) --- packages/utils/src/misc.ts | 16 +++++++++++----- packages/utils/test/misc.test.ts | 30 +++++++++++++++++++++++++++--- 2 files changed, 38 insertions(+), 8 deletions(-) diff --git a/packages/utils/src/misc.ts b/packages/utils/src/misc.ts index a474c8e157e4..9799df421c31 100644 --- a/packages/utils/src/misc.ts +++ b/packages/utils/src/misc.ts @@ -25,13 +25,19 @@ export function uuid4(): string { const gbl = GLOBAL_OBJ as typeof GLOBAL_OBJ & CryptoGlobal; const crypto = gbl.crypto || gbl.msCrypto; - if (crypto && crypto.randomUUID) { - return crypto.randomUUID().replace(/-/g, ''); + let getRandomByte = (): number => Math.random() * 16; + try { + if (crypto && crypto.randomUUID) { + return crypto.randomUUID().replace(/-/g, ''); + } + if (crypto && crypto.getRandomValues) { + getRandomByte = () => crypto.getRandomValues(new Uint8Array(1))[0]; + } + } catch (_) { + // some runtimes can crash invoking crypto + // https://github.com/getsentry/sentry-javascript/issues/8935 } - const getRandomByte = - crypto && crypto.getRandomValues ? () => crypto.getRandomValues(new Uint8Array(1))[0] : () => Math.random() * 16; - // http://stackoverflow.com/questions/105034/how-to-create-a-guid-uuid-in-javascript/2117523#2117523 // Concatenating the following numbers as strings results in '10000000100040008000100000000000' return (([1e7] as unknown as string) + 1e3 + 4e3 + 8e3 + 1e11).replace(/[018]/g, c => diff --git a/packages/utils/test/misc.test.ts b/packages/utils/test/misc.test.ts index aaac0d46c0b0..dc75b70d4286 100644 --- a/packages/utils/test/misc.test.ts +++ b/packages/utils/test/misc.test.ts @@ -290,11 +290,12 @@ describe('checkOrSetAlreadyCaught()', () => { }); describe('uuid4 generation', () => { + const uuid4Regex = /^[0-9A-F]{12}[4][0-9A-F]{3}[89AB][0-9A-F]{15}$/i; // Jest messes with the global object, so there is no global crypto object in any node version // For this reason we need to create our own crypto object for each test to cover all the code paths it('returns valid uuid v4 ids via Math.random', () => { for (let index = 0; index < 1_000; index++) { - expect(uuid4()).toMatch(/^[0-9A-F]{12}[4][0-9A-F]{3}[89AB][0-9A-F]{15}$/i); + expect(uuid4()).toMatch(uuid4Regex); } }); @@ -305,7 +306,7 @@ describe('uuid4 generation', () => { (global as any).crypto = { getRandomValues: cryptoMod.getRandomValues }; for (let index = 0; index < 1_000; index++) { - expect(uuid4()).toMatch(/^[0-9A-F]{12}[4][0-9A-F]{3}[89AB][0-9A-F]{15}$/i); + expect(uuid4()).toMatch(uuid4Regex); } }); @@ -316,7 +317,30 @@ describe('uuid4 generation', () => { (global as any).crypto = { randomUUID: cryptoMod.randomUUID }; for (let index = 0; index < 1_000; index++) { - expect(uuid4()).toMatch(/^[0-9A-F]{12}[4][0-9A-F]{3}[89AB][0-9A-F]{15}$/i); + expect(uuid4()).toMatch(uuid4Regex); + } + }); + + it("return valid uuid v4 even if crypto doesn't exists", () => { + (global as any).crypto = { getRandomValues: undefined, randomUUID: undefined }; + + for (let index = 0; index < 1_000; index++) { + expect(uuid4()).toMatch(uuid4Regex); + } + }); + + it('return valid uuid v4 even if crypto invoked causes an error', () => { + (global as any).crypto = { + getRandomValues: () => { + throw new Error('yo'); + }, + randomUUID: () => { + throw new Error('yo'); + }, + }; + + for (let index = 0; index < 1_000; index++) { + expect(uuid4()).toMatch(uuid4Regex); } }); }); From 4c17f6e3b187251c29ebb87dcaccffb3fbc93fb8 Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Thu, 7 Sep 2023 12:01:49 +0200 Subject: [PATCH 06/35] ref(core): Introduce protected `_getBreadcrumbs()` on scope (#8961) Making it easier to potentially change this e.g. for POTEL. --- packages/core/src/scope.ts | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/packages/core/src/scope.ts b/packages/core/src/scope.ts index b2342169a510..ef6832bc773d 100644 --- a/packages/core/src/scope.ts +++ b/packages/core/src/scope.ts @@ -515,8 +515,9 @@ export class Scope implements ScopeInterface { this._applyFingerprint(event); - event.breadcrumbs = [...(event.breadcrumbs || []), ...this._breadcrumbs]; - event.breadcrumbs = event.breadcrumbs.length > 0 ? event.breadcrumbs : undefined; + const scopeBreadcrumbs = this._getBreadcrumbs(); + const breadcrumbs = [...(event.breadcrumbs || []), ...scopeBreadcrumbs]; + event.breadcrumbs = breadcrumbs.length > 0 ? breadcrumbs : undefined; event.sdkProcessingMetadata = { ...event.sdkProcessingMetadata, @@ -551,6 +552,13 @@ export class Scope implements ScopeInterface { return this._propagationContext; } + /** + * Get the breadcrumbs for this scope. + */ + protected _getBreadcrumbs(): Breadcrumb[] { + return this._breadcrumbs; + } + /** * This will be called after {@link applyToEvent} is finished. */ From 843c07218702fc8a3268e1a841351a84c4f689f4 Mon Sep 17 00:00:00 2001 From: Onur Temizkan Date: Thu, 7 Sep 2023 13:31:26 +0100 Subject: [PATCH 07/35] fix(remix): Add `glob` to Remix SDK dependencies. (#8963) --- .../create-remix-app/package.json | 2 +- .../create-remix-app/upload-sourcemaps.sh | 5 ++++ packages/remix/package.json | 1 + yarn.lock | 24 +++++++++++++++++++ 4 files changed, 31 insertions(+), 1 deletion(-) create mode 100755 packages/e2e-tests/test-applications/create-remix-app/upload-sourcemaps.sh diff --git a/packages/e2e-tests/test-applications/create-remix-app/package.json b/packages/e2e-tests/test-applications/create-remix-app/package.json index 06adb8434d79..27069ccb42fc 100644 --- a/packages/e2e-tests/test-applications/create-remix-app/package.json +++ b/packages/e2e-tests/test-applications/create-remix-app/package.json @@ -2,7 +2,7 @@ "private": true, "sideEffects": false, "scripts": { - "build": "remix build", + "build": "remix build --sourcemap && ./upload-sourcemaps.sh", "dev": "remix dev", "start": "remix-serve build", "typecheck": "tsc", diff --git a/packages/e2e-tests/test-applications/create-remix-app/upload-sourcemaps.sh b/packages/e2e-tests/test-applications/create-remix-app/upload-sourcemaps.sh new file mode 100755 index 000000000000..852c07bf4498 --- /dev/null +++ b/packages/e2e-tests/test-applications/create-remix-app/upload-sourcemaps.sh @@ -0,0 +1,5 @@ +export SENTRY_ORG=${E2E_TEST_SENTRY_ORG_SLUG} +export SENTRY_PROJECT=${E2E_TEST_SENTRY_TEST_PROJECT} +export SENTRY_AUTH_TOKEN=${E2E_TEST_AUTH_TOKEN} + +sentry-upload-sourcemaps diff --git a/packages/remix/package.json b/packages/remix/package.json index 66ab65d26bc4..3d37de4bbdac 100644 --- a/packages/remix/package.json +++ b/packages/remix/package.json @@ -33,6 +33,7 @@ "@sentry/react": "7.68.0", "@sentry/types": "7.68.0", "@sentry/utils": "7.68.0", + "glob": "^10.3.4", "tslib": "^2.4.1 || ^1.9.3", "yargs": "^17.6.0" }, diff --git a/yarn.lock b/yarn.lock index ec2d72d684e8..76f2f4de7581 100644 --- a/yarn.lock +++ b/yarn.lock @@ -14727,6 +14727,17 @@ glob@^10.2.2: minipass "^5.0.0 || ^6.0.2" path-scurry "^1.10.0" +glob@^10.3.4: + version "10.3.4" + resolved "https://registry.yarnpkg.com/glob/-/glob-10.3.4.tgz#c85c9c7ab98669102b6defda76d35c5b1ef9766f" + integrity sha512-6LFElP3A+i/Q8XQKEvZjkEWEOTgAIALR9AO2rwT8bgPhDd1anmqDJDZ6lLddI4ehxxxR1S5RIqKe1uapMQfYaQ== + dependencies: + foreground-child "^3.1.0" + jackspeak "^2.0.3" + minimatch "^9.0.1" + minipass "^5.0.0 || ^6.0.2 || ^7.0.0" + path-scurry "^1.10.1" + glob@^5.0.10, glob@^5.0.15: version "5.0.15" resolved "https://registry.yarnpkg.com/glob/-/glob-5.0.15.tgz#1bc936b9e02f4a603fcc222ecf7633d30b8b93b1" @@ -19396,6 +19407,11 @@ minipass@^5.0.0: resolved "https://registry.yarnpkg.com/minipass/-/minipass-6.0.2.tgz#542844b6c4ce95b202c0995b0a471f1229de4c81" integrity sha512-MzWSV5nYVT7mVyWCwn2o7JH13w2TBRmmSqSRCKzTw+lmft9X4z+3wjvs06Tzijo5z4W/kahUCDpRXTF+ZrmF/w== +"minipass@^5.0.0 || ^6.0.2 || ^7.0.0": + version "7.0.3" + resolved "https://registry.yarnpkg.com/minipass/-/minipass-7.0.3.tgz#05ea638da44e475037ed94d1c7efcc76a25e1974" + integrity sha512-LhbbwCfz3vsb12j/WkWQPZfKTsgqIe1Nf/ti1pKjYESGLHIVjWU96G9/ljLH4F9mWNVhlQOm0VySdAWzf05dpg== + minizlib@^1.3.3: version "1.3.3" resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-1.3.3.tgz#2290de96818a34c29551c8a8d301216bd65a861d" @@ -21526,6 +21542,14 @@ path-scurry@^1.10.0: lru-cache "^9.1.1 || ^10.0.0" minipass "^5.0.0 || ^6.0.2" +path-scurry@^1.10.1: + version "1.10.1" + resolved "https://registry.yarnpkg.com/path-scurry/-/path-scurry-1.10.1.tgz#9ba6bf5aa8500fe9fd67df4f0d9483b2b0bfc698" + integrity sha512-MkhCqzzBEpPvxxQ71Md0b1Kk51W01lrYvlMzSUaIzNsODdd7mqhiimSZlr+VegAz5Z6Vzt9Xg2ttE//XBhH3EQ== + dependencies: + lru-cache "^9.1.1 || ^10.0.0" + minipass "^5.0.0 || ^6.0.2 || ^7.0.0" + path-scurry@^1.6.1: version "1.6.4" resolved "https://registry.yarnpkg.com/path-scurry/-/path-scurry-1.6.4.tgz#020a9449e5382a4acb684f9c7e1283bc5695de66" From eafe791bad26cb1b86903e75b28143a8883ebba2 Mon Sep 17 00:00:00 2001 From: Onur Temizkan Date: Thu, 7 Sep 2023 13:32:36 +0100 Subject: [PATCH 08/35] chore(e2e): Use Node 18 for E2E tests. (#8964) --- .github/workflows/build.yml | 4 ++-- packages/e2e-tests/Dockerfile.publish-packages | 2 +- packages/e2e-tests/package.json | 6 ++---- .../test-applications/create-next-app/package.json | 3 +-- .../test-applications/create-next-app/playwright.config.ts | 5 +++++ .../test-applications/create-react-app/package.json | 3 +-- .../test-applications/create-remix-app/package.json | 3 +++ .../e2e-tests/test-applications/nextjs-app-dir/package.json | 3 +-- .../test-applications/nextjs-app-dir/playwright.config.ts | 5 +++++ .../test-applications/node-express-app/package.json | 3 +-- .../test-applications/react-create-hash-router/package.json | 3 +-- .../standard-frontend-react-tracing-import/package.json | 3 +-- .../test-applications/standard-frontend-react/package.json | 3 +-- 13 files changed, 25 insertions(+), 21 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 4cfa56ec640e..68ecae92b8bd 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -818,7 +818,7 @@ jobs: - name: Set up Node uses: actions/setup-node@v3 with: - node-version-file: 'package.json' + node-version-file: 'packages/e2e-tests/package.json' - name: Restore caches uses: ./.github/actions/restore-cache env: @@ -833,7 +833,7 @@ jobs: - name: Get node version id: versions run: | - echo "echo node=$(jq -r '.volta.node' package.json)" >> $GITHUB_OUTPUT + echo "echo node=$(jq -r '.volta.node' packages/e2e-tests/package.json)" >> $GITHUB_OUTPUT - name: Validate Verdaccio run: yarn test:validate diff --git a/packages/e2e-tests/Dockerfile.publish-packages b/packages/e2e-tests/Dockerfile.publish-packages index 79151bb0ae9e..117535a0e326 100644 --- a/packages/e2e-tests/Dockerfile.publish-packages +++ b/packages/e2e-tests/Dockerfile.publish-packages @@ -1,5 +1,5 @@ # This Dockerfile exists for the purpose of using a specific node/npm version (ie. the same we use in CI) to run npm publish with -ARG NODE_VERSION=16.19.0 +ARG NODE_VERSION=18.17.1 FROM node:${NODE_VERSION} WORKDIR /sentry-javascript/packages/e2e-tests diff --git a/packages/e2e-tests/package.json b/packages/e2e-tests/package.json index 21c6832b3e65..371f7313093e 100644 --- a/packages/e2e-tests/package.json +++ b/packages/e2e-tests/package.json @@ -2,9 +2,6 @@ "name": "@sentry-internal/e2e-tests", "version": "7.68.0", "license": "MIT", - "engines": { - "node": ">=10" - }, "private": true, "scripts": { "fix": "run-s fix:eslint fix:prettier", @@ -32,6 +29,7 @@ "yaml": "2.2.2" }, "volta": { - "extends": "../../package.json" + "node": "18.17.1", + "yarn": "1.22.19" } } diff --git a/packages/e2e-tests/test-applications/create-next-app/package.json b/packages/e2e-tests/test-applications/create-next-app/package.json index 0fd95034f36c..58b89cb7dda7 100644 --- a/packages/e2e-tests/test-applications/create-next-app/package.json +++ b/packages/e2e-tests/test-applications/create-next-app/package.json @@ -25,7 +25,6 @@ "@playwright/test": "^1.27.1" }, "volta": { - "node": "16.19.0", - "yarn": "1.22.19" + "extends": "../../package.json" } } diff --git a/packages/e2e-tests/test-applications/create-next-app/playwright.config.ts b/packages/e2e-tests/test-applications/create-next-app/playwright.config.ts index dafe32a31126..734bda58c10f 100644 --- a/packages/e2e-tests/test-applications/create-next-app/playwright.config.ts +++ b/packages/e2e-tests/test-applications/create-next-app/playwright.config.ts @@ -1,6 +1,11 @@ import type { PlaywrightTestConfig } from '@playwright/test'; import { devices } from '@playwright/test'; +// Fix urls not resolving to localhost on Node v17+ +// See: https://github.com/axios/axios/issues/3821#issuecomment-1413727575 +import { setDefaultResultOrder } from 'dns'; +setDefaultResultOrder('ipv4first'); + const testEnv = process.env.TEST_ENV; if (!testEnv) { diff --git a/packages/e2e-tests/test-applications/create-react-app/package.json b/packages/e2e-tests/test-applications/create-react-app/package.json index 1cdcc07f6617..07c4210736a8 100644 --- a/packages/e2e-tests/test-applications/create-react-app/package.json +++ b/packages/e2e-tests/test-applications/create-react-app/package.json @@ -48,7 +48,6 @@ ] }, "volta": { - "node": "16.19.0", - "yarn": "1.22.19" + "extends": "../../package.json" } } diff --git a/packages/e2e-tests/test-applications/create-remix-app/package.json b/packages/e2e-tests/test-applications/create-remix-app/package.json index 27069ccb42fc..0927785fc50d 100644 --- a/packages/e2e-tests/test-applications/create-remix-app/package.json +++ b/packages/e2e-tests/test-applications/create-remix-app/package.json @@ -31,5 +31,8 @@ }, "engines": { "node": ">=14" + }, + "volta": { + "extends": "../../package.json" } } diff --git a/packages/e2e-tests/test-applications/nextjs-app-dir/package.json b/packages/e2e-tests/test-applications/nextjs-app-dir/package.json index 7edc1429c308..461eabaf4dc2 100644 --- a/packages/e2e-tests/test-applications/nextjs-app-dir/package.json +++ b/packages/e2e-tests/test-applications/nextjs-app-dir/package.json @@ -32,7 +32,6 @@ "@sentry/utils": "latest || *" }, "volta": { - "node": "16.19.0", - "yarn": "1.22.19" + "extends": "../../package.json" } } diff --git a/packages/e2e-tests/test-applications/nextjs-app-dir/playwright.config.ts b/packages/e2e-tests/test-applications/nextjs-app-dir/playwright.config.ts index c0c413c0627b..391b5eac610e 100644 --- a/packages/e2e-tests/test-applications/nextjs-app-dir/playwright.config.ts +++ b/packages/e2e-tests/test-applications/nextjs-app-dir/playwright.config.ts @@ -1,6 +1,11 @@ import type { PlaywrightTestConfig } from '@playwright/test'; import { devices } from '@playwright/test'; +// Fix urls not resolving to localhost on Node v17+ +// See: https://github.com/axios/axios/issues/3821#issuecomment-1413727575 +import { setDefaultResultOrder } from 'dns'; +setDefaultResultOrder('ipv4first'); + const testEnv = process.env.TEST_ENV; if (!testEnv) { diff --git a/packages/e2e-tests/test-applications/node-express-app/package.json b/packages/e2e-tests/test-applications/node-express-app/package.json index 1c1264bd52ec..02dcb35da7fe 100644 --- a/packages/e2e-tests/test-applications/node-express-app/package.json +++ b/packages/e2e-tests/test-applications/node-express-app/package.json @@ -24,7 +24,6 @@ "@playwright/test": "^1.27.1" }, "volta": { - "node": "16.19.0", - "yarn": "1.22.19" + "extends": "../../package.json" } } diff --git a/packages/e2e-tests/test-applications/react-create-hash-router/package.json b/packages/e2e-tests/test-applications/react-create-hash-router/package.json index 72e1fc09c055..2ea0b4077e18 100644 --- a/packages/e2e-tests/test-applications/react-create-hash-router/package.json +++ b/packages/e2e-tests/test-applications/react-create-hash-router/package.json @@ -51,7 +51,6 @@ "serve": "14.0.1" }, "volta": { - "node": "16.19.0", - "yarn": "1.22.19" + "extends": "../../package.json" } } diff --git a/packages/e2e-tests/test-applications/standard-frontend-react-tracing-import/package.json b/packages/e2e-tests/test-applications/standard-frontend-react-tracing-import/package.json index 72a6f223e960..bb5b06eb0378 100644 --- a/packages/e2e-tests/test-applications/standard-frontend-react-tracing-import/package.json +++ b/packages/e2e-tests/test-applications/standard-frontend-react-tracing-import/package.json @@ -51,7 +51,6 @@ "serve": "14.0.1" }, "volta": { - "node": "16.19.0", - "yarn": "1.22.19" + "extends": "../../package.json" } } diff --git a/packages/e2e-tests/test-applications/standard-frontend-react/package.json b/packages/e2e-tests/test-applications/standard-frontend-react/package.json index 5d0d4716ef1c..ab9f06cb3ca0 100644 --- a/packages/e2e-tests/test-applications/standard-frontend-react/package.json +++ b/packages/e2e-tests/test-applications/standard-frontend-react/package.json @@ -52,7 +52,6 @@ "serve": "14.0.1" }, "volta": { - "node": "16.19.0", - "yarn": "1.22.19" + "extends": "../../package.json" } } From cf75ffea455a6265aa94c0fe855417bff826839c Mon Sep 17 00:00:00 2001 From: Abhijeet Prasad Date: Thu, 7 Sep 2023 11:17:08 -0400 Subject: [PATCH 09/35] feat(core): Update span performance API names (#8971) As per the new changes in RFC 101 in https://github.com/getsentry/rfcs/pull/113, update the span performance API names. - `startActiveSpan` -> `startSpan` - `startSpan` -> `startInactiveSpan` https://github.com/getsentry/rfcs/blob/main/text/0101-revamping-the-sdk-performance-api.md `startActiveSpan` is deprecated, while `startInactiveSpan` is being introduced. The breaking change is that `startSpan` is being changed, but considering that basically no-one is using the `startSpan` API, we should be fine to break here for correctness reasons. Better break now than to have everyone refactor their code in v8. --- packages/browser/src/exports.ts | 3 +++ packages/core/src/tracing/index.ts | 3 ++- packages/core/src/tracing/trace.ts | 21 +++++++++------ packages/core/test/lib/tracing/trace.test.ts | 22 ++++++++-------- packages/node-experimental/src/sdk/trace.ts | 13 +++++++--- .../node-experimental/test/sdk/trace.test.ts | 26 +++++++++---------- packages/node/src/index.ts | 4 ++- packages/serverless/src/index.ts | 4 ++- packages/sveltekit/src/server/index.ts | 4 ++- 9 files changed, 60 insertions(+), 40 deletions(-) diff --git a/packages/browser/src/exports.ts b/packages/browser/src/exports.ts index e9050399e641..659b80bc8962 100644 --- a/packages/browser/src/exports.ts +++ b/packages/browser/src/exports.ts @@ -37,6 +37,9 @@ export { makeMain, Scope, startTransaction, + getActiveSpan, + startSpan, + startInactiveSpan, SDK_VERSION, setContext, setExtra, diff --git a/packages/core/src/tracing/index.ts b/packages/core/src/tracing/index.ts index 470286366c81..c4ed853c5316 100644 --- a/packages/core/src/tracing/index.ts +++ b/packages/core/src/tracing/index.ts @@ -6,6 +6,7 @@ export { extractTraceparentData, getActiveTransaction } from './utils'; // eslint-disable-next-line deprecation/deprecation export { SpanStatus } from './spanstatus'; export type { SpanStatusType } from './span'; -export { trace, getActiveSpan, startActiveSpan, startSpan } from './trace'; +// eslint-disable-next-line deprecation/deprecation +export { trace, getActiveSpan, startSpan, startInactiveSpan, startActiveSpan } from './trace'; export { getDynamicSamplingContextFromClient } from './dynamicSamplingContext'; export { setMeasurement } from './measurement'; diff --git a/packages/core/src/tracing/trace.ts b/packages/core/src/tracing/trace.ts index 0ca928e9002a..31671104bd02 100644 --- a/packages/core/src/tracing/trace.ts +++ b/packages/core/src/tracing/trace.ts @@ -34,14 +34,14 @@ export function trace( const parentSpan = scope.getSpan(); - function startActiveSpan(): Span | undefined { + function createChildSpanOrTransaction(): Span | undefined { if (!hasTracingEnabled()) { return undefined; } return parentSpan ? parentSpan.startChild(ctx) : hub.startTransaction(ctx); } - const activeSpan = startActiveSpan(); + const activeSpan = createChildSpanOrTransaction(); scope.setSpan(activeSpan); function finishAndSetSpan(): void { @@ -82,13 +82,13 @@ export function trace( * The created span is the active span and will be used as parent by other spans created inside the function * and can be accessed via `Sentry.getSpan()`, as long as the function is executed while the scope is active. * - * If you want to create a span that is not set as active, use {@link startSpan}. + * If you want to create a span that is not set as active, use {@link startInactiveSpan}. * * Note that if you have not enabled tracing extensions via `addTracingExtensions` * or you didn't set `tracesSampleRate`, this function will not generate spans * and the `span` returned from the callback will be undefined. */ -export function startActiveSpan(context: TransactionContext, callback: (span: Span | undefined) => T): T { +export function startSpan(context: TransactionContext, callback: (span: Span | undefined) => T): T { const ctx = { ...context }; // If a name is set and a description is not, set the description to the name. if (ctx.name !== undefined && ctx.description === undefined) { @@ -100,14 +100,14 @@ export function startActiveSpan(context: TransactionContext, callback: (span: const parentSpan = scope.getSpan(); - function startActiveSpan(): Span | undefined { + function createChildSpanOrTransaction(): Span | undefined { if (!hasTracingEnabled()) { return undefined; } return parentSpan ? parentSpan.startChild(ctx) : hub.startTransaction(ctx); } - const activeSpan = startActiveSpan(); + const activeSpan = createChildSpanOrTransaction(); scope.setSpan(activeSpan); function finishAndSetSpan(): void { @@ -141,17 +141,22 @@ export function startActiveSpan(context: TransactionContext, callback: (span: return maybePromiseResult; } +/** + * @deprecated Use {@link startSpan} instead. + */ +export const startActiveSpan = startSpan; + /** * Creates a span. This span is not set as active, so will not get automatic instrumentation spans * as children or be able to be accessed via `Sentry.getSpan()`. * - * If you want to create a span that is set as active, use {@link startActiveSpan}. + * If you want to create a span that is set as active, use {@link startSpan}. * * Note that if you have not enabled tracing extensions via `addTracingExtensions` * or you didn't set `tracesSampleRate` or `tracesSampler`, this function will not generate spans * and the `span` returned from the callback will be undefined. */ -export function startSpan(context: TransactionContext): Span | undefined { +export function startInactiveSpan(context: TransactionContext): Span | undefined { if (!hasTracingEnabled()) { return undefined; } diff --git a/packages/core/test/lib/tracing/trace.test.ts b/packages/core/test/lib/tracing/trace.test.ts index f607aa7369f9..e3469e93a2e2 100644 --- a/packages/core/test/lib/tracing/trace.test.ts +++ b/packages/core/test/lib/tracing/trace.test.ts @@ -1,5 +1,5 @@ import { addTracingExtensions, Hub, makeMain } from '../../../src'; -import { startActiveSpan } from '../../../src/tracing'; +import { startSpan } from '../../../src/tracing'; import { getDefaultTestClientOptions, TestClient } from '../../mocks/client'; beforeAll(() => { @@ -14,7 +14,7 @@ const enum Type { let hub: Hub; let client: TestClient; -describe('startActiveSpan', () => { +describe('startSpan', () => { beforeEach(() => { const options = getDefaultTestClientOptions({ tracesSampleRate: 0.0 }); client = new TestClient(options); @@ -38,7 +38,7 @@ describe('startActiveSpan', () => { ])('with %s callback and error %s', (_type, isError, callback, expected) => { it('should return the same value as the callback', async () => { try { - const result = await startActiveSpan({ name: 'GET users/[id]' }, () => { + const result = await startSpan({ name: 'GET users/[id]' }, () => { return callback(); }); expect(result).toEqual(expected); @@ -53,7 +53,7 @@ describe('startActiveSpan', () => { // if tracingExtensions are not enabled jest.spyOn(hub, 'startTransaction').mockReturnValue(undefined); try { - const result = await startActiveSpan({ name: 'GET users/[id]' }, () => { + const result = await startSpan({ name: 'GET users/[id]' }, () => { return callback(); }); expect(result).toEqual(expected); @@ -68,7 +68,7 @@ describe('startActiveSpan', () => { ref = transaction; }); try { - await startActiveSpan({ name: 'GET users/[id]' }, () => { + await startSpan({ name: 'GET users/[id]' }, () => { return callback(); }); } catch (e) { @@ -86,7 +86,7 @@ describe('startActiveSpan', () => { ref = transaction; }); try { - await startActiveSpan( + await startSpan( { name: 'GET users/[id]', parentSampled: true, @@ -113,7 +113,7 @@ describe('startActiveSpan', () => { ref = transaction; }); try { - await startActiveSpan({ name: 'GET users/[id]' }, span => { + await startSpan({ name: 'GET users/[id]' }, span => { if (span) { span.op = 'http.server'; } @@ -132,8 +132,8 @@ describe('startActiveSpan', () => { ref = transaction; }); try { - await startActiveSpan({ name: 'GET users/[id]', parentSampled: true }, () => { - return startActiveSpan({ name: 'SELECT * from users' }, () => { + await startSpan({ name: 'GET users/[id]', parentSampled: true }, () => { + return startSpan({ name: 'SELECT * from users' }, () => { return callback(); }); }); @@ -153,8 +153,8 @@ describe('startActiveSpan', () => { ref = transaction; }); try { - await startActiveSpan({ name: 'GET users/[id]', parentSampled: true }, () => { - return startActiveSpan({ name: 'SELECT * from users' }, childSpan => { + await startSpan({ name: 'GET users/[id]', parentSampled: true }, () => { + return startSpan({ name: 'SELECT * from users' }, childSpan => { if (childSpan) { childSpan.op = 'db.query'; } diff --git a/packages/node-experimental/src/sdk/trace.ts b/packages/node-experimental/src/sdk/trace.ts index a086b8edd2c2..e2fd14663e28 100644 --- a/packages/node-experimental/src/sdk/trace.ts +++ b/packages/node-experimental/src/sdk/trace.ts @@ -12,13 +12,13 @@ import type { NodeExperimentalClient } from './client'; * The created span is the active span and will be used as parent by other spans created inside the function * and can be accessed via `Sentry.getSpan()`, as long as the function is executed while the scope is active. * - * If you want to create a span that is not set as active, use {@link startSpan}. + * If you want to create a span that is not set as active, use {@link startInactiveSpan}. * * Note that if you have not enabled tracing extensions via `addTracingExtensions` * or you didn't set `tracesSampleRate`, this function will not generate spans * and the `span` returned from the callback will be undefined. */ -export function startActiveSpan(context: TransactionContext, callback: (span: Span | undefined) => T): T { +export function startSpan(context: TransactionContext, callback: (span: Span | undefined) => T): T { const tracer = getTracer(); if (!tracer) { return callback(undefined); @@ -66,17 +66,22 @@ export function startActiveSpan(context: TransactionContext, callback: (span: }); } +/** + * @deprecated Use {@link startSpan} instead. + */ +export const startActiveSpan = startSpan; + /** * Creates a span. This span is not set as active, so will not get automatic instrumentation spans * as children or be able to be accessed via `Sentry.getSpan()`. * - * If you want to create a span that is set as active, use {@link startActiveSpan}. + * If you want to create a span that is set as active, use {@link startSpan}. * * Note that if you have not enabled tracing extensions via `addTracingExtensions` * or you didn't set `tracesSampleRate` or `tracesSampler`, this function will not generate spans * and the `span` returned from the callback will be undefined. */ -export function startSpan(context: TransactionContext): Span | undefined { +export function startInactiveSpan(context: TransactionContext): Span | undefined { const tracer = getTracer(); if (!tracer) { return undefined; diff --git a/packages/node-experimental/test/sdk/trace.test.ts b/packages/node-experimental/test/sdk/trace.test.ts index 87aae6c66689..c53606140fa1 100644 --- a/packages/node-experimental/test/sdk/trace.test.ts +++ b/packages/node-experimental/test/sdk/trace.test.ts @@ -8,13 +8,13 @@ describe('trace', () => { mockSdkInit({ enableTracing: true }); }); - describe('startActiveSpan', () => { + describe('startSpan', () => { it('works with a sync callback', () => { const spans: Span[] = []; expect(Sentry.getActiveSpan()).toEqual(undefined); - Sentry.startActiveSpan({ name: 'outer' }, outerSpan => { + Sentry.startSpan({ name: 'outer' }, outerSpan => { expect(outerSpan).toBeDefined(); spans.push(outerSpan!); @@ -22,7 +22,7 @@ describe('trace', () => { expect(outerSpan).toBeInstanceOf(Transaction); expect(Sentry.getActiveSpan()).toEqual(outerSpan); - Sentry.startActiveSpan({ name: 'inner' }, innerSpan => { + Sentry.startSpan({ name: 'inner' }, innerSpan => { expect(innerSpan).toBeDefined(); spans.push(innerSpan!); @@ -49,7 +49,7 @@ describe('trace', () => { expect(Sentry.getActiveSpan()).toEqual(undefined); - await Sentry.startActiveSpan({ name: 'outer' }, async outerSpan => { + await Sentry.startSpan({ name: 'outer' }, async outerSpan => { expect(outerSpan).toBeDefined(); spans.push(outerSpan!); @@ -59,7 +59,7 @@ describe('trace', () => { expect(outerSpan).toBeInstanceOf(Transaction); expect(Sentry.getActiveSpan()).toEqual(outerSpan); - await Sentry.startActiveSpan({ name: 'inner' }, async innerSpan => { + await Sentry.startSpan({ name: 'inner' }, async innerSpan => { expect(innerSpan).toBeDefined(); spans.push(innerSpan!); @@ -89,7 +89,7 @@ describe('trace', () => { expect(Sentry.getActiveSpan()).toEqual(undefined); - Sentry.startActiveSpan({ name: 'outer' }, outerSpan => { + Sentry.startSpan({ name: 'outer' }, outerSpan => { expect(outerSpan).toBeDefined(); spans1.push(outerSpan!); @@ -97,7 +97,7 @@ describe('trace', () => { expect(outerSpan).toBeInstanceOf(Transaction); expect(Sentry.getActiveSpan()).toEqual(outerSpan); - Sentry.startActiveSpan({ name: 'inner' }, innerSpan => { + Sentry.startSpan({ name: 'inner' }, innerSpan => { expect(innerSpan).toBeDefined(); spans1.push(innerSpan!); @@ -108,7 +108,7 @@ describe('trace', () => { }); }); - Sentry.startActiveSpan({ name: 'outer2' }, outerSpan => { + Sentry.startSpan({ name: 'outer2' }, outerSpan => { expect(outerSpan).toBeDefined(); spans2.push(outerSpan!); @@ -116,7 +116,7 @@ describe('trace', () => { expect(outerSpan).toBeInstanceOf(Transaction); expect(Sentry.getActiveSpan()).toEqual(outerSpan); - Sentry.startActiveSpan({ name: 'inner2' }, innerSpan => { + Sentry.startSpan({ name: 'inner2' }, innerSpan => { expect(innerSpan).toBeDefined(); spans2.push(innerSpan!); @@ -133,9 +133,9 @@ describe('trace', () => { }); }); - describe('startSpan', () => { + describe('startInactiveSpan', () => { it('works at the root', () => { - const span = Sentry.startSpan({ name: 'test' }); + const span = Sentry.startInactiveSpan({ name: 'test' }); expect(span).toBeDefined(); expect(span).toBeInstanceOf(Transaction); @@ -150,11 +150,11 @@ describe('trace', () => { }); it('works as a child span', () => { - Sentry.startActiveSpan({ name: 'outer' }, outerSpan => { + Sentry.startSpan({ name: 'outer' }, outerSpan => { expect(outerSpan).toBeDefined(); expect(Sentry.getActiveSpan()).toEqual(outerSpan); - const innerSpan = Sentry.startSpan({ name: 'test' }); + const innerSpan = Sentry.startInactiveSpan({ name: 'test' }); expect(innerSpan).toBeDefined(); expect(innerSpan).toBeInstanceOf(Span); diff --git a/packages/node/src/index.ts b/packages/node/src/index.ts index 1c172bc89618..ab8c82e5a3fd 100644 --- a/packages/node/src/index.ts +++ b/packages/node/src/index.ts @@ -56,8 +56,10 @@ export { captureCheckIn, setMeasurement, getActiveSpan, - startActiveSpan, startSpan, + // eslint-disable-next-line deprecation/deprecation + startActiveSpan, + startInactiveSpan, } from '@sentry/core'; export type { SpanStatusType } from '@sentry/core'; export { autoDiscoverNodePerformanceMonitoringIntegrations } from './tracing'; diff --git a/packages/serverless/src/index.ts b/packages/serverless/src/index.ts index f7a195aba4e8..3d1a8c7c3aad 100644 --- a/packages/serverless/src/index.ts +++ b/packages/serverless/src/index.ts @@ -51,6 +51,8 @@ export { Integrations, setMeasurement, getActiveSpan, - startActiveSpan, startSpan, + // eslint-disable-next-line deprecation/deprecation + startActiveSpan, + startInactiveSpan, } from '@sentry/node'; diff --git a/packages/sveltekit/src/server/index.ts b/packages/sveltekit/src/server/index.ts index f7c0b99f6301..b4a128843217 100644 --- a/packages/sveltekit/src/server/index.ts +++ b/packages/sveltekit/src/server/index.ts @@ -46,8 +46,10 @@ export { Handlers, setMeasurement, getActiveSpan, - startActiveSpan, startSpan, + // eslint-disable-next-line deprecation/deprecation + startActiveSpan, + startInactiveSpan, } from '@sentry/node'; // We can still leave this for the carrier init and type exports From 4278a71e0d62d24c50821808622f6aacae901b95 Mon Sep 17 00:00:00 2001 From: Onur Temizkan Date: Fri, 8 Sep 2023 13:43:12 +0100 Subject: [PATCH 10/35] feat(remix): Accept `org`, `project` and `url` as args to upload script (#8985) --- .../create-remix-app/upload-sourcemaps.sh | 4 +--- packages/remix/scripts/createRelease.js | 8 ++++++-- .../remix/scripts/sentry-upload-sourcemaps.js | 15 +++++++++++++++ 3 files changed, 22 insertions(+), 5 deletions(-) diff --git a/packages/e2e-tests/test-applications/create-remix-app/upload-sourcemaps.sh b/packages/e2e-tests/test-applications/create-remix-app/upload-sourcemaps.sh index 852c07bf4498..721a71490e69 100755 --- a/packages/e2e-tests/test-applications/create-remix-app/upload-sourcemaps.sh +++ b/packages/e2e-tests/test-applications/create-remix-app/upload-sourcemaps.sh @@ -1,5 +1,3 @@ -export SENTRY_ORG=${E2E_TEST_SENTRY_ORG_SLUG} -export SENTRY_PROJECT=${E2E_TEST_SENTRY_TEST_PROJECT} export SENTRY_AUTH_TOKEN=${E2E_TEST_AUTH_TOKEN} -sentry-upload-sourcemaps +sentry-upload-sourcemaps --org ${E2E_TEST_SENTRY_ORG_SLUG} --project ${E2E_TEST_SENTRY_TEST_PROJECT} diff --git a/packages/remix/scripts/createRelease.js b/packages/remix/scripts/createRelease.js index 3e9ca11a931f..ab969c48be18 100644 --- a/packages/remix/scripts/createRelease.js +++ b/packages/remix/scripts/createRelease.js @@ -3,9 +3,13 @@ const SentryCli = require('@sentry/cli'); const { deleteSourcemaps } = require('./deleteSourcemaps'); -const sentry = new SentryCli(); - async function createRelease(argv, URL_PREFIX, BUILD_PATH) { + const sentry = new SentryCli(null, { + url: argv.url, + org: argv.org, + project: argv.project, + }); + let release; if (!argv.release) { diff --git a/packages/remix/scripts/sentry-upload-sourcemaps.js b/packages/remix/scripts/sentry-upload-sourcemaps.js index c7eef6b0fa92..625526af5a8a 100755 --- a/packages/remix/scripts/sentry-upload-sourcemaps.js +++ b/packages/remix/scripts/sentry-upload-sourcemaps.js @@ -15,6 +15,18 @@ const argv = yargs(process.argv.slice(2)) "If not provided, a new release id will be determined by Sentry CLI's `propose-version`.\n" + 'See: https://docs.sentry.io/product/releases/suspect-commits/#using-the-cli\n', }) + .option('org', { + type: 'string', + describe: 'The Sentry organization slug', + }) + .option('project', { + type: 'string', + describe: 'The Sentry project slug', + }) + .option('url', { + type: 'string', + describe: 'The Sentry server URL', + }) .option('urlPrefix', { type: 'string', describe: 'URL prefix to add to the beginning of all filenames', @@ -38,6 +50,9 @@ const argv = yargs(process.argv.slice(2)) .usage( 'Usage: $0\n' + ' [--release RELEASE]\n' + + ' [--org ORG]\n' + + ' [--project PROJECT]\n' + + ' [--url URL]\n' + ' [--urlPrefix URL_PREFIX]\n' + ' [--buildPath BUILD_PATH]\n\n' + ' [--disableDebugIds true|false]\n\n' + From f63b33b488c14cef6f42754e576731fc69fb0245 Mon Sep 17 00:00:00 2001 From: Abhijeet Prasad Date: Fri, 8 Sep 2023 09:31:57 -0400 Subject: [PATCH 11/35] ref: Remove all usages of ts-ignore (#8974) Replace ts-ignore with ts-expect-error --- packages/angular/test/errorhandler.test.ts | 2 +- .../suites/replay/bufferMode/test.ts | 4 +- .../fetch/captureRequestBody/test.ts | 10 +- .../fetch/captureRequestHeaders/test.ts | 10 +- .../fetch/captureRequestSize/test.ts | 4 +- .../fetch/captureResponseBody/test.ts | 8 +- .../fetch/captureResponseHeaders/test.ts | 6 +- .../fetch/captureResponseSize/test.ts | 6 +- .../xhr/captureRequestBody/test.ts | 10 +- .../xhr/captureRequestHeaders/test.ts | 4 +- .../xhr/captureRequestSize/test.ts | 4 +- .../xhr/captureResponseBody/test.ts | 8 +- .../xhr/captureResponseHeaders/test.ts | 4 +- .../xhr/captureResponseSize/test.ts | 6 +- .../suites/replay/multiple-pages/test.ts | 18 +-- .../suites/wasm/test.ts | 2 +- .../utils/replayHelpers.ts | 2 +- packages/browser/src/integrations/dedupe.ts | 2 +- .../browser/src/profiling/hubextensions.ts | 2 +- packages/browser/src/profiling/utils.ts | 4 +- packages/browser/src/transports/offline.ts | 4 +- .../test/unit/integrations/helpers.test.ts | 10 +- .../test/unit/profiling/hubextensions.test.ts | 30 ++-- .../browser/test/unit/profiling/utils.test.ts | 26 ++-- packages/core/src/baseclient.ts | 3 +- packages/core/src/hub.ts | 2 +- .../core/src/integrations/inboundfilters.ts | 4 +- packages/core/test/lib/base.test.ts | 6 +- packages/core/test/lib/hint.test.ts | 2 +- .../core/test/lib/serverruntimeclient.test.ts | 4 +- packages/core/test/lib/tracing/trace.test.ts | 2 +- .../core/test/lib/transports/offline.test.ts | 1 - .../tests/async-context-edge.test.ts | 4 +- .../sveltekit/test/transaction.test.ts | 2 +- packages/hub/test/global.test.ts | 2 +- packages/hub/test/scope.test.ts | 12 +- packages/integrations/src/dedupe.ts | 2 +- .../integrations/test/captureconsole.test.ts | 2 +- .../integrations/test/extraerrordata.test.ts | 3 +- .../integrations/test/rewriteframes.test.ts | 2 +- .../config/templates/apiWrapperTemplate.ts | 4 +- .../templates/middlewareWrapperTemplate.ts | 4 +- .../config/templates/pageWrapperTemplate.ts | 4 +- .../templates/routeHandlerWrapperTemplate.ts | 8 +- .../templates/sentryInitWrapperTemplate.ts | 6 +- .../serverComponentWrapperTemplate.ts | 6 +- .../nextjs/src/config/withSentryConfig.ts | 2 +- packages/nextjs/test/clientSdk.test.ts | 4 +- .../webpack/constructWebpackConfig.test.ts | 2 +- .../nextjs/test/edge/edgeWrapperUtils.test.ts | 12 +- packages/nextjs/test/edge/transport.test.ts | 6 +- .../nextjs/test/edge/withSentryAPI.test.ts | 12 +- .../pages/[id]/withInitialProps.tsx | 3 +- .../pages/[id]/withServerSideProps.tsx | 3 +- .../test/integration/pages/api/requireTest.ts | 2 +- .../nextjs/test/integration/pages/crashed.tsx | 3 +- .../tracingClientGetInitialProps.test.ts | 2 +- .../tracingClientGetServerSideProps.test.ts | 2 +- .../test/client/tracingFetch.test.ts | 2 +- .../integration/test/server/utils/helpers.ts | 2 +- packages/nextjs/test/serverSdk.test.ts | 12 +- .../node-experimental/test/sdk/init.test.ts | 10 +- .../suites/sessions/server.ts | 10 +- packages/node/src/integrations/requestdata.ts | 2 +- .../node/src/integrations/undici/index.ts | 2 +- packages/node/test/client.test.ts | 4 +- packages/node/test/index.test.ts | 1 - packages/node/test/integrations/http.test.ts | 4 +- .../node/test/integrations/undici.test.ts | 4 +- packages/node/test/requestdata.test.ts | 1 - packages/node/test/sdk.test.ts | 8 +- packages/node/test/transports/http.test.ts | 2 +- packages/node/test/transports/https.test.ts | 2 +- .../test/propagator.test.ts | 2 +- .../test/spanprocessor.test.ts | 3 +- packages/overhead-metrics/src/perf/sampler.ts | 2 +- packages/react/src/reactrouter.tsx | 4 +- packages/react/src/reactrouterv6.tsx | 4 +- packages/react/test/errorboundary.test.tsx | 14 +- packages/react/test/reactrouterv6.4.test.tsx | 16 +-- packages/remix/src/client/performance.tsx | 8 +- packages/remix/src/utils/web-fetch.ts | 4 +- packages/remix/test/index.client.test.ts | 4 +- packages/remix/test/index.server.test.ts | 4 +- packages/replay-worker/src/_worker.ts | 2 +- packages/replay-worker/src/handleMessage.ts | 8 +- .../test/unit/Compressor.test.ts | 2 +- packages/replay/jest.setup.ts | 12 +- .../src/coreHandlers/util/networkUtils.ts | 2 +- packages/replay/src/util/addMemoryEntry.ts | 2 +- .../src/util/createPerformanceEntries.ts | 7 +- packages/replay/src/util/isRrwebError.ts | 2 +- packages/replay/src/util/sendReplay.ts | 2 +- packages/replay/src/util/sendReplayRequest.ts | 2 +- .../coreHandlers/handleAfterSendEvent.test.ts | 2 +- .../coreHandlers/handleGlobalEvent.test.ts | 6 +- .../replay/test/integration/flush.test.ts | 6 +- .../replay/test/integration/sampling.test.ts | 6 +- .../replay/test/integration/session.test.ts | 2 +- packages/replay/test/integration/stop.test.ts | 2 +- packages/replay/test/mocks/resetSdkMock.ts | 2 +- .../unit/coreHandlers/handleScope.test.ts | 2 +- .../EventBufferCompressionWorker.test.ts | 4 +- .../replay/test/unit/util/addEvent.test.ts | 2 +- .../unit/util/createPerformanceEntry.test.ts | 2 +- .../unit/util/createReplayEnvelope.test.ts | 1 - .../test/unit/util/prepareReplayEvent.test.ts | 1 - packages/serverless/test/awslambda.test.ts | 56 ++++---- packages/serverless/test/awsservices.test.ts | 12 +- packages/serverless/test/gcpfunction.test.ts | 131 +++++++++--------- .../serverless/test/google-cloud-grpc.test.ts | 10 +- .../serverless/test/google-cloud-http.test.ts | 8 +- packages/sveltekit/src/vite/sourceMaps.ts | 10 +- packages/sveltekit/src/vite/svelteConfig.ts | 6 +- packages/sveltekit/test/client/router.test.ts | 14 +- packages/sveltekit/test/client/sdk.test.ts | 8 +- packages/sveltekit/test/server/load.test.ts | 2 +- packages/sveltekit/test/server/sdk.test.ts | 4 +- packages/sveltekit/test/server/utils.test.ts | 2 +- .../test/vite/autoInstrument.test.ts | 12 +- .../test/vite/injectGlobalValues.test.ts | 2 +- .../test/vite/sentrySvelteKitPlugins.test.ts | 4 +- .../sveltekit/test/vite/sourceMaps.test.ts | 18 +-- packages/sveltekit/test/vitest.setup.ts | 4 +- .../src/browser/metrics/index.ts | 4 +- .../test/browser/backgroundtab.test.ts | 8 +- .../test/browser/browsertracing.test.ts | 7 +- .../test/browser/request.test.ts | 2 +- .../test/browser/router.test.ts | 8 +- packages/tracing/test/idletransaction.test.ts | 2 +- packages/tracing/test/transaction.test.ts | 8 +- packages/utils/src/env.ts | 2 +- packages/utils/src/supports.ts | 2 +- packages/utils/test/browser.test.ts | 2 +- packages/utils/test/is.test.ts | 4 +- packages/utils/test/normalize.test.ts | 2 +- packages/utils/test/object.test.ts | 4 +- packages/utils/test/syncpromise.test.ts | 3 +- packages/utils/test/worldwide.test.ts | 2 +- packages/vue/test/errorHandler.test.ts | 4 +- 140 files changed, 441 insertions(+), 460 deletions(-) diff --git a/packages/angular/test/errorhandler.test.ts b/packages/angular/test/errorhandler.test.ts index 633d4d81f7e9..3a78d715de27 100644 --- a/packages/angular/test/errorhandler.test.ts +++ b/packages/angular/test/errorhandler.test.ts @@ -532,7 +532,7 @@ describe('SentryErrorHandler', () => { }), }; - // @ts-ignore this is a minmal hub, we're missing a few props but that's ok + // @ts-expect-error this is a minmal hub, we're missing a few props but that's ok jest.spyOn(SentryBrowser, 'getCurrentHub').mockImplementationOnce(() => { return { getClient: () => client }; }); diff --git a/packages/browser-integration-tests/suites/replay/bufferMode/test.ts b/packages/browser-integration-tests/suites/replay/bufferMode/test.ts index 5b9c0da33899..f049267a51c1 100644 --- a/packages/browser-integration-tests/suites/replay/bufferMode/test.ts +++ b/packages/browser-integration-tests/suites/replay/bufferMode/test.ts @@ -68,7 +68,7 @@ sentryTest( expect( await page.evaluate(() => { const replayIntegration = (window as unknown as Window & { Replay: InstanceType }).Replay; - // @ts-ignore private + // @ts-expect-error private const replay = replayIntegration._replay; replayIntegration.startBuffering(); return replay.isEnabled(); @@ -210,7 +210,7 @@ sentryTest( expect( await page.evaluate(() => { const replayIntegration = (window as unknown as Window & { Replay: InstanceType }).Replay; - // @ts-ignore private + // @ts-expect-error private const replay = replayIntegration._replay; replayIntegration.startBuffering(); return replay.isEnabled(); diff --git a/packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/captureRequestBody/test.ts b/packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/captureRequestBody/test.ts index f526c60d6fb1..4f29b0422d2a 100644 --- a/packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/captureRequestBody/test.ts +++ b/packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/captureRequestBody/test.ts @@ -41,7 +41,7 @@ sentryTest('captures text request body', async ({ getLocalTestPath, page, browse method: 'POST', body: 'input body', }).then(() => { - // @ts-ignore Sentry is a global + // @ts-expect-error Sentry is a global Sentry.captureException('test error'); }); /* eslint-enable */ @@ -120,7 +120,7 @@ sentryTest('captures JSON request body', async ({ getLocalTestPath, page, browse method: 'POST', body: '{"foo":"bar"}', }).then(() => { - // @ts-ignore Sentry is a global + // @ts-expect-error Sentry is a global Sentry.captureException('test error'); }); /* eslint-enable */ @@ -203,7 +203,7 @@ sentryTest('captures non-text request body', async ({ getLocalTestPath, page, br method: 'POST', body: body, }).then(() => { - // @ts-ignore Sentry is a global + // @ts-expect-error Sentry is a global Sentry.captureException('test error'); }); /* eslint-enable */ @@ -282,7 +282,7 @@ sentryTest('captures text request body when matching relative URL', async ({ get method: 'POST', body: 'input body', }).then(() => { - // @ts-ignore Sentry is a global + // @ts-expect-error Sentry is a global Sentry.captureException('test error'); }); /* eslint-enable */ @@ -359,7 +359,7 @@ sentryTest('does not capture request body when URL does not match', async ({ get method: 'POST', body: 'input body', }).then(() => { - // @ts-ignore Sentry is a global + // @ts-expect-error Sentry is a global Sentry.captureException('test error'); }); /* eslint-enable */ diff --git a/packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/captureRequestHeaders/test.ts b/packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/captureRequestHeaders/test.ts index 4b1b1d882eb2..8c119390eadc 100644 --- a/packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/captureRequestHeaders/test.ts +++ b/packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/captureRequestHeaders/test.ts @@ -38,7 +38,7 @@ sentryTest('handles empty/missing request headers', async ({ getLocalTestPath, p fetch('http://localhost:7654/foo', { method: 'POST', }).then(() => { - // @ts-ignore Sentry is a global + // @ts-expect-error Sentry is a global Sentry.captureException('test error'); }); /* eslint-enable */ @@ -117,7 +117,7 @@ sentryTest('captures request headers as POJO', async ({ getLocalTestPath, page, 'X-Test-Header': 'test-value', }, }).then(() => { - // @ts-ignore Sentry is a global + // @ts-expect-error Sentry is a global Sentry.captureException('test error'); }); /* eslint-enable */ @@ -201,7 +201,7 @@ sentryTest('captures request headers on Request', async ({ getLocalTestPath, pag }); /* eslint-disable */ fetch(request).then(() => { - // @ts-ignore Sentry is a global + // @ts-expect-error Sentry is a global Sentry.captureException('test error'); }); /* eslint-enable */ @@ -284,7 +284,7 @@ sentryTest('captures request headers as Headers instance', async ({ getLocalTest method: 'POST', headers, }).then(() => { - // @ts-ignore Sentry is a global + // @ts-expect-error Sentry is a global Sentry.captureException('test error'); }); /* eslint-enable */ @@ -367,7 +367,7 @@ sentryTest('does not captures request headers if URL does not match', async ({ g 'X-Test-Header': 'test-value', }, }).then(() => { - // @ts-ignore Sentry is a global + // @ts-expect-error Sentry is a global Sentry.captureException('test error'); }); /* eslint-enable */ diff --git a/packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/captureRequestSize/test.ts b/packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/captureRequestSize/test.ts index 712210558176..3e250bd20df3 100644 --- a/packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/captureRequestSize/test.ts +++ b/packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/captureRequestSize/test.ts @@ -39,7 +39,7 @@ sentryTest('captures request body size when body is sent', async ({ getLocalTest method: 'POST', body: '{"foo":"bar"}', }).then(() => { - // @ts-ignore Sentry is a global + // @ts-expect-error Sentry is a global Sentry.captureException('test error'); }); /* eslint-enable */ @@ -125,7 +125,7 @@ sentryTest('captures request size from non-text request body', async ({ getLocal method: 'POST', body: blob, }).then(() => { - // @ts-ignore Sentry is a global + // @ts-expect-error Sentry is a global Sentry.captureException('test error'); }); /* eslint-enable */ diff --git a/packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/captureResponseBody/test.ts b/packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/captureResponseBody/test.ts index 1741b6a19803..b1c0a496476e 100644 --- a/packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/captureResponseBody/test.ts +++ b/packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/captureResponseBody/test.ts @@ -41,7 +41,7 @@ sentryTest('captures text response body', async ({ getLocalTestPath, page, brows fetch('http://localhost:7654/foo', { method: 'POST', }).then(() => { - // @ts-ignore Sentry is a global + // @ts-expect-error Sentry is a global Sentry.captureException('test error'); }); /* eslint-enable */ @@ -122,7 +122,7 @@ sentryTest('captures JSON response body', async ({ getLocalTestPath, page, brows fetch('http://localhost:7654/foo', { method: 'POST', }).then(() => { - // @ts-ignore Sentry is a global + // @ts-expect-error Sentry is a global Sentry.captureException('test error'); }); /* eslint-enable */ @@ -203,7 +203,7 @@ sentryTest('captures non-text response body', async ({ getLocalTestPath, page, b fetch('http://localhost:7654/foo', { method: 'POST', }).then(() => { - // @ts-ignore Sentry is a global + // @ts-expect-error Sentry is a global Sentry.captureException('test error'); }); /* eslint-enable */ @@ -282,7 +282,7 @@ sentryTest('does not capture response body when URL does not match', async ({ ge fetch('http://localhost:7654/bar', { method: 'POST', }).then(() => { - // @ts-ignore Sentry is a global + // @ts-expect-error Sentry is a global Sentry.captureException('test error'); }); /* eslint-enable */ diff --git a/packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/captureResponseHeaders/test.ts b/packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/captureResponseHeaders/test.ts index b377e1667ee6..93fe566c6bb6 100644 --- a/packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/captureResponseHeaders/test.ts +++ b/packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/captureResponseHeaders/test.ts @@ -38,7 +38,7 @@ sentryTest('handles empty headers', async ({ getLocalTestPath, page, browserName await page.evaluate(() => { /* eslint-disable */ fetch('http://localhost:7654/foo').then(() => { - // @ts-ignore Sentry is a global + // @ts-expect-error Sentry is a global Sentry.captureException('test error'); }); /* eslint-enable */ @@ -113,7 +113,7 @@ sentryTest('captures response headers', async ({ getLocalTestPath, page }) => { await page.evaluate(() => { /* eslint-disable */ fetch('http://localhost:7654/foo').then(() => { - // @ts-ignore Sentry is a global + // @ts-expect-error Sentry is a global Sentry.captureException('test error'); }); /* eslint-enable */ @@ -194,7 +194,7 @@ sentryTest('does not capture response headers if URL does not match', async ({ g await page.evaluate(() => { /* eslint-disable */ fetch('http://localhost:7654/bar').then(() => { - // @ts-ignore Sentry is a global + // @ts-expect-error Sentry is a global Sentry.captureException('test error'); }); /* eslint-enable */ diff --git a/packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/captureResponseSize/test.ts b/packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/captureResponseSize/test.ts index 3604f270ec23..cba36c1814b9 100644 --- a/packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/captureResponseSize/test.ts +++ b/packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/fetch/captureResponseSize/test.ts @@ -43,7 +43,7 @@ sentryTest('captures response size from Content-Length header if available', asy await page.evaluate(() => { /* eslint-disable */ fetch('http://localhost:7654/foo').then(() => { - // @ts-ignore Sentry is a global + // @ts-expect-error Sentry is a global Sentry.captureException('test error'); }); /* eslint-enable */ @@ -131,7 +131,7 @@ sentryTest('captures response size without Content-Length header', async ({ getL await page.evaluate(() => { /* eslint-disable */ fetch('http://localhost:7654/foo').then(() => { - // @ts-ignore Sentry is a global + // @ts-expect-error Sentry is a global Sentry.captureException('test error'); }); /* eslint-enable */ @@ -218,7 +218,7 @@ sentryTest('captures response size from non-text response body', async ({ getLoc fetch('http://localhost:7654/foo', { method: 'POST', }).then(() => { - // @ts-ignore Sentry is a global + // @ts-expect-error Sentry is a global Sentry.captureException('test error'); }); /* eslint-enable */ diff --git a/packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/xhr/captureRequestBody/test.ts b/packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/xhr/captureRequestBody/test.ts index 6fc19f18f9c7..b2d4fddaad9e 100644 --- a/packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/xhr/captureRequestBody/test.ts +++ b/packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/xhr/captureRequestBody/test.ts @@ -43,7 +43,7 @@ sentryTest('captures text request body', async ({ getLocalTestPath, page, browse xhr.addEventListener('readystatechange', function () { if (xhr.readyState === 4) { - // @ts-ignore Sentry is a global + // @ts-expect-error Sentry is a global setTimeout(() => Sentry.captureException('test error', 0)); } }); @@ -124,7 +124,7 @@ sentryTest('captures JSON request body', async ({ getLocalTestPath, page, browse xhr.addEventListener('readystatechange', function () { if (xhr.readyState === 4) { - // @ts-ignore Sentry is a global + // @ts-expect-error Sentry is a global setTimeout(() => Sentry.captureException('test error', 0)); } }); @@ -209,7 +209,7 @@ sentryTest('captures non-text request body', async ({ getLocalTestPath, page, br xhr.addEventListener('readystatechange', function () { if (xhr.readyState === 4) { - // @ts-ignore Sentry is a global + // @ts-expect-error Sentry is a global setTimeout(() => Sentry.captureException('test error', 0)); } }); @@ -290,7 +290,7 @@ sentryTest('captures text request body when matching relative URL', async ({ get xhr.addEventListener('readystatechange', function () { if (xhr.readyState === 4) { - // @ts-ignore Sentry is a global + // @ts-expect-error Sentry is a global setTimeout(() => Sentry.captureException('test error', 0)); } }); @@ -371,7 +371,7 @@ sentryTest('does not capture request body when URL does not match', async ({ get xhr.addEventListener('readystatechange', function () { if (xhr.readyState === 4) { - // @ts-ignore Sentry is a global + // @ts-expect-error Sentry is a global setTimeout(() => Sentry.captureException('test error', 0)); } }); diff --git a/packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/xhr/captureRequestHeaders/test.ts b/packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/xhr/captureRequestHeaders/test.ts index 3a341f9df8a6..08fcf0a25446 100644 --- a/packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/xhr/captureRequestHeaders/test.ts +++ b/packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/xhr/captureRequestHeaders/test.ts @@ -47,7 +47,7 @@ sentryTest('captures request headers', async ({ getLocalTestPath, page, browserN xhr.addEventListener('readystatechange', function () { if (xhr.readyState === 4) { - // @ts-ignore Sentry is a global + // @ts-expect-error Sentry is a global setTimeout(() => Sentry.captureException('test error', 0)); } }); @@ -135,7 +135,7 @@ sentryTest( xhr.addEventListener('readystatechange', function () { if (xhr.readyState === 4) { - // @ts-ignore Sentry is a global + // @ts-expect-error Sentry is a global setTimeout(() => Sentry.captureException('test error', 0)); } }); diff --git a/packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/xhr/captureRequestSize/test.ts b/packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/xhr/captureRequestSize/test.ts index 83461fd61486..15e5cc431d35 100644 --- a/packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/xhr/captureRequestSize/test.ts +++ b/packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/xhr/captureRequestSize/test.ts @@ -43,7 +43,7 @@ sentryTest('captures request body size when body is sent', async ({ getLocalTest xhr.addEventListener('readystatechange', function () { if (xhr.readyState === 4) { - // @ts-ignore Sentry is a global + // @ts-expect-error Sentry is a global setTimeout(() => Sentry.captureException('test error', 0)); } }); @@ -134,7 +134,7 @@ sentryTest('captures request size from non-text request body', async ({ getLocal xhr.addEventListener('readystatechange', function () { if (xhr.readyState === 4) { - // @ts-ignore Sentry is a global + // @ts-expect-error Sentry is a global setTimeout(() => Sentry.captureException('test error', 0)); } }); diff --git a/packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/xhr/captureResponseBody/test.ts b/packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/xhr/captureResponseBody/test.ts index 4e0eb915f98a..46e20da391cc 100644 --- a/packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/xhr/captureResponseBody/test.ts +++ b/packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/xhr/captureResponseBody/test.ts @@ -47,7 +47,7 @@ sentryTest('captures text response body', async ({ getLocalTestPath, page, brows xhr.addEventListener('readystatechange', function () { if (xhr.readyState === 4) { - // @ts-ignore Sentry is a global + // @ts-expect-error Sentry is a global setTimeout(() => Sentry.captureException('test error', 0)); } }); @@ -132,7 +132,7 @@ sentryTest('captures JSON response body', async ({ getLocalTestPath, page, brows xhr.addEventListener('readystatechange', function () { if (xhr.readyState === 4) { - // @ts-ignore Sentry is a global + // @ts-expect-error Sentry is a global setTimeout(() => Sentry.captureException('test error', 0)); } }); @@ -217,7 +217,7 @@ sentryTest('captures non-text response body', async ({ getLocalTestPath, page, b xhr.addEventListener('readystatechange', function () { if (xhr.readyState === 4) { - // @ts-ignore Sentry is a global + // @ts-expect-error Sentry is a global setTimeout(() => Sentry.captureException('test error', 0)); } }); @@ -304,7 +304,7 @@ sentryTest( xhr.addEventListener('readystatechange', function () { if (xhr.readyState === 4) { - // @ts-ignore Sentry is a global + // @ts-expect-error Sentry is a global setTimeout(() => Sentry.captureException('test error', 0)); } }); diff --git a/packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/xhr/captureResponseHeaders/test.ts b/packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/xhr/captureResponseHeaders/test.ts index ac80334663d8..ed2c2f5b2765 100644 --- a/packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/xhr/captureResponseHeaders/test.ts +++ b/packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/xhr/captureResponseHeaders/test.ts @@ -50,7 +50,7 @@ sentryTest('captures response headers', async ({ getLocalTestPath, page, browser xhr.addEventListener('readystatechange', function () { if (xhr.readyState === 4) { - // @ts-ignore Sentry is a global + // @ts-expect-error Sentry is a global setTimeout(() => Sentry.captureException('test error', 0)); } }); @@ -141,7 +141,7 @@ sentryTest( xhr.addEventListener('readystatechange', function () { if (xhr.readyState === 4) { - // @ts-ignore Sentry is a global + // @ts-expect-error Sentry is a global setTimeout(() => Sentry.captureException('test error', 0)); } }); diff --git a/packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/xhr/captureResponseSize/test.ts b/packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/xhr/captureResponseSize/test.ts index cf3de69d8fd4..ea0d6240c8e9 100644 --- a/packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/xhr/captureResponseSize/test.ts +++ b/packages/browser-integration-tests/suites/replay/extendNetworkBreadcrumbs/xhr/captureResponseSize/test.ts @@ -48,7 +48,7 @@ sentryTest( xhr.addEventListener('readystatechange', function () { if (xhr.readyState === 4) { - // @ts-ignore Sentry is a global + // @ts-expect-error Sentry is a global setTimeout(() => Sentry.captureException('test error', 0)); } }); @@ -144,7 +144,7 @@ sentryTest('captures response size without Content-Length header', async ({ getL xhr.addEventListener('readystatechange', function () { if (xhr.readyState === 4) { - // @ts-ignore Sentry is a global + // @ts-expect-error Sentry is a global setTimeout(() => Sentry.captureException('test error', 0)); } }); @@ -237,7 +237,7 @@ sentryTest('captures response size for non-string bodies', async ({ getLocalTest xhr.addEventListener('readystatechange', function () { if (xhr.readyState === 4) { - // @ts-ignore Sentry is a global + // @ts-expect-error Sentry is a global setTimeout(() => Sentry.captureException('test error', 0)); } }); diff --git a/packages/browser-integration-tests/suites/replay/multiple-pages/test.ts b/packages/browser-integration-tests/suites/replay/multiple-pages/test.ts index c54baf8be6f4..956397c84a49 100644 --- a/packages/browser-integration-tests/suites/replay/multiple-pages/test.ts +++ b/packages/browser-integration-tests/suites/replay/multiple-pages/test.ts @@ -146,13 +146,13 @@ sentryTest( expect(replayEvent4).toEqual( getExpectedReplayEvent({ segment_id: 4, - // @ts-ignore this is fine + // @ts-expect-error this is fine urls: [expect.stringContaining('page-0.html')], request: { - // @ts-ignore this is fine + // @ts-expect-error this is fine url: expect.stringContaining('page-0.html'), headers: { - // @ts-ignore this is fine + // @ts-expect-error this is fine 'User-Agent': expect.stringContaining(''), }, }, @@ -172,10 +172,10 @@ sentryTest( segment_id: 5, urls: [], request: { - // @ts-ignore this is fine + // @ts-expect-error this is fine url: expect.stringContaining('page-0.html'), headers: { - // @ts-ignore this is fine + // @ts-expect-error this is fine 'User-Agent': expect.stringContaining(''), }, }, @@ -219,10 +219,10 @@ sentryTest( urls: ['/spa'], request: { - // @ts-ignore this is fine + // @ts-expect-error this is fine url: expect.stringContaining('page-0.html'), headers: { - // @ts-ignore this is fine + // @ts-expect-error this is fine 'User-Agent': expect.stringContaining(''), }, }, @@ -243,10 +243,10 @@ sentryTest( urls: [], request: { - // @ts-ignore this is fine + // @ts-expect-error this is fine url: expect.stringContaining('page-0.html'), headers: { - // @ts-ignore this is fine + // @ts-expect-error this is fine 'User-Agent': expect.stringContaining(''), }, }, diff --git a/packages/browser-integration-tests/suites/wasm/test.ts b/packages/browser-integration-tests/suites/wasm/test.ts index f88a4d2cdb16..9ecad4c8b5cf 100644 --- a/packages/browser-integration-tests/suites/wasm/test.ts +++ b/packages/browser-integration-tests/suites/wasm/test.ts @@ -29,7 +29,7 @@ sentryTest( await page.goto(url); const event = await page.evaluate(async () => { - // @ts-ignore this function exists + // @ts-expect-error this function exists return window.getEvent(); }); diff --git a/packages/browser-integration-tests/utils/replayHelpers.ts b/packages/browser-integration-tests/utils/replayHelpers.ts index bbc02d50494f..0e03019a7463 100644 --- a/packages/browser-integration-tests/utils/replayHelpers.ts +++ b/packages/browser-integration-tests/utils/replayHelpers.ts @@ -401,6 +401,6 @@ function normalizeNumberAttribute(num: number): string { /** Get a request from either a request or a response */ function getRequest(resOrReq: Request | Response): Request { - // @ts-ignore we check this + // @ts-expect-error we check this return typeof resOrReq.request === 'function' ? (resOrReq as Response).request() : (resOrReq as Request); } diff --git a/packages/browser/src/integrations/dedupe.ts b/packages/browser/src/integrations/dedupe.ts index 05bd88a8c786..673ded6484cb 100644 --- a/packages/browser/src/integrations/dedupe.ts +++ b/packages/browser/src/integrations/dedupe.ts @@ -204,7 +204,7 @@ function _getFramesFromEvent(event: Event): StackFrame[] | undefined { if (exception) { try { - // @ts-ignore Object could be undefined + // @ts-expect-error Object could be undefined return exception.values[0].stacktrace.frames; } catch (_oO) { return undefined; diff --git a/packages/browser/src/profiling/hubextensions.ts b/packages/browser/src/profiling/hubextensions.ts index 187e4a463224..aadcb541dbb0 100644 --- a/packages/browser/src/profiling/hubextensions.ts +++ b/packages/browser/src/profiling/hubextensions.ts @@ -71,7 +71,7 @@ export function wrapTransactionWithProfiling(transaction: Transaction): Transact return transaction; } - // @ts-ignore profilesSampleRate is not part of the browser options yet + // @ts-expect-error profilesSampleRate is not part of the browser options yet const profilesSampleRate: number | boolean | undefined = options.profilesSampleRate; // Since this is coming from the user (or from a function provided by the user), who knows what we might get. (The diff --git a/packages/browser/src/profiling/utils.ts b/packages/browser/src/profiling/utils.ts index e720a2152f9f..40944fc260fe 100644 --- a/packages/browser/src/profiling/utils.ts +++ b/packages/browser/src/profiling/utils.ts @@ -44,7 +44,7 @@ function isUserAgentData(data: unknown): data is UserAgentData { return typeof data === 'object' && data !== null && 'getHighEntropyValues' in data; } -// @ts-ignore userAgentData is not part of the navigator interface yet +// @ts-expect-error userAgentData is not part of the navigator interface yet const userAgentData = WINDOW.navigator && WINDOW.navigator.userAgentData; if (isUserAgentData(userAgentData)) { @@ -290,7 +290,7 @@ export function addProfilesToEnvelope(envelope: Envelope, profiles: Profile[]): } for (const profile of profiles) { - // @ts-ignore untyped envelope + // @ts-expect-error untyped envelope envelope[1].push([{ type: 'profile' }, profile]); } return envelope; diff --git a/packages/browser/src/transports/offline.ts b/packages/browser/src/transports/offline.ts index 8f7a399e3fd2..099e6ad462c1 100644 --- a/packages/browser/src/transports/offline.ts +++ b/packages/browser/src/transports/offline.ts @@ -28,9 +28,9 @@ type Store = (callback: (store: IDBObjectStore) => T | PromiseLike) => Pro function promisifyRequest(request: IDBRequest | IDBTransaction): Promise { return new Promise((resolve, reject) => { - // @ts-ignore - file size hacks + // @ts-expect-error - file size hacks request.oncomplete = request.onsuccess = () => resolve(request.result); - // @ts-ignore - file size hacks + // @ts-expect-error - file size hacks request.onabort = request.onerror = () => reject(request.error); }); } diff --git a/packages/browser/test/unit/integrations/helpers.test.ts b/packages/browser/test/unit/integrations/helpers.test.ts index 5b06835f834d..34420a6d30bc 100644 --- a/packages/browser/test/unit/integrations/helpers.test.ts +++ b/packages/browser/test/unit/integrations/helpers.test.ts @@ -12,13 +12,13 @@ describe('internal wrap()', () => { const num = 42; expect(wrap(fn)).not.toBe(fn); - // @ts-ignore Issue with `WrappedFunction` type from wrap fn + // @ts-expect-error Issue with `WrappedFunction` type from wrap fn expect(wrap(obj)).toBe(obj); - // @ts-ignore Issue with `WrappedFunction` type from wrap fn + // @ts-expect-error Issue with `WrappedFunction` type from wrap fn expect(wrap(arr)).toBe(arr); - // @ts-ignore Issue with `WrappedFunction` type from wrap fn + // @ts-expect-error Issue with `WrappedFunction` type from wrap fn expect(wrap(str)).toBe(str); - // @ts-ignore Issue with `WrappedFunction` type from wrap fn + // @ts-expect-error Issue with `WrappedFunction` type from wrap fn expect(wrap(num)).toBe(num); }); @@ -134,7 +134,7 @@ describe('internal wrap()', () => { return; }, }; - // @ts-ignore eventFn does not have property handleEvent + // @ts-expect-error eventFn does not have property handleEvent context.eventFn.handleEvent = function (): void { expect(this).toBe(context); }; diff --git a/packages/browser/test/unit/profiling/hubextensions.test.ts b/packages/browser/test/unit/profiling/hubextensions.test.ts index 66bd6191f2ee..26d836b12b02 100644 --- a/packages/browser/test/unit/profiling/hubextensions.test.ts +++ b/packages/browser/test/unit/profiling/hubextensions.test.ts @@ -1,7 +1,7 @@ import { TextDecoder, TextEncoder } from 'util'; -// @ts-ignore patch the encoder on the window, else importing JSDOM fails (deleted in afterAll) +// @ts-expect-error patch the encoder on the window, else importing JSDOM fails (deleted in afterAll) const patchedEncoder = (!global.window.TextEncoder && (global.window.TextEncoder = TextEncoder)) || true; -// @ts-ignore patch the encoder on the window, else importing JSDOM fails (deleted in afterAll) +// @ts-expect-error patch the encoder on the window, else importing JSDOM fails (deleted in afterAll) const patchedDecoder = (!global.window.TextDecoder && (global.window.TextDecoder = TextDecoder)) || true; import { getCurrentHub } from '@sentry/core'; @@ -10,21 +10,21 @@ import { JSDOM } from 'jsdom'; import { onProfilingStartRouteTransaction } from '../../../src'; -// @ts-ignore store a reference so we can reset it later +// @ts-expect-error store a reference so we can reset it later const globalDocument = global.document; -// @ts-ignore store a reference so we can reset it later +// @ts-expect-error store a reference so we can reset it later const globalWindow = global.window; -// @ts-ignore store a reference so we can reset it later +// @ts-expect-error store a reference so we can reset it later const globalLocation = global.location; describe('BrowserProfilingIntegration', () => { beforeEach(() => { const dom = new JSDOM(); - // @ts-ignore need to override global document + // @ts-expect-error need to override global document global.document = dom.window.document; - // @ts-ignore need to override global document + // @ts-expect-error need to override global document global.window = dom.window; - // @ts-ignore need to override global document + // @ts-expect-error need to override global document global.location = dom.window.location; const hub = getCurrentHub(); @@ -49,22 +49,22 @@ describe('BrowserProfilingIntegration', () => { // Reset back to previous values afterEach(() => { - // @ts-ignore need to override global document + // @ts-expect-error need to override global document global.document = globalDocument; - // @ts-ignore need to override global document + // @ts-expect-error need to override global document global.window = globalWindow; - // @ts-ignore need to override global document + // @ts-expect-error need to override global document global.location = globalLocation; }); afterAll(() => { - // @ts-ignore patch the encoder on the window, else importing JSDOM fails + // @ts-expect-error patch the encoder on the window, else importing JSDOM fails patchedEncoder && delete global.window.TextEncoder; - // @ts-ignore patch the encoder on the window, else importing JSDOM fails + // @ts-expect-error patch the encoder on the window, else importing JSDOM fails patchedDecoder && delete global.window.TextDecoder; }); it('does not throw if Profiler is not available', () => { - // @ts-ignore force api to be undefined + // @ts-expect-error force api to be undefined global.window.Profiler = undefined; // set sampled to true so that profiling does not early return const mockTransaction = { sampled: true } as Transaction; @@ -83,7 +83,7 @@ describe('BrowserProfilingIntegration', () => { // set sampled to true so that profiling does not early return const mockTransaction = { sampled: true } as Transaction; - // @ts-ignore override with our own constructor + // @ts-expect-error override with our own constructor global.window.Profiler = Profiler; expect(() => onProfilingStartRouteTransaction(mockTransaction)).not.toThrow(); expect(spy).toHaveBeenCalled(); diff --git a/packages/browser/test/unit/profiling/utils.test.ts b/packages/browser/test/unit/profiling/utils.test.ts index bbba7dbf5f08..a3141dfcb327 100644 --- a/packages/browser/test/unit/profiling/utils.test.ts +++ b/packages/browser/test/unit/profiling/utils.test.ts @@ -1,7 +1,7 @@ import { TextDecoder, TextEncoder } from 'util'; -// @ts-ignore patch the encoder on the window, else importing JSDOM fails (deleted in afterAll) +// @ts-expect-error patch the encoder on the window, else importing JSDOM fails (deleted in afterAll) const patchedEncoder = (!global.window.TextEncoder && (global.window.TextEncoder = TextEncoder)) || true; -// @ts-ignore patch the encoder on the window, else importing JSDOM fails (deleted in afterAll) +// @ts-expect-error patch the encoder on the window, else importing JSDOM fails (deleted in afterAll) const patchedDecoder = (!global.window.TextDecoder && (global.window.TextDecoder = TextDecoder)) || true; import { JSDOM } from 'jsdom'; @@ -19,38 +19,38 @@ const makeJSProfile = (partial: Partial = {}): JSSelfProfile => { }; }; -// @ts-ignore store a reference so we can reset it later +// @ts-expect-error store a reference so we can reset it later const globalDocument = global.document; -// @ts-ignore store a reference so we can reset it later +// @ts-expect-error store a reference so we can reset it later const globalWindow = global.window; -// @ts-ignore store a reference so we can reset it later +// @ts-expect-error store a reference so we can reset it later const globalLocation = global.location; describe('convertJSSelfProfileToSampledFormat', () => { beforeEach(() => { const dom = new JSDOM(); - // @ts-ignore need to override global document + // @ts-expect-error need to override global document global.document = dom.window.document; - // @ts-ignore need to override global document + // @ts-expect-error need to override global document global.window = dom.window; - // @ts-ignore need to override global document + // @ts-expect-error need to override global document global.location = dom.window.location; }); // Reset back to previous values afterEach(() => { - // @ts-ignore need to override global document + // @ts-expect-error need to override global document global.document = globalDocument; - // @ts-ignore need to override global document + // @ts-expect-error need to override global document global.window = globalWindow; - // @ts-ignore need to override global document + // @ts-expect-error need to override global document global.location = globalLocation; }); afterAll(() => { - // @ts-ignore patch the encoder on the window, else importing JSDOM fails + // @ts-expect-error patch the encoder on the window, else importing JSDOM fails patchedEncoder && delete global.window.TextEncoder; - // @ts-ignore patch the encoder on the window, else importing JSDOM fails + // @ts-expect-error patch the encoder on the window, else importing JSDOM fails patchedDecoder && delete global.window.TextDecoder; }); diff --git a/packages/core/src/baseclient.ts b/packages/core/src/baseclient.ts index cb106b958a53..7f22466cc281 100644 --- a/packages/core/src/baseclient.ts +++ b/packages/core/src/baseclient.ts @@ -407,7 +407,7 @@ export abstract class BaseClient implements Client { this._hooks[hook] = []; } - // @ts-ignore We assue the types are correct + // @ts-expect-error We assue the types are correct this._hooks[hook].push(callback); } @@ -435,7 +435,6 @@ export abstract class BaseClient implements Client { /** @inheritdoc */ public emit(hook: string, ...rest: unknown[]): void { if (this._hooks[hook]) { - // @ts-ignore we cannot enforce the callback to match the hook this._hooks[hook].forEach(callback => callback(...rest)); } } diff --git a/packages/core/src/hub.ts b/packages/core/src/hub.ts index 5961529be687..c25f9fe00f67 100644 --- a/packages/core/src/hub.ts +++ b/packages/core/src/hub.ts @@ -497,7 +497,7 @@ Sentry.init({...}); /** * Calls global extension method and binding current instance to the function call */ - // @ts-ignore Function lacks ending return statement and return type does not include 'undefined'. ts(2366) + // @ts-expect-error Function lacks ending return statement and return type does not include 'undefined'. ts(2366) // eslint-disable-next-line @typescript-eslint/no-explicit-any private _callExtensionMethod(method: string, ...args: any[]): T { const carrier = getMainCarrier(); diff --git a/packages/core/src/integrations/inboundfilters.ts b/packages/core/src/integrations/inboundfilters.ts index abe7948f2547..b00eda944ebe 100644 --- a/packages/core/src/integrations/inboundfilters.ts +++ b/packages/core/src/integrations/inboundfilters.ts @@ -187,7 +187,7 @@ function _getPossibleEventMessages(event: Event): string[] { function _isSentryError(event: Event): boolean { try { - // @ts-ignore can't be a sentry error if undefined + // @ts-expect-error can't be a sentry error if undefined // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access return event.exception.values[0].type === 'SentryError'; } catch (e) { @@ -212,7 +212,7 @@ function _getEventFilterUrl(event: Event): string | null { try { let frames; try { - // @ts-ignore we only care about frames if the whole thing here is defined + // @ts-expect-error we only care about frames if the whole thing here is defined frames = event.exception.values[0].stacktrace.frames; } catch (e) { // ignore diff --git a/packages/core/test/lib/base.test.ts b/packages/core/test/lib/base.test.ts index eed6b4cb3a2f..dd0906921fa3 100644 --- a/packages/core/test/lib/base.test.ts +++ b/packages/core/test/lib/base.test.ts @@ -722,7 +722,7 @@ describe('BaseClient', () => { test('skips empty integrations', () => { const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN, - // @ts-ignore we want to force invalid integrations here + // @ts-expect-error we want to force invalid integrations here integrations: [new TestIntegration(), null, undefined], }); const client = new TestClient(options); @@ -1046,7 +1046,7 @@ describe('BaseClient', () => { for (const val of invalidValues) { const beforeSend = jest.fn(() => val); - // @ts-ignore we need to test regular-js behavior + // @ts-expect-error we need to test regular-js behavior const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN, beforeSend }); const client = new TestClient(options); const loggerWarnSpy = jest.spyOn(logger, 'warn'); @@ -1067,7 +1067,7 @@ describe('BaseClient', () => { for (const val of invalidValues) { const beforeSendTransaction = jest.fn(() => val); - // @ts-ignore we need to test regular-js behavior + // @ts-expect-error we need to test regular-js behavior const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN, beforeSendTransaction }); const client = new TestClient(options); const loggerWarnSpy = jest.spyOn(logger, 'warn'); diff --git a/packages/core/test/lib/hint.test.ts b/packages/core/test/lib/hint.test.ts index bd795ed79c8e..656a70ab1a1d 100644 --- a/packages/core/test/lib/hint.test.ts +++ b/packages/core/test/lib/hint.test.ts @@ -16,7 +16,7 @@ describe('Hint', () => { afterEach(() => { jest.clearAllMocks(); - // @ts-ignore for testing + // @ts-expect-error for testing delete GLOBAL_OBJ.__SENTRY__; }); diff --git a/packages/core/test/lib/serverruntimeclient.test.ts b/packages/core/test/lib/serverruntimeclient.test.ts index 8f4c898fe580..a0cd2e5a3f96 100644 --- a/packages/core/test/lib/serverruntimeclient.test.ts +++ b/packages/core/test/lib/serverruntimeclient.test.ts @@ -78,7 +78,7 @@ describe('ServerRuntimeClient', () => { }); client = new ServerRuntimeClient(options); - // @ts-ignore accessing private method + // @ts-expect-error accessing private method const sendEnvelopeSpy = jest.spyOn(client, '_sendEnvelope'); const id = client.captureCheckIn( @@ -145,7 +145,7 @@ describe('ServerRuntimeClient', () => { const options = getDefaultClientOptions({ dsn: PUBLIC_DSN, serverName: 'bar', enabled: false }); client = new ServerRuntimeClient(options); - // @ts-ignore accessing private method + // @ts-expect-error accessing private method const sendEnvelopeSpy = jest.spyOn(client, '_sendEnvelope'); client.captureCheckIn({ monitorSlug: 'foo', status: 'in_progress' }); diff --git a/packages/core/test/lib/tracing/trace.test.ts b/packages/core/test/lib/tracing/trace.test.ts index e3469e93a2e2..2480d449a9d9 100644 --- a/packages/core/test/lib/tracing/trace.test.ts +++ b/packages/core/test/lib/tracing/trace.test.ts @@ -48,7 +48,7 @@ describe('startSpan', () => { }); it('should return the same value as the callback if transactions are undefined', async () => { - // @ts-ignore we are force overriding the transaction return to be undefined + // @ts-expect-error we are force overriding the transaction return to be undefined // The `startTransaction` types are actually wrong - it can return undefined // if tracingExtensions are not enabled jest.spyOn(hub, 'startTransaction').mockReturnValue(undefined); diff --git a/packages/core/test/lib/transports/offline.test.ts b/packages/core/test/lib/transports/offline.test.ts index e7174cb7c2e0..cdaf092fca5d 100644 --- a/packages/core/test/lib/transports/offline.test.ts +++ b/packages/core/test/lib/transports/offline.test.ts @@ -27,7 +27,6 @@ const ERROR_ENVELOPE = createEnvelope({ event_id: 'aa3ff046696b4b ]); const REPLAY_EVENT: ReplayEvent = { - // @ts-ignore private api type: 'replay_event', timestamp: 1670837008.634, error_ids: ['errorId'], diff --git a/packages/e2e-tests/test-applications/nextjs-app-dir/tests/async-context-edge.test.ts b/packages/e2e-tests/test-applications/nextjs-app-dir/tests/async-context-edge.test.ts index fb8e23530686..1d784b31c0e9 100644 --- a/packages/e2e-tests/test-applications/nextjs-app-dir/tests/async-context-edge.test.ts +++ b/packages/e2e-tests/test-applications/nextjs-app-dir/tests/async-context-edge.test.ts @@ -15,8 +15,8 @@ test('Should allow for async context isolation in the edge SDK', async ({ reques const outerSpan = asyncContextEdgerouteTransaction.spans?.find(span => span.description === 'outer-span'); const innerSpan = asyncContextEdgerouteTransaction.spans?.find(span => span.description === 'inner-span'); - // @ts-ignore parent_span_id exists + // @ts-expect-error parent_span_id exists expect(outerSpan?.parent_span_id).toStrictEqual(asyncContextEdgerouteTransaction.contexts?.trace?.span_id); - // @ts-ignore parent_span_id exists + // @ts-expect-error parent_span_id exists expect(innerSpan?.parent_span_id).toStrictEqual(asyncContextEdgerouteTransaction.contexts?.trace?.span_id); }); diff --git a/packages/e2e-tests/test-applications/sveltekit/test/transaction.test.ts b/packages/e2e-tests/test-applications/sveltekit/test/transaction.test.ts index 9cc72745e030..edc36cb5ca9c 100644 --- a/packages/e2e-tests/test-applications/sveltekit/test/transaction.test.ts +++ b/packages/e2e-tests/test-applications/sveltekit/test/transaction.test.ts @@ -1,5 +1,5 @@ import { test, expect } from '@playwright/test'; -// @ts-ignore ok ok +// @ts-expect-error ok ok import { waitForTransaction } from '../event-proxy-server.ts'; import axios, { AxiosError } from 'axios'; diff --git a/packages/hub/test/global.test.ts b/packages/hub/test/global.test.ts index 46b937112870..026cc0028dee 100644 --- a/packages/hub/test/global.test.ts +++ b/packages/hub/test/global.test.ts @@ -28,7 +28,7 @@ describe('global', () => { const newestHub = new Hub(undefined, undefined, 999999); GLOBAL_OBJ.__SENTRY__.hub = newestHub; const fn = jest.fn().mockImplementation(function (...args: []) { - // @ts-ignore typescript complains that this can be `any` + // @ts-expect-error typescript complains that this can be `any` expect(this).toBe(newestHub); expect(args).toEqual([1, 2, 3]); }); diff --git a/packages/hub/test/scope.test.ts b/packages/hub/test/scope.test.ts index edcaad465930..ed7b35f379c2 100644 --- a/packages/hub/test/scope.test.ts +++ b/packages/hub/test/scope.test.ts @@ -17,7 +17,7 @@ describe('Scope', () => { test('it creates a propagation context', () => { const scope = new Scope(); - // @ts-ignore asserting on private properties + // @ts-expect-error asserting on private properties expect(scope._propagationContext).toEqual({ traceId: expect.any(String), spanId: expect.any(String), @@ -230,7 +230,7 @@ describe('Scope', () => { const parentScope = new Scope(); const scope = Scope.clone(parentScope); - // @ts-ignore accessing private property for test + // @ts-expect-error accessing private property for test expect(scope._propagationContext).toEqual(parentScope._propagationContext); }); }); @@ -304,13 +304,13 @@ describe('Scope', () => { const scope = new Scope(); const event: Event = {}; - // @ts-ignore we want to be able to assign string value + // @ts-expect-error we want to be able to assign string value event.fingerprint = 'foo'; await scope.applyToEvent(event).then(processedEvent => { expect(processedEvent!.fingerprint).toEqual(['foo']); }); - // @ts-ignore we want to be able to assign string value + // @ts-expect-error we want to be able to assign string value event.fingerprint = 'bar'; await scope.applyToEvent(event).then(processedEvent => { expect(processedEvent!.fingerprint).toEqual(['bar']); @@ -478,7 +478,7 @@ describe('Scope', () => { }); test('given neither function, Scope or plain object, returns original scope', () => { - // @ts-ignore we want to be able to update scope with string + // @ts-expect-error we want to be able to update scope with string const updatedScope = scope.update('wat'); expect(updatedScope).toEqual(scope); }); @@ -540,7 +540,7 @@ describe('Scope', () => { expect(updatedScope._level).toEqual('warning'); expect(updatedScope._fingerprint).toEqual(['bar']); expect(updatedScope._requestSession.status).toEqual('ok'); - // @ts-ignore accessing private property for test + // @ts-expect-error accessing private property for test expect(updatedScope._propagationContext).toEqual(localScope._propagationContext); }); diff --git a/packages/integrations/src/dedupe.ts b/packages/integrations/src/dedupe.ts index 9467e7f6b6e1..8f156e76784d 100644 --- a/packages/integrations/src/dedupe.ts +++ b/packages/integrations/src/dedupe.ts @@ -204,7 +204,7 @@ function _getFramesFromEvent(event: Event): StackFrame[] | undefined { if (exception) { try { - // @ts-ignore Object could be undefined + // @ts-expect-error Object could be undefined return exception.values[0].stacktrace.frames; } catch (_oO) { return undefined; diff --git a/packages/integrations/test/captureconsole.test.ts b/packages/integrations/test/captureconsole.test.ts index 9a107f8dbd66..bddf7ef603dd 100644 --- a/packages/integrations/test/captureconsole.test.ts +++ b/packages/integrations/test/captureconsole.test.ts @@ -116,7 +116,7 @@ describe('CaptureConsole setup', () => { it('setup should fail gracefully when console is not available', () => { const consoleRef = GLOBAL_OBJ.console; - // @ts-ignore remove console + // @ts-expect-error remove console delete GLOBAL_OBJ.console; const captureConsoleIntegration = new CaptureConsole(); diff --git a/packages/integrations/test/extraerrordata.test.ts b/packages/integrations/test/extraerrordata.test.ts index 2ecee1faae19..bc7a6312a65a 100644 --- a/packages/integrations/test/extraerrordata.test.ts +++ b/packages/integrations/test/extraerrordata.test.ts @@ -69,7 +69,6 @@ describe('ExtraErrorData()', () => { it('should not remove previous data existing in extra field', () => { event = { - // @ts-ignore Allow contexts on event contexts: { foo: { bar: 42 }, }, @@ -107,7 +106,7 @@ describe('ExtraErrorData()', () => { it('should return event if there is no originalException', () => { const enhancedEvent = extraErrorData.enhanceEventWithErrorData(event, { - // @ts-ignore Allow event to have extra properties + // @ts-expect-error Allow event to have extra properties notOriginalException: 'fooled you', }); diff --git a/packages/integrations/test/rewriteframes.test.ts b/packages/integrations/test/rewriteframes.test.ts index b2a47d309360..749df9a862e1 100644 --- a/packages/integrations/test/rewriteframes.test.ts +++ b/packages/integrations/test/rewriteframes.test.ts @@ -108,7 +108,7 @@ describe('RewriteFrames', () => { }); it('ignore exception without StackTrace', () => { - // @ts-ignore Validates that the Stacktrace does not exist before validating the test. + // @ts-expect-error Validates that the Stacktrace does not exist before validating the test. expect(exceptionWithoutStackTrace.exception?.values[0].stacktrace).toEqual(undefined); const event = rewriteFrames.process(exceptionWithoutStackTrace); expect(event.exception!.values![0].stacktrace).toEqual(undefined); diff --git a/packages/nextjs/src/config/templates/apiWrapperTemplate.ts b/packages/nextjs/src/config/templates/apiWrapperTemplate.ts index 28d5e4efc806..4869d0c0d6e6 100644 --- a/packages/nextjs/src/config/templates/apiWrapperTemplate.ts +++ b/packages/nextjs/src/config/templates/apiWrapperTemplate.ts @@ -6,7 +6,7 @@ * this causes both TS and ESLint to complain, hence the pragma comments below. */ -// @ts-ignore See above +// @ts-expect-error See above // eslint-disable-next-line import/no-unresolved import * as origModule from '__SENTRY_WRAPPING_TARGET_FILE__'; // eslint-disable-next-line import/no-extraneous-dependencies @@ -68,6 +68,6 @@ export default wrappedHandler; // Re-export anything exported by the page module we're wrapping. When processing this code, Rollup is smart enough to // not include anything whose name matchs something we've explicitly exported above. -// @ts-ignore See above +// @ts-expect-error See above // eslint-disable-next-line import/no-unresolved export * from '__SENTRY_WRAPPING_TARGET_FILE__'; diff --git a/packages/nextjs/src/config/templates/middlewareWrapperTemplate.ts b/packages/nextjs/src/config/templates/middlewareWrapperTemplate.ts index 0c833023cffe..bd01f47ba236 100644 --- a/packages/nextjs/src/config/templates/middlewareWrapperTemplate.ts +++ b/packages/nextjs/src/config/templates/middlewareWrapperTemplate.ts @@ -5,7 +5,7 @@ * this causes both TS and ESLint to complain, hence the pragma comments below. */ -// @ts-ignore See above +// @ts-expect-error See above // eslint-disable-next-line import/no-unresolved import * as origModule from '__SENTRY_WRAPPING_TARGET_FILE__'; // eslint-disable-next-line import/no-extraneous-dependencies @@ -47,6 +47,6 @@ export default userProvidedDefaultHandler ? Sentry.wrapMiddlewareWithSentry(user // Re-export anything exported by the page module we're wrapping. When processing this code, Rollup is smart enough to // not include anything whose name matchs something we've explicitly exported above. -// @ts-ignore See above +// @ts-expect-error See above // eslint-disable-next-line import/no-unresolved export * from '__SENTRY_WRAPPING_TARGET_FILE__'; diff --git a/packages/nextjs/src/config/templates/pageWrapperTemplate.ts b/packages/nextjs/src/config/templates/pageWrapperTemplate.ts index b98087aec142..2e1b0d7b6537 100644 --- a/packages/nextjs/src/config/templates/pageWrapperTemplate.ts +++ b/packages/nextjs/src/config/templates/pageWrapperTemplate.ts @@ -6,7 +6,7 @@ * this causes both TS and ESLint to complain, hence the pragma comments below. */ -// @ts-ignore See above +// @ts-expect-error See above // eslint-disable-next-line import/no-unresolved import * as wrapee from '__SENTRY_WRAPPING_TARGET_FILE__'; // eslint-disable-next-line import/no-extraneous-dependencies @@ -52,6 +52,6 @@ export default pageComponent; // Re-export anything exported by the page module we're wrapping. When processing this code, Rollup is smart enough to // not include anything whose name matchs something we've explicitly exported above. -// @ts-ignore See above +// @ts-expect-error See above // eslint-disable-next-line import/no-unresolved export * from '__SENTRY_WRAPPING_TARGET_FILE__'; diff --git a/packages/nextjs/src/config/templates/routeHandlerWrapperTemplate.ts b/packages/nextjs/src/config/templates/routeHandlerWrapperTemplate.ts index 82fc8b1ad67a..930ab21eaadd 100644 --- a/packages/nextjs/src/config/templates/routeHandlerWrapperTemplate.ts +++ b/packages/nextjs/src/config/templates/routeHandlerWrapperTemplate.ts @@ -1,8 +1,8 @@ -// @ts-ignore Because we cannot be sure if the RequestAsyncStorage module exists (it is not part of the Next.js public +// @ts-expect-error Because we cannot be sure if the RequestAsyncStorage module exists (it is not part of the Next.js public // API) we use a shim if it doesn't exist. The logic for this is in the wrapping loader. // eslint-disable-next-line import/no-unresolved import { requestAsyncStorage } from '__SENTRY_NEXTJS_REQUEST_ASYNC_STORAGE_SHIM__'; -// @ts-ignore See above +// @ts-expect-error See above // eslint-disable-next-line import/no-unresolved import * as routeModule from '__SENTRY_WRAPPING_TARGET_FILE__'; // eslint-disable-next-line import/no-extraneous-dependencies @@ -58,11 +58,11 @@ function wrapHandler(handler: T, method: 'GET' | 'POST' | 'PUT' | 'PATCH' | ' }); } -// @ts-ignore See above +// @ts-expect-error See above // eslint-disable-next-line import/no-unresolved export * from '__SENTRY_WRAPPING_TARGET_FILE__'; -// @ts-ignore This is the file we're wrapping +// @ts-expect-error This is the file we're wrapping // eslint-disable-next-line import/no-unresolved export { default } from '__SENTRY_WRAPPING_TARGET_FILE__'; diff --git a/packages/nextjs/src/config/templates/sentryInitWrapperTemplate.ts b/packages/nextjs/src/config/templates/sentryInitWrapperTemplate.ts index 1720c3b62672..085154b18faa 100644 --- a/packages/nextjs/src/config/templates/sentryInitWrapperTemplate.ts +++ b/packages/nextjs/src/config/templates/sentryInitWrapperTemplate.ts @@ -1,11 +1,11 @@ -// @ts-ignore This will be replaced with the user's sentry config gile +// @ts-expect-error This will be replaced with the user's sentry config gile // eslint-disable-next-line import/no-unresolved import '__SENTRY_CONFIG_IMPORT_PATH__'; -// @ts-ignore This is the file we're wrapping +// @ts-expect-error This is the file we're wrapping // eslint-disable-next-line import/no-unresolved export * from '__SENTRY_WRAPPING_TARGET_FILE__'; -// @ts-ignore This is the file we're wrapping +// @ts-expect-error This is the file we're wrapping // eslint-disable-next-line import/no-unresolved export { default } from '__SENTRY_WRAPPING_TARGET_FILE__'; diff --git a/packages/nextjs/src/config/templates/serverComponentWrapperTemplate.ts b/packages/nextjs/src/config/templates/serverComponentWrapperTemplate.ts index 7ebf29099f3a..6730202ecaa5 100644 --- a/packages/nextjs/src/config/templates/serverComponentWrapperTemplate.ts +++ b/packages/nextjs/src/config/templates/serverComponentWrapperTemplate.ts @@ -1,8 +1,8 @@ -// @ts-ignore Because we cannot be sure if the RequestAsyncStorage module exists (it is not part of the Next.js public +// @ts-expect-error Because we cannot be sure if the RequestAsyncStorage module exists (it is not part of the Next.js public // API) we use a shim if it doesn't exist. The logic for this is in the wrapping loader. // eslint-disable-next-line import/no-unresolved import { requestAsyncStorage } from '__SENTRY_NEXTJS_REQUEST_ASYNC_STORAGE_SHIM__'; -// @ts-ignore We use `__SENTRY_WRAPPING_TARGET_FILE__` as a placeholder for the path to the file being wrapped. +// @ts-expect-error We use `__SENTRY_WRAPPING_TARGET_FILE__` as a placeholder for the path to the file being wrapped. // eslint-disable-next-line import/no-unresolved import * as serverComponentModule from '__SENTRY_WRAPPING_TARGET_FILE__'; // eslint-disable-next-line import/no-extraneous-dependencies @@ -51,7 +51,7 @@ if (typeof serverComponent === 'function') { // Re-export anything exported by the page module we're wrapping. When processing this code, Rollup is smart enough to // not include anything whose name matchs something we've explicitly exported above. -// @ts-ignore See above +// @ts-expect-error See above // eslint-disable-next-line import/no-unresolved export * from '__SENTRY_WRAPPING_TARGET_FILE__'; diff --git a/packages/nextjs/src/config/withSentryConfig.ts b/packages/nextjs/src/config/withSentryConfig.ts index 06f5808642ca..07a22a815ade 100644 --- a/packages/nextjs/src/config/withSentryConfig.ts +++ b/packages/nextjs/src/config/withSentryConfig.ts @@ -119,7 +119,7 @@ function setUpTunnelRewriteRules(userNextConfig: NextConfigObject, tunnelPath: s return [injectedRewrite]; } - // @ts-ignore Expected 0 arguments but got 1 - this is from the future-proofing mentioned above, so we don't care about it + // @ts-expect-error Expected 0 arguments but got 1 - this is from the future-proofing mentioned above, so we don't care about it const originalRewritesResult = await originalRewrites(...args); if (Array.isArray(originalRewritesResult)) { diff --git a/packages/nextjs/test/clientSdk.test.ts b/packages/nextjs/test/clientSdk.test.ts index ed3bb666d58d..499b62425442 100644 --- a/packages/nextjs/test/clientSdk.test.ts +++ b/packages/nextjs/test/clientSdk.test.ts @@ -72,12 +72,12 @@ describe('Client init()', () => { it('sets runtime on scope', () => { const currentScope = getCurrentHub().getScope(); - // @ts-ignore need access to protected _tags attribute + // @ts-expect-error need access to protected _tags attribute expect(currentScope._tags).toEqual({}); init({}); - // @ts-ignore need access to protected _tags attribute + // @ts-expect-error need access to protected _tags attribute expect(currentScope._tags).toEqual({ runtime: 'browser' }); }); diff --git a/packages/nextjs/test/config/webpack/constructWebpackConfig.test.ts b/packages/nextjs/test/config/webpack/constructWebpackConfig.test.ts index 86bb2d03d3fd..9ac4645630e5 100644 --- a/packages/nextjs/test/config/webpack/constructWebpackConfig.test.ts +++ b/packages/nextjs/test/config/webpack/constructWebpackConfig.test.ts @@ -41,7 +41,7 @@ describe('constructWebpackConfigFunction()', () => { // Run the user's webpack config function, so we can check the results against ours. Delete `entry` because we'll // test it separately, and besides, it's one that we *should* be overwriting. const materializedUserWebpackConfig = userNextConfig.webpack!(serverWebpackConfig, serverBuildContext); - // @ts-ignore `entry` may be required in real life, but we don't need it for our tests + // @ts-expect-error `entry` may be required in real life, but we don't need it for our tests delete materializedUserWebpackConfig.entry; expect(finalWebpackConfig).toEqual(expect.objectContaining(materializedUserWebpackConfig)); diff --git a/packages/nextjs/test/edge/edgeWrapperUtils.test.ts b/packages/nextjs/test/edge/edgeWrapperUtils.test.ts index 3dd963077a00..872277339c68 100644 --- a/packages/nextjs/test/edge/edgeWrapperUtils.test.ts +++ b/packages/nextjs/test/edge/edgeWrapperUtils.test.ts @@ -7,12 +7,12 @@ import { withEdgeWrapping } from '../../src/common/utils/edgeWrapperUtils'; // constructor but the client isn't used in these tests. addTracingExtensions(); -// @ts-ignore Request does not exist on type Global +// @ts-expect-error Request does not exist on type Global const origRequest = global.Request; -// @ts-ignore Response does not exist on type Global +// @ts-expect-error Response does not exist on type Global const origResponse = global.Response; -// @ts-ignore Request does not exist on type Global +// @ts-expect-error Request does not exist on type Global global.Request = class Request { headers = { get() { @@ -21,13 +21,13 @@ global.Request = class Request { }; }; -// @ts-ignore Response does not exist on type Global +// @ts-expect-error Response does not exist on type Global global.Response = class Request {}; afterAll(() => { - // @ts-ignore Request does not exist on type Global + // @ts-expect-error Request does not exist on type Global global.Request = origRequest; - // @ts-ignore Response does not exist on type Global + // @ts-expect-error Response does not exist on type Global global.Response = origResponse; }); diff --git a/packages/nextjs/test/edge/transport.test.ts b/packages/nextjs/test/edge/transport.test.ts index e3eb063702fd..26be44c56e95 100644 --- a/packages/nextjs/test/edge/transport.test.ts +++ b/packages/nextjs/test/edge/transport.test.ts @@ -27,13 +27,13 @@ class Headers { const mockFetch = jest.fn(); -// @ts-ignore fetch is not on global +// @ts-expect-error fetch is not on global const oldFetch = global.fetch; -// @ts-ignore fetch is not on global +// @ts-expect-error fetch is not on global global.fetch = mockFetch; afterAll(() => { - // @ts-ignore fetch is not on global + // @ts-expect-error fetch is not on global global.fetch = oldFetch; }); diff --git a/packages/nextjs/test/edge/withSentryAPI.test.ts b/packages/nextjs/test/edge/withSentryAPI.test.ts index a991ecf88e6b..0fb111df57a6 100644 --- a/packages/nextjs/test/edge/withSentryAPI.test.ts +++ b/packages/nextjs/test/edge/withSentryAPI.test.ts @@ -6,12 +6,12 @@ import { wrapApiHandlerWithSentry } from '../../src/edge'; // constructor but the client isn't used in these tests. coreSdk.addTracingExtensions(); -// @ts-ignore Request does not exist on type Global +// @ts-expect-error Request does not exist on type Global const origRequest = global.Request; -// @ts-ignore Response does not exist on type Global +// @ts-expect-error Response does not exist on type Global const origResponse = global.Response; -// @ts-ignore Request does not exist on type Global +// @ts-expect-error Request does not exist on type Global global.Request = class Request { headers = { get() { @@ -22,13 +22,13 @@ global.Request = class Request { method = 'POST'; }; -// @ts-ignore Response does not exist on type Global +// @ts-expect-error Response does not exist on type Global global.Response = class Request {}; afterAll(() => { - // @ts-ignore Request does not exist on type Global + // @ts-expect-error Request does not exist on type Global global.Request = origRequest; - // @ts-ignore Response does not exist on type Global + // @ts-expect-error Response does not exist on type Global global.Response = origResponse; }); diff --git a/packages/nextjs/test/integration/pages/[id]/withInitialProps.tsx b/packages/nextjs/test/integration/pages/[id]/withInitialProps.tsx index 891ce12caa58..8fa2f228e90d 100644 --- a/packages/nextjs/test/integration/pages/[id]/withInitialProps.tsx +++ b/packages/nextjs/test/integration/pages/[id]/withInitialProps.tsx @@ -3,8 +3,7 @@ import Link from 'next/link'; const WithInitialPropsPage = ({ data }: { data: string }) => ( <>

WithInitialPropsPage {data}

- {/* - // @ts-ignore https://nextjs.org/docs/api-reference/next/link#if-the-child-is-a-custom-component-that-wraps-an-a-tag */} + {/* @ts-ignore https://nextjs.org/docs/api-reference/next/link#if-the-child-is-a-custom-component-that-wraps-an-a-tag */} Go to withServerSideProps diff --git a/packages/nextjs/test/integration/pages/[id]/withServerSideProps.tsx b/packages/nextjs/test/integration/pages/[id]/withServerSideProps.tsx index 3ae6416fd17d..420f88856656 100644 --- a/packages/nextjs/test/integration/pages/[id]/withServerSideProps.tsx +++ b/packages/nextjs/test/integration/pages/[id]/withServerSideProps.tsx @@ -3,8 +3,7 @@ import Link from 'next/link'; const WithServerSidePropsPage = ({ data }: { data: string }) => ( <>

WithServerSidePropsPage {data}

- {/* - // @ts-ignore https://nextjs.org/docs/api-reference/next/link#if-the-child-is-a-custom-component-that-wraps-an-a-tag */} + {/* @ts-ignore https://nextjs.org/docs/api-reference/next/link#if-the-child-is-a-custom-component-that-wraps-an-a-tag */} Go to withInitialProps diff --git a/packages/nextjs/test/integration/pages/api/requireTest.ts b/packages/nextjs/test/integration/pages/api/requireTest.ts index cb0674d28c42..38d27c637d2c 100644 --- a/packages/nextjs/test/integration/pages/api/requireTest.ts +++ b/packages/nextjs/test/integration/pages/api/requireTest.ts @@ -6,7 +6,7 @@ if (process.env.NEXT_PUBLIC_SOME_FALSE_ENV_VAR === 'enabled') { const handler = async (_req: NextApiRequest, res: NextApiResponse): Promise => { require('@sentry/nextjs').captureException; // Should not throw unless the wrapping loader messes up cjs imports - // @ts-ignore + // @ts-expect-error require.context('.'); // This is a webpack utility call. Should not throw unless the wrapping loader messes it up by mangling. res.status(200).json({ success: true }); }; diff --git a/packages/nextjs/test/integration/pages/crashed.tsx b/packages/nextjs/test/integration/pages/crashed.tsx index 70a35bbfc930..bddf97b5cb6c 100644 --- a/packages/nextjs/test/integration/pages/crashed.tsx +++ b/packages/nextjs/test/integration/pages/crashed.tsx @@ -1,10 +1,9 @@ const CrashedPage = (): JSX.Element => { // Magic to naively trigger onerror to make session crashed and allow for SSR try { - // @ts-ignore if (typeof window !== 'undefined' && typeof window.onerror === 'function') { // Lovely oldschool browsers syntax with 5 arguments <3 - // @ts-ignore + // @ts-expect-error window.onerror(null, null, null, null, new Error('Crashed')); } } catch (_e) { diff --git a/packages/nextjs/test/integration/test/client/tracingClientGetInitialProps.test.ts b/packages/nextjs/test/integration/test/client/tracingClientGetInitialProps.test.ts index fffd84afe25b..6b7e05761743 100644 --- a/packages/nextjs/test/integration/test/client/tracingClientGetInitialProps.test.ts +++ b/packages/nextjs/test/integration/test/client/tracingClientGetInitialProps.test.ts @@ -19,7 +19,7 @@ test('should instrument `getInitialProps` for performance tracing', async ({ pag const nextDataTag = await page.waitForSelector('#__NEXT_DATA__', { state: 'attached' }); const nextDataTagValue = JSON.parse(await nextDataTag.evaluate(tag => (tag as HTMLElement).innerText)); - // @ts-ignore - We know `contexts` is defined in the Transaction envelope + // @ts-expect-error - We know `contexts` is defined in the Transaction envelope const traceId = transaction[0].contexts.trace.trace_id; expect(traceId).toBeDefined(); diff --git a/packages/nextjs/test/integration/test/client/tracingClientGetServerSideProps.test.ts b/packages/nextjs/test/integration/test/client/tracingClientGetServerSideProps.test.ts index 4b839ffd3244..434a87301332 100644 --- a/packages/nextjs/test/integration/test/client/tracingClientGetServerSideProps.test.ts +++ b/packages/nextjs/test/integration/test/client/tracingClientGetServerSideProps.test.ts @@ -19,7 +19,7 @@ test('should instrument `getServerSideProps` for performance tracing', async ({ const nextDataTag = await page.waitForSelector('#__NEXT_DATA__', { state: 'attached' }); const nextDataTagValue = JSON.parse(await nextDataTag.evaluate(tag => (tag as HTMLElement).innerText)); - // @ts-ignore - We know `contexts` is defined in the Transaction envelope + // @ts-expect-error - We know `contexts` is defined in the Transaction envelope const traceId = transaction[0].contexts.trace.trace_id; expect(traceId).toBeDefined(); diff --git a/packages/nextjs/test/integration/test/client/tracingFetch.test.ts b/packages/nextjs/test/integration/test/client/tracingFetch.test.ts index b1eb8a5f1bb8..ef3953b39e75 100644 --- a/packages/nextjs/test/integration/test/client/tracingFetch.test.ts +++ b/packages/nextjs/test/integration/test/client/tracingFetch.test.ts @@ -27,7 +27,7 @@ test('should correctly instrument `fetch` for performance tracing', async ({ pag }, }); - // @ts-ignore - We know that `spans` is inside Transaction envelopes + // @ts-expect-error - We know that `spans` is inside Transaction envelopes expect(transaction[0].spans).toEqual( expect.arrayContaining([ expect.objectContaining({ diff --git a/packages/nextjs/test/integration/test/server/utils/helpers.ts b/packages/nextjs/test/integration/test/server/utils/helpers.ts index efc8c144eee2..8e1660e5dfe0 100644 --- a/packages/nextjs/test/integration/test/server/utils/helpers.ts +++ b/packages/nextjs/test/integration/test/server/utils/helpers.ts @@ -7,7 +7,7 @@ import next from 'next'; import { AddressInfo } from 'net'; // Type not exported from NextJS -// @ts-ignore +// @ts-expect-error export const createNextServer = async config => { const app = next({ ...config, customServer: false }); // customServer: false because: https://github.com/vercel/next.js/pull/49805#issuecomment-1557321794 const handle = app.getRequestHandler(); diff --git a/packages/nextjs/test/serverSdk.test.ts b/packages/nextjs/test/serverSdk.test.ts index 8e73f71b3771..b1c6f93b2c16 100644 --- a/packages/nextjs/test/serverSdk.test.ts +++ b/packages/nextjs/test/serverSdk.test.ts @@ -21,7 +21,7 @@ function findIntegrationByName(integrations: Integration[] = [], name: string): describe('Server init()', () => { afterEach(() => { jest.clearAllMocks(); - // @ts-ignore for testing + // @ts-expect-error for testing delete GLOBAL_OBJ.__SENTRY__; delete process.env.VERCEL; }); @@ -73,12 +73,12 @@ describe('Server init()', () => { it('sets runtime on scope', () => { const currentScope = getCurrentHub().getScope(); - // @ts-ignore need access to protected _tags attribute + // @ts-expect-error need access to protected _tags attribute expect(currentScope._tags).toEqual({}); init({}); - // @ts-ignore need access to protected _tags attribute + // @ts-expect-error need access to protected _tags attribute expect(currentScope._tags).toEqual({ runtime: 'node' }); }); @@ -93,7 +93,7 @@ describe('Server init()', () => { init({}); - // @ts-ignore need access to protected _tags attribute + // @ts-expect-error need access to protected _tags attribute expect(currentScope._tags.vercel).toBeUndefined(); }); @@ -135,9 +135,9 @@ describe('Server init()', () => { expect(globalHub.getClient()).toEqual(expect.any(NodeClient)); expect(domainHub.getClient()).toBe(globalHub.getClient()); - // @ts-ignore need access to protected _tags attribute + // @ts-expect-error need access to protected _tags attribute expect(globalHub.getScope()._tags).toEqual({ runtime: 'node' }); - // @ts-ignore need access to protected _tags attribute + // @ts-expect-error need access to protected _tags attribute expect(domainHub.getScope()._tags).toEqual({ runtime: 'node', dogs: 'areGreat' }); }); }); diff --git a/packages/node-experimental/test/sdk/init.test.ts b/packages/node-experimental/test/sdk/init.test.ts index a9c2a11885a8..a150d61f3bf5 100644 --- a/packages/node-experimental/test/sdk/init.test.ts +++ b/packages/node-experimental/test/sdk/init.test.ts @@ -29,7 +29,7 @@ describe('init()', () => { }); afterEach(() => { - // @ts-ignore - Reset the default integrations of node sdk to original + // @ts-expect-error - Reset the default integrations of node sdk to original sdk.defaultIntegrations = defaultIntegrationsBackup; }); @@ -39,7 +39,7 @@ describe('init()', () => { new MockIntegration('Mock integration 1.2'), ]; - // @ts-ignore - Replace default integrations with mock integrations, needs ts-ignore because imports are readonly + // @ts-expect-error - Replace default integrations with mock integrations, needs ts-ignore because imports are readonly sdk.defaultIntegrations = mockDefaultIntegrations; init({ dsn: PUBLIC_DSN, defaultIntegrations: false }); @@ -55,7 +55,7 @@ describe('init()', () => { new MockIntegration('Some mock integration 2.2'), ]; - // @ts-ignore - Replace default integrations with mock integrations, needs ts-ignore because imports are readonly + // @ts-expect-error - Replace default integrations with mock integrations, needs ts-ignore because imports are readonly sdk.defaultIntegrations = mockDefaultIntegrations; const mockIntegrations = [ @@ -78,7 +78,7 @@ describe('init()', () => { new MockIntegration('Some mock integration 3.2'), ]; - // @ts-ignore - Replace default integrations with mock integrations, needs ts-ignore because imports are readonly + // @ts-expect-error - Replace default integrations with mock integrations, needs ts-ignore because imports are readonly sdk.defaultIntegrations = mockDefaultIntegrations; const newIntegration = new MockIntegration('Some mock integration 3.3'); @@ -104,7 +104,7 @@ describe('init()', () => { new MockIntegration('Some mock integration 4.2'), ]; - // @ts-ignore - Replace default integrations with mock integrations, needs ts-ignore because imports are readonly + // @ts-expect-error - Replace default integrations with mock integrations, needs ts-ignore because imports are readonly sdk.defaultIntegrations = mockDefaultIntegrations; const autoPerformanceIntegration = new MockIntegration('Some mock integration 4.4'); diff --git a/packages/node-integration-tests/suites/sessions/server.ts b/packages/node-integration-tests/suites/sessions/server.ts index 2823458a1435..8329dffa2bc5 100644 --- a/packages/node-integration-tests/suites/sessions/server.ts +++ b/packages/node-integration-tests/suites/sessions/server.ts @@ -1,4 +1,5 @@ /* eslint-disable no-console */ +import type { SessionFlusher } from '@sentry/core'; import * as Sentry from '@sentry/node'; import express from 'express'; @@ -13,16 +14,13 @@ app.use(Sentry.Handlers.requestHandler()); // ### Taken from manual tests ### // Hack that resets the 60s default flush interval, and replaces it with just a one second interval -// @ts-ignore: need access to `_sessionFlusher` -const flusher = (Sentry.getCurrentHub()?.getClient() as Sentry.NodeClient)?._sessionFlusher; +const flusher = (Sentry.getCurrentHub()?.getClient() as Sentry.NodeClient)['_sessionFlusher'] as SessionFlusher; -// @ts-ignore: need access to `_intervalId` -let flusherIntervalId = flusher?._intervalId; +let flusherIntervalId = flusher && flusher['_intervalId']; clearInterval(flusherIntervalId); -// @ts-ignore: need access to `_intervalId` -flusherIntervalId = flusher?._intervalId = setInterval(() => flusher?.flush(), 2000); +flusherIntervalId = flusher['_intervalId'] = setInterval(() => flusher?.flush(), 2000); setTimeout(() => clearInterval(flusherIntervalId), 4000); diff --git a/packages/node/src/integrations/requestdata.ts b/packages/node/src/integrations/requestdata.ts index 2994b797b221..5521345a7b98 100644 --- a/packages/node/src/integrations/requestdata.ts +++ b/packages/node/src/integrations/requestdata.ts @@ -79,7 +79,7 @@ export class RequestData implements Integration { ...DEFAULT_OPTIONS, ...options, include: { - // @ts-ignore It's mad because `method` isn't a known `include` key. (It's only here and not set by default in + // @ts-expect-error It's mad because `method` isn't a known `include` key. (It's only here and not set by default in // `addRequestDataToEvent` for legacy reasons. TODO (v8): Change that.) method: true, ...DEFAULT_OPTIONS.include, diff --git a/packages/node/src/integrations/undici/index.ts b/packages/node/src/integrations/undici/index.ts index aeb614b3fddd..1cc51ab1fb70 100644 --- a/packages/node/src/integrations/undici/index.ts +++ b/packages/node/src/integrations/undici/index.ts @@ -50,7 +50,7 @@ export interface UndiciOptions { // // function debug(...args: any): void { // // Use a function like this one when debugging inside an AsyncHook callback -// // @ts-ignore any +// // @ts-expect-error any // writeFileSync('log.out', `${format(...args)}\n`, { flag: 'a' }); // } diff --git a/packages/node/test/client.test.ts b/packages/node/test/client.test.ts index ee5fd5bdd957..0ddf69105de7 100644 --- a/packages/node/test/client.test.ts +++ b/packages/node/test/client.test.ts @@ -291,7 +291,7 @@ describe('NodeClient', () => { }); client = new NodeClient(options); - // @ts-ignore accessing private method + // @ts-expect-error accessing private method const sendEnvelopeSpy = jest.spyOn(client, '_sendEnvelope'); const id = client.captureCheckIn( @@ -358,7 +358,7 @@ describe('NodeClient', () => { const options = getDefaultNodeClientOptions({ dsn: PUBLIC_DSN, serverName: 'bar', enabled: false }); client = new NodeClient(options); - // @ts-ignore accessing private method + // @ts-expect-error accessing private method const sendEnvelopeSpy = jest.spyOn(client, '_sendEnvelope'); client.captureCheckIn({ monitorSlug: 'foo', status: 'in_progress' }); diff --git a/packages/node/test/index.test.ts b/packages/node/test/index.test.ts index 732d54b61987..6ee7b526ca86 100644 --- a/packages/node/test/index.test.ts +++ b/packages/node/test/index.test.ts @@ -316,7 +316,6 @@ describe('SentryNode', () => { }); getCurrentHub().bindClient(new NodeClient(options)); try { - // @ts-ignore allow function declarations in strict mode // eslint-disable-next-line no-inner-declarations function testy(): void { throw new Error('test'); diff --git a/packages/node/test/integrations/http.test.ts b/packages/node/test/integrations/http.test.ts index 0f1613a27345..bb162789f0be 100644 --- a/packages/node/test/integrations/http.test.ts +++ b/packages/node/test/integrations/http.test.ts @@ -321,9 +321,9 @@ describe('tracing', () => { describe('Tracing options', () => { beforeEach(() => { // hacky way of restoring monkey patched functions - // @ts-ignore TS doesn't let us assign to this but we want to + // @ts-expect-error TS doesn't let us assign to this but we want to http.get = originalHttpGet; - // @ts-ignore TS doesn't let us assign to this but we want to + // @ts-expect-error TS doesn't let us assign to this but we want to http.request = originalHttpRequest; }); diff --git a/packages/node/test/integrations/undici.test.ts b/packages/node/test/integrations/undici.test.ts index e72f7ce70212..103304080b9c 100644 --- a/packages/node/test/integrations/undici.test.ts +++ b/packages/node/test/integrations/undici.test.ts @@ -400,9 +400,9 @@ function setupTestServer() { function patchUndici(userOptions: Partial): () => void { try { const undici = hub.getClient()!.getIntegration(Undici); - // @ts-ignore need to access private property + // @ts-expect-error need to access private property options = { ...undici._options }; - // @ts-ignore need to access private property + // @ts-expect-error need to access private property undici._options = Object.assign(undici._options, userOptions); } catch (_) { throw new Error('Could not undo patching of undici'); diff --git a/packages/node/test/requestdata.test.ts b/packages/node/test/requestdata.test.ts index e5acf69c3297..b73b5de2d985 100644 --- a/packages/node/test/requestdata.test.ts +++ b/packages/node/test/requestdata.test.ts @@ -95,7 +95,6 @@ describe.each([parseRequest, addRequestDataToEvent])( test(`${fn.name}.user doesnt blow up when someone passes non-object value`, () => { const reqWithUser = { ...mockReq, - // @ts-ignore user is not assignable to object user: 'wat', }; diff --git a/packages/node/test/sdk.test.ts b/packages/node/test/sdk.test.ts index abd0265b62c4..b4423d8138b9 100644 --- a/packages/node/test/sdk.test.ts +++ b/packages/node/test/sdk.test.ts @@ -24,7 +24,7 @@ describe('init()', () => { }); afterEach(() => { - // @ts-ignore - Reset the default integrations of node sdk to original + // @ts-expect-error - Reset the default integrations of node sdk to original sdk.defaultIntegrations = defaultIntegrationsBackup; }); @@ -34,7 +34,7 @@ describe('init()', () => { new MockIntegration('Mock integration 1.2'), ]; - // @ts-ignore - Replace default integrations with mock integrations, needs ts-ignore because imports are readonly + // @ts-expect-error - Replace default integrations with mock integrations, needs ts-ignore because imports are readonly sdk.defaultIntegrations = mockDefaultIntegrations; init({ dsn: PUBLIC_DSN, defaultIntegrations: false }); @@ -49,7 +49,7 @@ describe('init()', () => { new MockIntegration('Some mock integration 2.2'), ]; - // @ts-ignore - Replace default integrations with mock integrations, needs ts-ignore because imports are readonly + // @ts-expect-error - Replace default integrations with mock integrations, needs ts-ignore because imports are readonly sdk.defaultIntegrations = mockDefaultIntegrations; const mockIntegrations = [ @@ -71,7 +71,7 @@ describe('init()', () => { new MockIntegration('Some mock integration 3.2'), ]; - // @ts-ignore - Replace default integrations with mock integrations, needs ts-ignore because imports are readonly + // @ts-expect-error - Replace default integrations with mock integrations, needs ts-ignore because imports are readonly sdk.defaultIntegrations = mockDefaultIntegrations; const newIntegration = new MockIntegration('Some mock integration 3.3'); diff --git a/packages/node/test/transports/http.test.ts b/packages/node/test/transports/http.test.ts index 4b914f234981..22d091a8583d 100644 --- a/packages/node/test/transports/http.test.ts +++ b/packages/node/test/transports/http.test.ts @@ -212,7 +212,7 @@ describe('makeNewHttpTransport()', () => { describe('proxy', () => { const proxyAgentSpy = jest .spyOn(httpProxyAgent, 'HttpsProxyAgent') - // @ts-ignore + // @ts-expect-error .mockImplementation(() => new http.Agent({ keepAlive: false, maxSockets: 30, timeout: 2000 })); it('can be configured through option', () => { diff --git a/packages/node/test/transports/https.test.ts b/packages/node/test/transports/https.test.ts index e63898a3b11e..b4ae670b7542 100644 --- a/packages/node/test/transports/https.test.ts +++ b/packages/node/test/transports/https.test.ts @@ -183,7 +183,7 @@ describe('makeNewHttpsTransport()', () => { describe('proxy', () => { const proxyAgentSpy = jest .spyOn(httpProxyAgent, 'HttpsProxyAgent') - // @ts-ignore + // @ts-expect-error .mockImplementation(() => new http.Agent({ keepAlive: false, maxSockets: 30, timeout: 2000 })); it('can be configured through option', () => { diff --git a/packages/opentelemetry-node/test/propagator.test.ts b/packages/opentelemetry-node/test/propagator.test.ts index a70ecb051663..8136b81d9b9a 100644 --- a/packages/opentelemetry-node/test/propagator.test.ts +++ b/packages/opentelemetry-node/test/propagator.test.ts @@ -46,7 +46,7 @@ describe('SentryPropagator', () => { publicKey: 'abc', }), }; - // @ts-ignore Use mock client for unit tests + // @ts-expect-error Use mock client for unit tests const hub: Hub = new Hub(client); makeMain(hub); diff --git a/packages/opentelemetry-node/test/spanprocessor.test.ts b/packages/opentelemetry-node/test/spanprocessor.test.ts index 0e479722efe5..695086d9cce2 100644 --- a/packages/opentelemetry-node/test/spanprocessor.test.ts +++ b/packages/opentelemetry-node/test/spanprocessor.test.ts @@ -66,7 +66,7 @@ describe('SentrySpanProcessor', () => { function getContext(transaction: Transaction) { const transactionWithContext = transaction as unknown as Transaction; - // @ts-ignore accessing private property + // @ts-expect-error accessing private property return transactionWithContext._contexts; } @@ -876,7 +876,6 @@ describe('SentrySpanProcessor', () => { parentOtelSpan.end(); }); - // @ts-ignore Accessing private attributes expect(sentryTransaction._hub.getScope()._tags.foo).toEqual('bar'); }); }); diff --git a/packages/overhead-metrics/src/perf/sampler.ts b/packages/overhead-metrics/src/perf/sampler.ts index 3db2131dc3f7..9ba693c5cc91 100644 --- a/packages/overhead-metrics/src/perf/sampler.ts +++ b/packages/overhead-metrics/src/perf/sampler.ts @@ -23,7 +23,7 @@ export class TimeBasedMap extends Map { * */ public toJSON(): JsonObject { - // @ts-ignore this actually exists + // @ts-expect-error this actually exists return Object.fromEntries(this.entries()); } } diff --git a/packages/react/src/reactrouter.tsx b/packages/react/src/reactrouter.tsx index ab45d4e5f82e..769c17b611a0 100644 --- a/packages/react/src/reactrouter.tsx +++ b/packages/react/src/reactrouter.tsx @@ -168,7 +168,7 @@ export function withSentryRouting

, R extends React activeTransaction.setName(props.computedMatch.path, 'route'); } - // @ts-ignore Setting more specific React Component typing for `R` generic above + // @ts-expect-error Setting more specific React Component typing for `R` generic above // will break advanced type inference done by react router params: // https://github.com/DefinitelyTyped/DefinitelyTyped/blob/13dc4235c069e25fe7ee16e11f529d909f9f3ff8/types/react-router/index.d.ts#L154-L164 return ; @@ -176,7 +176,7 @@ export function withSentryRouting

, R extends React WrappedRoute.displayName = `sentryRoute(${componentDisplayName})`; hoistNonReactStatics(WrappedRoute, Route); - // @ts-ignore Setting more specific React Component typing for `R` generic above + // @ts-expect-error Setting more specific React Component typing for `R` generic above // will break advanced type inference done by react router params: // https://github.com/DefinitelyTyped/DefinitelyTyped/blob/13dc4235c069e25fe7ee16e11f529d909f9f3ff8/types/react-router/index.d.ts#L154-L164 return WrappedRoute; diff --git a/packages/react/src/reactrouterv6.tsx b/packages/react/src/reactrouterv6.tsx index de356b05347b..0ba88c8f958d 100644 --- a/packages/react/src/reactrouterv6.tsx +++ b/packages/react/src/reactrouterv6.tsx @@ -202,14 +202,14 @@ export function withSentryReactRouterV6Routing

, R [location, navigationType], ); - // @ts-ignore Setting more specific React Component typing for `R` generic above + // @ts-expect-error Setting more specific React Component typing for `R` generic above // will break advanced type inference done by react router params return ; }; hoistNonReactStatics(SentryRoutes, Routes); - // @ts-ignore Setting more specific React Component typing for `R` generic above + // @ts-expect-error Setting more specific React Component typing for `R` generic above // will break advanced type inference done by react router params return SentryRoutes; } diff --git a/packages/react/test/errorboundary.test.tsx b/packages/react/test/errorboundary.test.tsx index f90aaa04b346..33486043f675 100644 --- a/packages/react/test/errorboundary.test.tsx +++ b/packages/react/test/errorboundary.test.tsx @@ -88,7 +88,7 @@ describe('ErrorBoundary', () => { it('renders null if not given a valid `fallback` prop', () => { const { container } = render( - // @ts-ignore Passing wrong type on purpose + // @ts-expect-error Passing wrong type on purpose , @@ -99,7 +99,7 @@ describe('ErrorBoundary', () => { it('renders null if not given a valid `fallback` prop function', () => { const { container } = render( - // @ts-ignore Passing wrong type on purpose + // @ts-expect-error Passing wrong type on purpose 'Not a ReactElement'}> , @@ -308,9 +308,9 @@ describe('ErrorBoundary', () => { const firstError = new Error('bam'); const secondError = new Error('bam2'); const thirdError = new Error('bam3'); - // @ts-ignore Need to set cause on error + // @ts-expect-error Need to set cause on error secondError.cause = firstError; - // @ts-ignore Need to set cause on error + // @ts-expect-error Need to set cause on error thirdError.cause = secondError; throw thirdError; } @@ -349,9 +349,9 @@ describe('ErrorBoundary', () => { function CustomBam(): JSX.Element { const firstError = new Error('bam'); const secondError = new Error('bam2'); - // @ts-ignore Need to set cause on error + // @ts-expect-error Need to set cause on error firstError.cause = secondError; - // @ts-ignore Need to set cause on error + // @ts-expect-error Need to set cause on error secondError.cause = firstError; throw firstError; } @@ -429,7 +429,7 @@ describe('ErrorBoundary', () => { it('shows a Sentry Report Dialog with correct options if client has hooks', () => { let callback: any; const hub = getCurrentHub(); - // @ts-ignore mock client + // @ts-expect-error mock client hub.bindClient({ on: (name: string, cb: any) => { callback = cb; diff --git a/packages/react/test/reactrouterv6.4.test.tsx b/packages/react/test/reactrouterv6.4.test.tsx index 913b6041e053..19a714f853a3 100644 --- a/packages/react/test/reactrouterv6.4.test.tsx +++ b/packages/react/test/reactrouterv6.4.test.tsx @@ -16,7 +16,7 @@ import { reactRouterV6Instrumentation, wrapCreateBrowserRouter } from '../src'; import type { CreateRouterFunction } from '../src/types'; beforeAll(() => { - // @ts-ignore need to override global Request because it's not in the jest environment (even with an + // @ts-expect-error need to override global Request because it's not in the jest environment (even with an // `@jest-environment jsdom` directive, for some reason) global.Request = Request; }); @@ -63,7 +63,7 @@ describe('React Router v6.4', () => { }, ); - // @ts-ignore router is fine + // @ts-expect-error router is fine render(); expect(mockStartTransaction).toHaveBeenCalledTimes(1); @@ -100,7 +100,7 @@ describe('React Router v6.4', () => { }, ); - // @ts-ignore router is fine + // @ts-expect-error router is fine render(); expect(mockStartTransaction).toHaveBeenCalledTimes(2); @@ -139,7 +139,7 @@ describe('React Router v6.4', () => { }, ); - // @ts-ignore router is fine + // @ts-expect-error router is fine render(); expect(mockStartTransaction).toHaveBeenCalledTimes(2); @@ -178,7 +178,7 @@ describe('React Router v6.4', () => { }, ); - // @ts-ignore router is fine + // @ts-expect-error router is fine render(); expect(mockStartTransaction).toHaveBeenCalledTimes(2); @@ -229,7 +229,7 @@ describe('React Router v6.4', () => { }, ); - // @ts-ignore router is fine + // @ts-expect-error router is fine render(); expect(mockStartTransaction).toHaveBeenCalledTimes(2); @@ -264,7 +264,7 @@ describe('React Router v6.4', () => { }, ); - // @ts-ignore router is fine + // @ts-expect-error router is fine render(); expect(mockStartTransaction).toHaveBeenCalledTimes(1); @@ -298,7 +298,7 @@ describe('React Router v6.4', () => { }, ); - // @ts-ignore router is fine + // @ts-expect-error router is fine render(); expect(mockStartTransaction).toHaveBeenCalledTimes(2); diff --git a/packages/remix/src/client/performance.tsx b/packages/remix/src/client/performance.tsx index ed7dea13cfc3..f47d11760556 100644 --- a/packages/remix/src/client/performance.tsx +++ b/packages/remix/src/client/performance.tsx @@ -108,7 +108,7 @@ export function withSentry

, R extends React.FC !isNodeEnv() && logger.warn('Remix SDK was unable to wrap your root because of one or more missing parameters.'); - // @ts-ignore Setting more specific React Component typing for `R` generic above + // @ts-expect-error Setting more specific React Component typing for `R` generic above // will break advanced type inference done by react router params return ; } @@ -154,18 +154,18 @@ export function withSentry

, R extends React.FC isBaseLocation = false; - // @ts-ignore Setting more specific React Component typing for `R` generic above + // @ts-expect-error Setting more specific React Component typing for `R` generic above // will break advanced type inference done by react router params return ; }; if (options.wrapWithErrorBoundary) { - // @ts-ignore Setting more specific React Component typing for `R` generic above + // @ts-expect-error Setting more specific React Component typing for `R` generic above // will break advanced type inference done by react router params return withErrorBoundary(SentryRoot, options.errorBoundaryOptions); } - // @ts-ignore Setting more specific React Component typing for `R` generic above + // @ts-expect-error Setting more specific React Component typing for `R` generic above // will break advanced type inference done by react router params return SentryRoot; } diff --git a/packages/remix/src/utils/web-fetch.ts b/packages/remix/src/utils/web-fetch.ts index 1961329c2f4b..f1b4487f8a05 100644 --- a/packages/remix/src/utils/web-fetch.ts +++ b/packages/remix/src/utils/web-fetch.ts @@ -141,11 +141,11 @@ export const normalizeRemixRequest = (request: RemixRequest): Record { it('sets runtime on scope', () => { const currentScope = getCurrentHub().getScope(); - // @ts-ignore need access to protected _tags attribute + // @ts-expect-error need access to protected _tags attribute expect(currentScope._tags).toEqual({}); init({}); - // @ts-ignore need access to protected _tags attribute + // @ts-expect-error need access to protected _tags attribute expect(currentScope._tags).toEqual({ runtime: 'browser' }); }); }); diff --git a/packages/remix/test/index.server.test.ts b/packages/remix/test/index.server.test.ts index ec1610aee400..fc977117897c 100644 --- a/packages/remix/test/index.server.test.ts +++ b/packages/remix/test/index.server.test.ts @@ -49,12 +49,12 @@ describe('Server init()', () => { it('sets runtime on scope', () => { const currentScope = getCurrentHub().getScope(); - // @ts-ignore need access to protected _tags attribute + // @ts-expect-error need access to protected _tags attribute expect(currentScope._tags).toEqual({}); init({}); - // @ts-ignore need access to protected _tags attribute + // @ts-expect-error need access to protected _tags attribute expect(currentScope._tags).toEqual({ runtime: 'node' }); }); diff --git a/packages/replay-worker/src/_worker.ts b/packages/replay-worker/src/_worker.ts index d81bb43d46df..597ce75bc4a9 100644 --- a/packages/replay-worker/src/_worker.ts +++ b/packages/replay-worker/src/_worker.ts @@ -3,7 +3,7 @@ import { handleMessage } from './handleMessage'; addEventListener('message', handleMessage); // Immediately send a message when worker loads, so we know the worker is ready -// @ts-ignore this syntax is actually fine +// @ts-expect-error this syntax is actually fine postMessage({ id: undefined, method: 'init', diff --git a/packages/replay-worker/src/handleMessage.ts b/packages/replay-worker/src/handleMessage.ts index 2a00f54a581f..bd0e7028eabd 100644 --- a/packages/replay-worker/src/handleMessage.ts +++ b/packages/replay-worker/src/handleMessage.ts @@ -36,12 +36,12 @@ export function handleMessage(e: MessageEvent): void { const id = e.data.id as number; const data = e.data.arg as string; - // @ts-ignore this syntax is actually fine + // @ts-expect-error this syntax is actually fine if (method in handlers && typeof handlers[method] === 'function') { try { - // @ts-ignore this syntax is actually fine + // @ts-expect-error this syntax is actually fine const response = handlers[method](data); - // @ts-ignore this syntax is actually fine + // @ts-expect-error this syntax is actually fine postMessage({ id, method, @@ -49,7 +49,7 @@ export function handleMessage(e: MessageEvent): void { response, }); } catch (err) { - // @ts-ignore this syntax is actually fine + // @ts-expect-error this syntax is actually fine postMessage({ id, method, diff --git a/packages/replay-worker/test/unit/Compressor.test.ts b/packages/replay-worker/test/unit/Compressor.test.ts index 73b067fa7213..902c143ec255 100644 --- a/packages/replay-worker/test/unit/Compressor.test.ts +++ b/packages/replay-worker/test/unit/Compressor.test.ts @@ -29,7 +29,7 @@ describe('Compressor', () => { it('throws on invalid/undefined events', () => { const compressor = new Compressor(); - // @ts-ignore ignoring type for test + // @ts-expect-error ignoring type for test expect(() => void compressor.addEvent(undefined)).toThrow(); const compressed = compressor.finish(); diff --git a/packages/replay/jest.setup.ts b/packages/replay/jest.setup.ts index 093c97dcdce4..b44298a751e1 100644 --- a/packages/replay/jest.setup.ts +++ b/packages/replay/jest.setup.ts @@ -82,17 +82,17 @@ function checkCallForSentReplay( const envelopeItems = call?.[1] || [[], []]; const [[replayEventHeader, replayEventPayload], [recordingHeader, recordingPayload] = []] = envelopeItems; - // @ts-ignore recordingPayload is always a string in our tests + // @ts-expect-error recordingPayload is always a string in our tests const [recordingPayloadHeader, recordingData] = recordingPayload?.split('\n') || []; const actualObj: Required = { - // @ts-ignore Custom envelope + // @ts-expect-error Custom envelope envelopeHeader: envelopeHeader, - // @ts-ignore Custom envelope + // @ts-expect-error Custom envelope replayEventHeader: replayEventHeader, - // @ts-ignore Custom envelope + // @ts-expect-error Custom envelope replayEventPayload: replayEventPayload, - // @ts-ignore Custom envelope + // @ts-expect-error Custom envelope recordingHeader: recordingHeader, recordingPayloadHeader: recordingPayloadHeader && JSON.parse(recordingPayloadHeader), recordingData, @@ -180,7 +180,7 @@ const toHaveSentReplay = function ( } } - // @ts-ignore use before assigned + // @ts-expect-error use before assigned const { results, call, pass } = result; const options = { diff --git a/packages/replay/src/coreHandlers/util/networkUtils.ts b/packages/replay/src/coreHandlers/util/networkUtils.ts index 8d73c5660463..130db1658354 100644 --- a/packages/replay/src/coreHandlers/util/networkUtils.ts +++ b/packages/replay/src/coreHandlers/util/networkUtils.ts @@ -185,7 +185,7 @@ export function getAllowedHeaders(headers: Record, allowedHeader function _serializeFormData(formData: FormData): string { // This is a bit simplified, but gives us a decent estimate // This converts e.g. { name: 'Anne Smith', age: 13 } to 'name=Anne+Smith&age=13' - // @ts-ignore passing FormData to URLSearchParams actually works + // @ts-expect-error passing FormData to URLSearchParams actually works return new URLSearchParams(formData).toString(); } diff --git a/packages/replay/src/util/addMemoryEntry.ts b/packages/replay/src/util/addMemoryEntry.ts index 0a67e81dd471..01c87daa7ead 100644 --- a/packages/replay/src/util/addMemoryEntry.ts +++ b/packages/replay/src/util/addMemoryEntry.ts @@ -19,7 +19,7 @@ export async function addMemoryEntry(replay: ReplayContainer): Promise function to normalize data for event -// @ts-ignore TODO: entry type does not fit the create* functions entry type const ENTRY_TYPES: Record< string, (entry: AllPerformanceEntry) => null | ReplayPerformanceEntry > = { - // @ts-ignore TODO: entry type does not fit the create* functions entry type + // @ts-expect-error TODO: entry type does not fit the create* functions entry type resource: createResourceEntry, paint: createPaintEntry, - // @ts-ignore TODO: entry type does not fit the create* functions entry type + // @ts-expect-error TODO: entry type does not fit the create* functions entry type navigation: createNavigationEntry, - // @ts-ignore TODO: entry type does not fit the create* functions entry type + // @ts-expect-error TODO: entry type does not fit the create* functions entry type ['largest-contentful-paint']: createLargestContentfulPaint, }; diff --git a/packages/replay/src/util/isRrwebError.ts b/packages/replay/src/util/isRrwebError.ts index a8d097e73af9..bab65638c382 100644 --- a/packages/replay/src/util/isRrwebError.ts +++ b/packages/replay/src/util/isRrwebError.ts @@ -8,7 +8,7 @@ export function isRrwebError(event: Event, hint: EventHint): boolean { return false; } - // @ts-ignore this may be set by rrweb when it finds errors + // @ts-expect-error this may be set by rrweb when it finds errors if (hint.originalException && hint.originalException.__rrweb__) { return true; } diff --git a/packages/replay/src/util/sendReplay.ts b/packages/replay/src/util/sendReplay.ts index 5f10011182c6..34814b10756c 100644 --- a/packages/replay/src/util/sendReplay.ts +++ b/packages/replay/src/util/sendReplay.ts @@ -45,7 +45,7 @@ export async function sendReplay( try { // In case browsers don't allow this property to be writable - // @ts-ignore This needs lib es2022 and newer + // @ts-expect-error This needs lib es2022 and newer error.cause = err; } catch { // nothing to do diff --git a/packages/replay/src/util/sendReplayRequest.ts b/packages/replay/src/util/sendReplayRequest.ts index 1bce7f4fd132..5dc0e1b1525e 100644 --- a/packages/replay/src/util/sendReplayRequest.ts +++ b/packages/replay/src/util/sendReplayRequest.ts @@ -110,7 +110,7 @@ export async function sendReplayRequest({ try { // In case browsers don't allow this property to be writable - // @ts-ignore This needs lib es2022 and newer + // @ts-expect-error This needs lib es2022 and newer error.cause = err; } catch { // nothing to do diff --git a/packages/replay/test/integration/coreHandlers/handleAfterSendEvent.test.ts b/packages/replay/test/integration/coreHandlers/handleAfterSendEvent.test.ts index 22320a5cacfc..a5d32ae50ca3 100644 --- a/packages/replay/test/integration/coreHandlers/handleAfterSendEvent.test.ts +++ b/packages/replay/test/integration/coreHandlers/handleAfterSendEvent.test.ts @@ -153,7 +153,7 @@ describe('Integration | coreHandlers | handleAfterSendEvent', () => { })); const client = getCurrentHub().getClient()!; - // @ts-ignore make sure to remove this + // @ts-expect-error make sure to remove this delete client.getTransport()!.send.__sentry__baseTransport__; const error1 = Error({ event_id: 'err1' }); diff --git a/packages/replay/test/integration/coreHandlers/handleGlobalEvent.test.ts b/packages/replay/test/integration/coreHandlers/handleGlobalEvent.test.ts index bdd84bed4133..dbe919b18079 100644 --- a/packages/replay/test/integration/coreHandlers/handleGlobalEvent.test.ts +++ b/packages/replay/test/integration/coreHandlers/handleGlobalEvent.test.ts @@ -35,7 +35,7 @@ describe('Integration | coreHandlers | handleGlobalEvent', () => { breadcrumbs: [{ type: 'fakecrumb' }], }; - // @ts-ignore replay event type + // @ts-expect-error replay event type expect(handleGlobalEventListener(replay)(replayEvent, {})).toEqual({ type: REPLAY_EVENT_NAME, }); @@ -105,7 +105,7 @@ describe('Integration | coreHandlers | handleGlobalEvent', () => { it('tags errors and transactions with replay id for session samples', async () => { let integration: ReplayIntegration; ({ replay, integration } = await resetSdkMock({})); - // @ts-ignore protected but ok to use for testing + // @ts-expect-error protected but ok to use for testing integration._initialize(); const transaction = Transaction(); const error = Error(); @@ -313,7 +313,7 @@ describe('Integration | coreHandlers | handleGlobalEvent', () => { }; const originalException = new window.Error('some exception'); - // @ts-ignore this could be set by rrweb + // @ts-expect-error this could be set by rrweb originalException.__rrweb__ = true; expect(handleGlobalEventListener(replay)(errorEvent, { originalException })).toEqual(null); diff --git a/packages/replay/test/integration/flush.test.ts b/packages/replay/test/integration/flush.test.ts index f29e1fa0390a..9b02ba980208 100644 --- a/packages/replay/test/integration/flush.test.ts +++ b/packages/replay/test/integration/flush.test.ts @@ -58,13 +58,13 @@ describe('Integration | flush', () => { }), ); - // @ts-ignore private API + // @ts-expect-error private API mockFlush = jest.spyOn(replay, '_flush'); - // @ts-ignore private API + // @ts-expect-error private API mockRunFlush = jest.spyOn(replay, '_runFlush'); - // @ts-ignore private API + // @ts-expect-error private API mockAddPerformanceEntries = jest.spyOn(replay, '_addPerformanceEntries'); mockAddPerformanceEntries.mockImplementation(async () => { diff --git a/packages/replay/test/integration/sampling.test.ts b/packages/replay/test/integration/sampling.test.ts index 0c926585b6f5..b82bb9538b5e 100644 --- a/packages/replay/test/integration/sampling.test.ts +++ b/packages/replay/test/integration/sampling.test.ts @@ -19,7 +19,7 @@ describe('Integration | sampling', () => { }, }); - // @ts-ignore private API + // @ts-expect-error private API const spyAddListeners = jest.spyOn(replay, '_addListeners'); jest.runAllTimers(); @@ -54,10 +54,10 @@ describe('Integration | sampling', () => { autoStart: false, // Needs to be false in order to spy on replay }); - // @ts-ignore private API + // @ts-expect-error private API const spyAddListeners = jest.spyOn(replay, '_addListeners'); - // @ts-ignore protected + // @ts-expect-error protected integration._initialize(); jest.runAllTimers(); diff --git a/packages/replay/test/integration/session.test.ts b/packages/replay/test/integration/session.test.ts index ca4e16be8c85..80d09124401a 100644 --- a/packages/replay/test/integration/session.test.ts +++ b/packages/replay/test/integration/session.test.ts @@ -367,7 +367,7 @@ describe('Integration | session', () => { expect(replay).not.toHaveSameSession(initialSession); expect(mockRecord.takeFullSnapshot).toHaveBeenCalled(); expect(replay).not.toHaveLastSentReplay(); - // @ts-ignore private + // @ts-expect-error private expect(replay._stopRecording).toBeDefined(); // Now do a click diff --git a/packages/replay/test/integration/stop.test.ts b/packages/replay/test/integration/stop.test.ts index cdc980ae5b62..04c85a9dedde 100644 --- a/packages/replay/test/integration/stop.test.ts +++ b/packages/replay/test/integration/stop.test.ts @@ -35,7 +35,7 @@ describe('Integration | stop', () => { ({ replay, integration } = await mockSdk()); - // @ts-ignore private API + // @ts-expect-error private API mockRunFlush = jest.spyOn(replay, '_runFlush'); jest.runAllTimers(); diff --git a/packages/replay/test/mocks/resetSdkMock.ts b/packages/replay/test/mocks/resetSdkMock.ts index 356434373df6..aa92cfead4a8 100644 --- a/packages/replay/test/mocks/resetSdkMock.ts +++ b/packages/replay/test/mocks/resetSdkMock.ts @@ -46,7 +46,7 @@ export async function resetSdkMock({ replayOptions, sentryOptions, autoStart }: jest.setSystemTime(new Date(BASE_TIMESTAMP)); return { - // @ts-ignore use before assign + // @ts-expect-error use before assign domHandler, mockRecord, replay, diff --git a/packages/replay/test/unit/coreHandlers/handleScope.test.ts b/packages/replay/test/unit/coreHandlers/handleScope.test.ts index 9ce56b89d1e0..894f49592f93 100644 --- a/packages/replay/test/unit/coreHandlers/handleScope.test.ts +++ b/packages/replay/test/unit/coreHandlers/handleScope.test.ts @@ -20,7 +20,7 @@ describe('Unit | coreHandlers | handleScope', () => { } as unknown as Scope; function addBreadcrumb(breadcrumb: Breadcrumb) { - // @ts-ignore using private member + // @ts-expect-error using private member scope._breadcrumbs.push(breadcrumb); } diff --git a/packages/replay/test/unit/eventBuffer/EventBufferCompressionWorker.test.ts b/packages/replay/test/unit/eventBuffer/EventBufferCompressionWorker.test.ts index 297744389cf6..8ef88bb33694 100644 --- a/packages/replay/test/unit/eventBuffer/EventBufferCompressionWorker.test.ts +++ b/packages/replay/test/unit/eventBuffer/EventBufferCompressionWorker.test.ts @@ -121,7 +121,7 @@ describe('Unit | eventBuffer | EventBufferCompressionWorker', () => { await buffer.addEvent(TEST_EVENT); await buffer.addEvent(TEST_EVENT); - // @ts-ignore Mock this private so it triggers an error + // @ts-expect-error Mock this private so it triggers an error jest.spyOn(buffer._compression._worker, 'postMessage').mockImplementationOnce(() => { return Promise.reject('test worker error'); }); @@ -142,7 +142,7 @@ describe('Unit | eventBuffer | EventBufferCompressionWorker', () => { await buffer.addEvent({ data: { o: 1 }, timestamp: BASE_TIMESTAMP, type: 3 }); await buffer.addEvent({ data: { o: 2 }, timestamp: BASE_TIMESTAMP, type: 3 }); - // @ts-ignore Mock this private so it triggers an error + // @ts-expect-error Mock this private so it triggers an error jest.spyOn(buffer._compression._worker, 'postMessage').mockImplementationOnce(() => { return Promise.reject('test worker error'); }); diff --git a/packages/replay/test/unit/util/addEvent.test.ts b/packages/replay/test/unit/util/addEvent.test.ts index 6230bd40c21c..ec6c752eb011 100644 --- a/packages/replay/test/unit/util/addEvent.test.ts +++ b/packages/replay/test/unit/util/addEvent.test.ts @@ -22,7 +22,7 @@ describe('Unit | util | addEvent', () => { await (replay.eventBuffer as EventBufferProxy).ensureWorkerIsLoaded(); - // @ts-ignore Mock this private so it triggers an error + // @ts-expect-error Mock this private so it triggers an error jest.spyOn(replay.eventBuffer._compression._worker, 'postMessage').mockImplementationOnce(() => { return Promise.reject('test worker error'); }); diff --git a/packages/replay/test/unit/util/createPerformanceEntry.test.ts b/packages/replay/test/unit/util/createPerformanceEntry.test.ts index 57cb780742eb..295aa009b4f8 100644 --- a/packages/replay/test/unit/util/createPerformanceEntry.test.ts +++ b/packages/replay/test/unit/util/createPerformanceEntry.test.ts @@ -54,7 +54,7 @@ describe('Unit | util | createPerformanceEntries', () => { workerTiming: [], } as const; - // @ts-ignore Needs a PerformanceEntry mock + // @ts-expect-error Needs a PerformanceEntry mock expect(createPerformanceEntries([data])).toEqual([]); }); diff --git a/packages/replay/test/unit/util/createReplayEnvelope.test.ts b/packages/replay/test/unit/util/createReplayEnvelope.test.ts index 150da47edf00..287f1f3ccb3f 100644 --- a/packages/replay/test/unit/util/createReplayEnvelope.test.ts +++ b/packages/replay/test/unit/util/createReplayEnvelope.test.ts @@ -7,7 +7,6 @@ describe('Unit | util | createReplayEnvelope', () => { const REPLAY_ID = 'MY_REPLAY_ID'; const replayEvent: ReplayEvent = { - // @ts-ignore private api type: 'replay_event', timestamp: 1670837008.634, error_ids: ['errorId'], diff --git a/packages/replay/test/unit/util/prepareReplayEvent.test.ts b/packages/replay/test/unit/util/prepareReplayEvent.test.ts index 5ada534f65a0..dd132f4e3633 100644 --- a/packages/replay/test/unit/util/prepareReplayEvent.test.ts +++ b/packages/replay/test/unit/util/prepareReplayEvent.test.ts @@ -39,7 +39,6 @@ describe('Unit | util | prepareReplayEvent', () => { const replayId = 'replay-ID'; const event: ReplayEvent = { - // @ts-ignore private api type: REPLAY_EVENT_NAME, timestamp: 1670837008.634, error_ids: ['error-ID'], diff --git a/packages/serverless/test/awslambda.test.ts b/packages/serverless/test/awslambda.test.ts index e03d17bfd14b..454b36296adb 100644 --- a/packages/serverless/test/awslambda.test.ts +++ b/packages/serverless/test/awslambda.test.ts @@ -10,7 +10,7 @@ import * as Sentry from '../src'; const { wrapHandler } = Sentry.AWSLambda; /** - * Why @ts-ignore some Sentry.X calls + * Why @ts-expect-error some Sentry.X calls * * A hack-ish way to contain everything related to mocks in the same __mocks__ file. * Thanks to this, we don't have to do more magic than necessary. Just add and export desired method and assert on it. @@ -42,15 +42,15 @@ const fakeCallback: Callback = (err, result) => { }; function expectScopeSettings(fakeTransactionContext: any) { - // @ts-ignore see "Why @ts-ignore" note + // @ts-expect-error see "Why @ts-expect-error" note const fakeTransaction = { ...SentryNode.fakeTransaction, ...fakeTransactionContext }; - // @ts-ignore see "Why @ts-ignore" note + // @ts-expect-error see "Why @ts-expect-error" note expect(SentryNode.fakeScope.setSpan).toBeCalledWith(fakeTransaction); - // @ts-ignore see "Why @ts-ignore" note + // @ts-expect-error see "Why @ts-expect-error" note expect(SentryNode.fakeScope.setTag).toBeCalledWith('server_name', expect.anything()); - // @ts-ignore see "Why @ts-ignore" note + // @ts-expect-error see "Why @ts-expect-error" note expect(SentryNode.fakeScope.setTag).toBeCalledWith('url', 'awslambda:///functionName'); - // @ts-ignore see "Why @ts-ignore" note + // @ts-expect-error see "Why @ts-expect-error" note expect(SentryNode.fakeScope.setContext).toBeCalledWith( 'aws.lambda', expect.objectContaining({ @@ -61,7 +61,7 @@ function expectScopeSettings(fakeTransactionContext: any) { remaining_time_in_millis: 100, }), ); - // @ts-ignore see "Why @ts-ignore" note + // @ts-expect-error see "Why @ts-expect-error" note expect(SentryNode.fakeScope.setContext).toBeCalledWith( 'aws.cloudwatch.logs', expect.objectContaining({ @@ -79,7 +79,7 @@ describe('AWSLambda', () => { }); afterEach(() => { - // @ts-ignore see "Why @ts-ignore" note + // @ts-expect-error see "Why @ts-expect-error" note SentryNode.resetMocks(); }); @@ -106,7 +106,7 @@ describe('AWSLambda', () => { await wrappedHandler(fakeEvent, fakeContext, fakeCallback); expect(Sentry.captureMessage).toBeCalled(); - // @ts-ignore see "Why @ts-ignore" note + // @ts-expect-error see "Why @ts-expect-error" note expect(SentryNode.fakeScope.setTag).toBeCalledWith('timeout', '1s'); }); @@ -154,7 +154,7 @@ describe('AWSLambda', () => { ); expect(Sentry.captureMessage).toBeCalled(); - // @ts-ignore see "Why @ts-ignore" note + // @ts-expect-error see "Why @ts-expect-error" note expect(SentryNode.fakeScope.setTag).toBeCalledWith('timeout', '1m40s'); }); @@ -200,10 +200,10 @@ describe('AWSLambda', () => { }; expect(rv).toStrictEqual(42); - // @ts-ignore see "Why @ts-ignore" note + // @ts-expect-error see "Why @ts-expect-error" note expect(SentryNode.fakeHub.startTransaction).toBeCalledWith(fakeTransactionContext); expectScopeSettings(fakeTransactionContext); - // @ts-ignore see "Why @ts-ignore" note + // @ts-expect-error see "Why @ts-expect-error" note expect(SentryNode.fakeTransaction.finish).toBeCalled(); expect(SentryNode.flush).toBeCalledWith(2000); }); @@ -227,11 +227,11 @@ describe('AWSLambda', () => { metadata: { source: 'component' }, }; - // @ts-ignore see "Why @ts-ignore" note + // @ts-expect-error see "Why @ts-expect-error" note expect(SentryNode.fakeHub.startTransaction).toBeCalledWith(fakeTransactionContext); expectScopeSettings(fakeTransactionContext); expect(SentryNode.captureException).toBeCalledWith(error, expect.any(Function)); - // @ts-ignore see "Why @ts-ignore" note + // @ts-expect-error see "Why @ts-expect-error" note expect(SentryNode.fakeTransaction.finish).toBeCalled(); expect(SentryNode.flush).toBeCalledWith(2000); } @@ -258,7 +258,7 @@ describe('AWSLambda', () => { }; const handler: Handler = (_event, _context, callback) => { - // @ts-ignore see "Why @ts-ignore" note + // @ts-expect-error see "Why @ts-expect-error" note expect(SentryNode.fakeHub.startTransaction).toBeCalledWith( expect.objectContaining({ parentSpanId: '1121201211212012', @@ -306,11 +306,11 @@ describe('AWSLambda', () => { metadata: { dynamicSamplingContext: {}, source: 'component' }, }; - // @ts-ignore see "Why @ts-ignore" note + // @ts-expect-error see "Why @ts-expect-error" note expect(SentryNode.fakeHub.startTransaction).toBeCalledWith(fakeTransactionContext); expectScopeSettings(fakeTransactionContext); expect(SentryNode.captureException).toBeCalledWith(e, expect.any(Function)); - // @ts-ignore see "Why @ts-ignore" note + // @ts-expect-error see "Why @ts-expect-error" note expect(SentryNode.fakeTransaction.finish).toBeCalled(); expect(SentryNode.flush).toBeCalled(); } @@ -335,10 +335,10 @@ describe('AWSLambda', () => { }; expect(rv).toStrictEqual(42); - // @ts-ignore see "Why @ts-ignore" note + // @ts-expect-error see "Why @ts-expect-error" note expect(SentryNode.fakeHub.startTransaction).toBeCalledWith(fakeTransactionContext); expectScopeSettings(fakeTransactionContext); - // @ts-ignore see "Why @ts-ignore" note + // @ts-expect-error see "Why @ts-expect-error" note expect(SentryNode.fakeTransaction.finish).toBeCalled(); expect(SentryNode.flush).toBeCalled(); }); @@ -373,11 +373,11 @@ describe('AWSLambda', () => { metadata: { source: 'component' }, }; - // @ts-ignore see "Why @ts-ignore" note + // @ts-expect-error see "Why @ts-expect-error" note expect(SentryNode.fakeHub.startTransaction).toBeCalledWith(fakeTransactionContext); expectScopeSettings(fakeTransactionContext); expect(SentryNode.captureException).toBeCalledWith(error, expect.any(Function)); - // @ts-ignore see "Why @ts-ignore" note + // @ts-expect-error see "Why @ts-expect-error" note expect(SentryNode.fakeTransaction.finish).toBeCalled(); expect(SentryNode.flush).toBeCalled(); } @@ -417,10 +417,10 @@ describe('AWSLambda', () => { }; expect(rv).toStrictEqual(42); - // @ts-ignore see "Why @ts-ignore" note + // @ts-expect-error see "Why @ts-expect-error" note expect(SentryNode.fakeHub.startTransaction).toBeCalledWith(fakeTransactionContext); expectScopeSettings(fakeTransactionContext); - // @ts-ignore see "Why @ts-ignore" note + // @ts-expect-error see "Why @ts-expect-error" note expect(SentryNode.fakeTransaction.finish).toBeCalled(); expect(SentryNode.flush).toBeCalled(); }); @@ -455,11 +455,11 @@ describe('AWSLambda', () => { metadata: { source: 'component' }, }; - // @ts-ignore see "Why @ts-ignore" note + // @ts-expect-error see "Why @ts-expect-error" note expect(SentryNode.fakeHub.startTransaction).toBeCalledWith(fakeTransactionContext); expectScopeSettings(fakeTransactionContext); expect(SentryNode.captureException).toBeCalledWith(error, expect.any(Function)); - // @ts-ignore see "Why @ts-ignore" note + // @ts-expect-error see "Why @ts-expect-error" note expect(SentryNode.fakeTransaction.finish).toBeCalled(); expect(SentryNode.flush).toBeCalled(); } @@ -479,14 +479,14 @@ describe('AWSLambda', () => { await wrappedHandler(fakeEvent, fakeContext, fakeCallback); } catch (e) { expect(SentryNode.captureException).toBeCalledWith(error, expect.any(Function)); - // @ts-ignore see "Why @ts-ignore" note + // @ts-expect-error see "Why @ts-expect-error" note const scopeFunction = SentryNode.captureException.mock.calls[0][1]; const event: Event = { exception: { values: [{}] } }; let evtProcessor: ((e: Event) => Event) | undefined = undefined; scopeFunction({ addEventProcessor: jest.fn().mockImplementation(proc => (evtProcessor = proc)) }); expect(evtProcessor).toBeInstanceOf(Function); - // @ts-ignore just mocking around... + // @ts-expect-error just mocking around... expect(evtProcessor(event).exception.values[0].mechanism).toEqual({ handled: false, type: 'generic', @@ -524,7 +524,7 @@ describe('AWSLambda', () => { }, }; - // @ts-ignore see "Why @ts-ignore" note + // @ts-expect-error see "Why @ts-expect-error" note Sentry.addGlobalEventProcessor.mockImplementationOnce(cb => cb(eventWithSomeData)); Sentry.AWSLambda.init({}); diff --git a/packages/serverless/test/awsservices.test.ts b/packages/serverless/test/awsservices.test.ts index b37f9aa527f0..cca793549c99 100644 --- a/packages/serverless/test/awsservices.test.ts +++ b/packages/serverless/test/awsservices.test.ts @@ -5,7 +5,7 @@ import * as nock from 'nock'; import { AWSServices } from '../src/awsservices'; /** - * Why @ts-ignore some Sentry.X calls + * Why @ts-expect-error some Sentry.X calls * * A hack-ish way to contain everything related to mocks in the same __mocks__ file. * Thanks to this, we don't have to do more magic than necessary. Just add and export desired method and assert on it. @@ -16,7 +16,7 @@ describe('AWSServices', () => { new AWSServices().setupOnce(); }); afterEach(() => { - // @ts-ignore see "Why @ts-ignore" note + // @ts-expect-error see "Why @ts-expect-error" note SentryNode.resetMocks(); }); afterAll(() => { @@ -30,13 +30,13 @@ describe('AWSServices', () => { nock('https://foo.s3.amazonaws.com').get('/bar').reply(200, 'contents'); const data = await s3.getObject({ Bucket: 'foo', Key: 'bar' }).promise(); expect(data.Body?.toString('utf-8')).toEqual('contents'); - // @ts-ignore see "Why @ts-ignore" note + // @ts-expect-error see "Why @ts-expect-error" note expect(SentryNode.fakeTransaction.startChild).toBeCalledWith({ op: 'http.client', origin: 'auto.http.serverless', description: 'aws.s3.getObject foo', }); - // @ts-ignore see "Why @ts-ignore" note + // @ts-expect-error see "Why @ts-expect-error" note expect(SentryNode.fakeSpan.finish).toBeCalled(); }); @@ -48,7 +48,7 @@ describe('AWSServices', () => { expect(data.Body?.toString('utf-8')).toEqual('contents'); done(); }); - // @ts-ignore see "Why @ts-ignore" note + // @ts-expect-error see "Why @ts-expect-error" note expect(SentryNode.fakeTransaction.startChild).toBeCalledWith({ op: 'http.client', origin: 'auto.http.serverless', @@ -64,7 +64,7 @@ describe('AWSServices', () => { nock('https://lambda.eu-north-1.amazonaws.com').post('/2015-03-31/functions/foo/invocations').reply(201, 'reply'); const data = await lambda.invoke({ FunctionName: 'foo' }).promise(); expect(data.Payload?.toString('utf-8')).toEqual('reply'); - // @ts-ignore see "Why @ts-ignore" note + // @ts-expect-error see "Why @ts-expect-error" note expect(SentryNode.fakeTransaction.startChild).toBeCalledWith({ op: 'http.client', origin: 'auto.http.serverless', diff --git a/packages/serverless/test/gcpfunction.test.ts b/packages/serverless/test/gcpfunction.test.ts index 812447106ad5..5f830b05cdee 100644 --- a/packages/serverless/test/gcpfunction.test.ts +++ b/packages/serverless/test/gcpfunction.test.ts @@ -14,7 +14,7 @@ import type { Response, } from '../src/gcpfunction/general'; /** - * Why @ts-ignore some Sentry.X calls + * Why @ts-expect-error some Sentry.X calls * * A hack-ish way to contain everything related to mocks in the same __mocks__ file. * Thanks to this, we don't have to do more magic than necessary. Just add and export desired method and assert on it. @@ -22,7 +22,7 @@ import type { describe('GCPFunction', () => { afterEach(() => { - // @ts-ignore see "Why @ts-ignore" note + // @ts-expect-error see "Why @ts-expect-error" note SentryNode.resetMocks(); }); @@ -117,16 +117,16 @@ describe('GCPFunction', () => { origin: 'auto.function.serverless.gcp_http', metadata: { source: 'route' }, }; - // @ts-ignore see "Why @ts-ignore" note + // @ts-expect-error see "Why @ts-expect-error" note const fakeTransaction = { ...SentryNode.fakeTransaction, ...fakeTransactionContext }; - // @ts-ignore see "Why @ts-ignore" note + // @ts-expect-error see "Why @ts-expect-error" note expect(SentryNode.fakeHub.startTransaction).toBeCalledWith(fakeTransactionContext); - // @ts-ignore see "Why @ts-ignore" note + // @ts-expect-error see "Why @ts-expect-error" note expect(SentryNode.fakeScope.setSpan).toBeCalledWith(fakeTransaction); - // @ts-ignore see "Why @ts-ignore" note + // @ts-expect-error see "Why @ts-expect-error" note expect(SentryNode.fakeTransaction.setHttpStatus).toBeCalledWith(200); - // @ts-ignore see "Why @ts-ignore" note + // @ts-expect-error see "Why @ts-expect-error" note expect(SentryNode.fakeTransaction.finish).toBeCalled(); expect(SentryNode.flush).toBeCalledWith(2000); }); @@ -160,11 +160,8 @@ describe('GCPFunction', () => { }, }; - // @ts-ignore see "Why @ts-ignore" note + // @ts-expect-error see "Why @ts-expect-error" note expect(SentryNode.fakeHub.startTransaction).toBeCalledWith(fakeTransactionContext); - - // @ts-ignore see "Why @ts-ignore" note - // expect(SentryNode.fakeHub.startTransaction).toBeCalledWith(expect.objectContaining(fakeTransactionContext)); }); test('capture error', async () => { @@ -191,15 +188,15 @@ describe('GCPFunction', () => { parentSampled: false, metadata: { dynamicSamplingContext: {}, source: 'route' }, }; - // @ts-ignore see "Why @ts-ignore" note + // @ts-expect-error see "Why @ts-expect-error" note const fakeTransaction = { ...SentryNode.fakeTransaction, ...fakeTransactionContext }; - // @ts-ignore see "Why @ts-ignore" note + // @ts-expect-error see "Why @ts-expect-error" note expect(SentryNode.fakeHub.startTransaction).toBeCalledWith(fakeTransactionContext); - // @ts-ignore see "Why @ts-ignore" note + // @ts-expect-error see "Why @ts-expect-error" note expect(SentryNode.fakeScope.setSpan).toBeCalledWith(fakeTransaction); expect(SentryNode.captureException).toBeCalledWith(error, expect.any(Function)); - // @ts-ignore see "Why @ts-ignore" note + // @ts-expect-error see "Why @ts-expect-error" note expect(SentryNode.fakeTransaction.finish).toBeCalled(); expect(SentryNode.flush).toBeCalled(); }); @@ -254,7 +251,7 @@ describe('GCPFunction', () => { }), ); - // @ts-ignore see "Why @ts-ignore" note + // @ts-expect-error see "Why @ts-expect-error" note expect(SentryNode.fakeScope.setSDKProcessingMetadata).toHaveBeenCalledWith({ request: { method: 'POST', @@ -282,14 +279,14 @@ describe('GCPFunction', () => { origin: 'auto.function.serverless.gcp_event', metadata: { source: 'component' }, }; - // @ts-ignore see "Why @ts-ignore" note + // @ts-expect-error see "Why @ts-expect-error" note const fakeTransaction = { ...SentryNode.fakeTransaction, ...fakeTransactionContext }; - // @ts-ignore see "Why @ts-ignore" note + // @ts-expect-error see "Why @ts-expect-error" note expect(SentryNode.fakeHub.startTransaction).toBeCalledWith(fakeTransactionContext); - // @ts-ignore see "Why @ts-ignore" note + // @ts-expect-error see "Why @ts-expect-error" note expect(SentryNode.fakeScope.setSpan).toBeCalledWith(fakeTransaction); - // @ts-ignore see "Why @ts-ignore" note + // @ts-expect-error see "Why @ts-expect-error" note expect(SentryNode.fakeTransaction.finish).toBeCalled(); expect(SentryNode.flush).toBeCalledWith(2000); }); @@ -310,15 +307,15 @@ describe('GCPFunction', () => { origin: 'auto.function.serverless.gcp_event', metadata: { source: 'component' }, }; - // @ts-ignore see "Why @ts-ignore" note + // @ts-expect-error see "Why @ts-expect-error" note const fakeTransaction = { ...SentryNode.fakeTransaction, ...fakeTransactionContext }; - // @ts-ignore see "Why @ts-ignore" note + // @ts-expect-error see "Why @ts-expect-error" note expect(SentryNode.fakeHub.startTransaction).toBeCalledWith(fakeTransactionContext); - // @ts-ignore see "Why @ts-ignore" note + // @ts-expect-error see "Why @ts-expect-error" note expect(SentryNode.fakeScope.setSpan).toBeCalledWith(fakeTransaction); expect(SentryNode.captureException).toBeCalledWith(error, expect.any(Function)); - // @ts-ignore see "Why @ts-ignore" note + // @ts-expect-error see "Why @ts-expect-error" note expect(SentryNode.fakeTransaction.finish).toBeCalled(); expect(SentryNode.flush).toBeCalled(); }); @@ -343,14 +340,14 @@ describe('GCPFunction', () => { origin: 'auto.function.serverless.gcp_event', metadata: { source: 'component' }, }; - // @ts-ignore see "Why @ts-ignore" note + // @ts-expect-error see "Why @ts-expect-error" note const fakeTransaction = { ...SentryNode.fakeTransaction, ...fakeTransactionContext }; - // @ts-ignore see "Why @ts-ignore" note + // @ts-expect-error see "Why @ts-expect-error" note expect(SentryNode.fakeHub.startTransaction).toBeCalledWith(fakeTransactionContext); - // @ts-ignore see "Why @ts-ignore" note + // @ts-expect-error see "Why @ts-expect-error" note expect(SentryNode.fakeScope.setSpan).toBeCalledWith(fakeTransaction); - // @ts-ignore see "Why @ts-ignore" note + // @ts-expect-error see "Why @ts-expect-error" note expect(SentryNode.fakeTransaction.finish).toBeCalled(); expect(SentryNode.flush).toBeCalledWith(2000); }); @@ -375,15 +372,15 @@ describe('GCPFunction', () => { origin: 'auto.function.serverless.gcp_event', metadata: { source: 'component' }, }; - // @ts-ignore see "Why @ts-ignore" note + // @ts-expect-error see "Why @ts-expect-error" note const fakeTransaction = { ...SentryNode.fakeTransaction, ...fakeTransactionContext }; - // @ts-ignore see "Why @ts-ignore" note + // @ts-expect-error see "Why @ts-expect-error" note expect(SentryNode.fakeHub.startTransaction).toBeCalledWith(fakeTransactionContext); - // @ts-ignore see "Why @ts-ignore" note + // @ts-expect-error see "Why @ts-expect-error" note expect(SentryNode.fakeScope.setSpan).toBeCalledWith(fakeTransaction); expect(SentryNode.captureException).toBeCalledWith(error, expect.any(Function)); - // @ts-ignore see "Why @ts-ignore" note + // @ts-expect-error see "Why @ts-expect-error" note expect(SentryNode.fakeTransaction.finish).toBeCalled(); expect(SentryNode.flush).toBeCalled(); }); @@ -405,14 +402,14 @@ describe('GCPFunction', () => { origin: 'auto.function.serverless.gcp_event', metadata: { source: 'component' }, }; - // @ts-ignore see "Why @ts-ignore" note + // @ts-expect-error see "Why @ts-expect-error" note const fakeTransaction = { ...SentryNode.fakeTransaction, ...fakeTransactionContext }; - // @ts-ignore see "Why @ts-ignore" note + // @ts-expect-error see "Why @ts-expect-error" note expect(SentryNode.fakeHub.startTransaction).toBeCalledWith(fakeTransactionContext); - // @ts-ignore see "Why @ts-ignore" note + // @ts-expect-error see "Why @ts-expect-error" note expect(SentryNode.fakeScope.setSpan).toBeCalledWith(fakeTransaction); - // @ts-ignore see "Why @ts-ignore" note + // @ts-expect-error see "Why @ts-expect-error" note expect(SentryNode.fakeTransaction.finish).toBeCalled(); expect(SentryNode.flush).toBeCalledWith(2000); }); @@ -433,15 +430,15 @@ describe('GCPFunction', () => { origin: 'auto.function.serverless.gcp_event', metadata: { source: 'component' }, }; - // @ts-ignore see "Why @ts-ignore" note + // @ts-expect-error see "Why @ts-expect-error" note const fakeTransaction = { ...SentryNode.fakeTransaction, ...fakeTransactionContext }; - // @ts-ignore see "Why @ts-ignore" note + // @ts-expect-error see "Why @ts-expect-error" note expect(SentryNode.fakeHub.startTransaction).toBeCalledWith(fakeTransactionContext); - // @ts-ignore see "Why @ts-ignore" note + // @ts-expect-error see "Why @ts-expect-error" note expect(SentryNode.fakeScope.setSpan).toBeCalledWith(fakeTransaction); expect(SentryNode.captureException).toBeCalledWith(error, expect.any(Function)); - // @ts-ignore see "Why @ts-ignore" note + // @ts-expect-error see "Why @ts-expect-error" note expect(SentryNode.fakeTransaction.finish).toBeCalled(); expect(SentryNode.flush).toBeCalled(); }); @@ -462,12 +459,12 @@ describe('GCPFunction', () => { origin: 'auto.function.serverless.gcp_event', metadata: { source: 'component' }, }; - // @ts-ignore see "Why @ts-ignore" note + // @ts-expect-error see "Why @ts-expect-error" note const fakeTransaction = { ...SentryNode.fakeTransaction, ...fakeTransactionContext }; - // @ts-ignore see "Why @ts-ignore" note + // @ts-expect-error see "Why @ts-expect-error" note expect(SentryNode.fakeHub.startTransaction).toBeCalledWith(fakeTransactionContext); - // @ts-ignore see "Why @ts-ignore" note + // @ts-expect-error see "Why @ts-expect-error" note expect(SentryNode.fakeScope.setSpan).toBeCalledWith(fakeTransaction); expect(SentryNode.captureException).toBeCalledWith(error, expect.any(Function)); }); @@ -485,14 +482,14 @@ describe('GCPFunction', () => { expect(SentryNode.captureException).toBeCalledWith(error, expect.any(Function)); - // @ts-ignore just mocking around... + // @ts-expect-error just mocking around... const scopeFunction = SentryNode.captureException.mock.calls[0][1]; const event: Event = { exception: { values: [{}] } }; let evtProcessor: ((e: Event) => Event) | undefined = undefined; scopeFunction({ addEventProcessor: jest.fn().mockImplementation(proc => (evtProcessor = proc)) }); expect(evtProcessor).toBeInstanceOf(Function); - // @ts-ignore just mocking around... + // @ts-expect-error just mocking around... expect(evtProcessor(event).exception.values[0].mechanism).toEqual({ handled: false, type: 'generic', @@ -505,7 +502,7 @@ describe('GCPFunction', () => { const handler: EventFunction = (_data, _context) => 42; const wrappedHandler = wrapEventFunction(handler); await handleEvent(wrappedHandler); - // @ts-ignore see "Why @ts-ignore" note + // @ts-expect-error see "Why @ts-expect-error" note expect(SentryNode.fakeScope.setContext).toBeCalledWith('gcp.function.context', { eventType: 'event.type', resource: 'some.resource', @@ -528,14 +525,14 @@ describe('GCPFunction', () => { origin: 'auto.function.serverless.gcp_cloud_event', metadata: { source: 'component' }, }; - // @ts-ignore see "Why @ts-ignore" note + // @ts-expect-error see "Why @ts-expect-error" note const fakeTransaction = { ...SentryNode.fakeTransaction, ...fakeTransactionContext }; - // @ts-ignore see "Why @ts-ignore" note + // @ts-expect-error see "Why @ts-expect-error" note expect(SentryNode.fakeHub.startTransaction).toBeCalledWith(fakeTransactionContext); - // @ts-ignore see "Why @ts-ignore" note + // @ts-expect-error see "Why @ts-expect-error" note expect(SentryNode.fakeScope.setSpan).toBeCalledWith(fakeTransaction); - // @ts-ignore see "Why @ts-ignore" note + // @ts-expect-error see "Why @ts-expect-error" note expect(SentryNode.fakeTransaction.finish).toBeCalled(); expect(SentryNode.flush).toBeCalledWith(2000); }); @@ -556,15 +553,15 @@ describe('GCPFunction', () => { origin: 'auto.function.serverless.gcp_cloud_event', metadata: { source: 'component' }, }; - // @ts-ignore see "Why @ts-ignore" note + // @ts-expect-error see "Why @ts-expect-error" note const fakeTransaction = { ...SentryNode.fakeTransaction, ...fakeTransactionContext }; - // @ts-ignore see "Why @ts-ignore" note + // @ts-expect-error see "Why @ts-expect-error" note expect(SentryNode.fakeHub.startTransaction).toBeCalledWith(fakeTransactionContext); - // @ts-ignore see "Why @ts-ignore" note + // @ts-expect-error see "Why @ts-expect-error" note expect(SentryNode.fakeScope.setSpan).toBeCalledWith(fakeTransaction); expect(SentryNode.captureException).toBeCalledWith(error, expect.any(Function)); - // @ts-ignore see "Why @ts-ignore" note + // @ts-expect-error see "Why @ts-expect-error" note expect(SentryNode.fakeTransaction.finish).toBeCalled(); expect(SentryNode.flush).toBeCalled(); }); @@ -586,14 +583,14 @@ describe('GCPFunction', () => { origin: 'auto.function.serverless.gcp_cloud_event', metadata: { source: 'component' }, }; - // @ts-ignore see "Why @ts-ignore" note + // @ts-expect-error see "Why @ts-expect-error" note const fakeTransaction = { ...SentryNode.fakeTransaction, ...fakeTransactionContext }; - // @ts-ignore see "Why @ts-ignore" note + // @ts-expect-error see "Why @ts-expect-error" note expect(SentryNode.fakeHub.startTransaction).toBeCalledWith(fakeTransactionContext); - // @ts-ignore see "Why @ts-ignore" note + // @ts-expect-error see "Why @ts-expect-error" note expect(SentryNode.fakeScope.setSpan).toBeCalledWith(fakeTransaction); - // @ts-ignore see "Why @ts-ignore" note + // @ts-expect-error see "Why @ts-expect-error" note expect(SentryNode.fakeTransaction.finish).toBeCalled(); expect(SentryNode.flush).toBeCalledWith(2000); }); @@ -614,15 +611,15 @@ describe('GCPFunction', () => { origin: 'auto.function.serverless.gcp_cloud_event', metadata: { source: 'component' }, }; - // @ts-ignore see "Why @ts-ignore" note + // @ts-expect-error see "Why @ts-expect-error" note const fakeTransaction = { ...SentryNode.fakeTransaction, ...fakeTransactionContext }; - // @ts-ignore see "Why @ts-ignore" note + // @ts-expect-error see "Why @ts-expect-error" note expect(SentryNode.fakeHub.startTransaction).toBeCalledWith(fakeTransactionContext); - // @ts-ignore see "Why @ts-ignore" note + // @ts-expect-error see "Why @ts-expect-error" note expect(SentryNode.fakeScope.setSpan).toBeCalledWith(fakeTransaction); expect(SentryNode.captureException).toBeCalledWith(error, expect.any(Function)); - // @ts-ignore see "Why @ts-ignore" note + // @ts-expect-error see "Why @ts-expect-error" note expect(SentryNode.fakeTransaction.finish).toBeCalled(); expect(SentryNode.flush).toBeCalled(); }); @@ -643,12 +640,12 @@ describe('GCPFunction', () => { origin: 'auto.function.serverless.gcp_cloud_event', metadata: { source: 'component' }, }; - // @ts-ignore see "Why @ts-ignore" note + // @ts-expect-error see "Why @ts-expect-error" note const fakeTransaction = { ...SentryNode.fakeTransaction, ...fakeTransactionContext }; - // @ts-ignore see "Why @ts-ignore" note + // @ts-expect-error see "Why @ts-expect-error" note expect(SentryNode.fakeHub.startTransaction).toBeCalledWith(fakeTransactionContext); - // @ts-ignore see "Why @ts-ignore" note + // @ts-expect-error see "Why @ts-expect-error" note expect(SentryNode.fakeScope.setSpan).toBeCalledWith(fakeTransaction); expect(SentryNode.captureException).toBeCalledWith(error, expect.any(Function)); @@ -661,7 +658,7 @@ describe('GCPFunction', () => { const handler: CloudEventFunction = _context => 42; const wrappedHandler = wrapCloudEventFunction(handler); await handleCloudEvent(wrappedHandler); - // @ts-ignore see "Why @ts-ignore" note + // @ts-expect-error see "Why @ts-expect-error" note expect(SentryNode.fakeScope.setContext).toBeCalledWith('gcp.function.context', { type: 'event.type' }); }); @@ -695,7 +692,7 @@ describe('GCPFunction', () => { }, }; - // @ts-ignore see "Why @ts-ignore" note + // @ts-expect-error see "Why @ts-expect-error" note Sentry.addGlobalEventProcessor.mockImplementationOnce(cb => cb(eventWithSomeData)); Sentry.GCPFunction.init({}); diff --git a/packages/serverless/test/google-cloud-grpc.test.ts b/packages/serverless/test/google-cloud-grpc.test.ts index 212faefa111f..d810dba7b011 100644 --- a/packages/serverless/test/google-cloud-grpc.test.ts +++ b/packages/serverless/test/google-cloud-grpc.test.ts @@ -12,7 +12,7 @@ import * as path from 'path'; import { GoogleCloudGrpc } from '../src/google-cloud-grpc'; /** - * Why @ts-ignore some Sentry.X calls + * Why @ts-expect-error some Sentry.X calls * * A hack-ish way to contain everything related to mocks in the same __mocks__ file. * Thanks to this, we don't have to do more magic than necessary. Just add and export desired method and assert on it. @@ -85,7 +85,7 @@ describe('GoogleCloudGrpc tracing', () => { nock('https://www.googleapis.com').post('/oauth2/v4/token').reply(200, []); }); afterEach(() => { - // @ts-ignore see "Why @ts-ignore" note + // @ts-expect-error see "Why @ts-expect-error" note SentryNode.resetMocks(); spyConnect.mockClear(); }); @@ -96,9 +96,9 @@ describe('GoogleCloudGrpc tracing', () => { // We use google cloud pubsub as an example of grpc service for which we can trace requests. describe('pubsub', () => { - // @ts-ignore see "Why @ts-ignore" note + // @ts-expect-error see "Why @ts-expect-error" note const dnsLookup = dns.lookup as jest.Mock; - // @ts-ignore see "Why @ts-ignore" note + // @ts-expect-error see "Why @ts-expect-error" note const resolveTxt = dns.resolveTxt as jest.Mock; dnsLookup.mockImplementation((hostname, ...args) => { expect(hostname).toEqual('pubsub.googleapis.com'); @@ -126,7 +126,7 @@ describe('GoogleCloudGrpc tracing', () => { mockHttp2Session().mockUnaryRequest(Buffer.from('00000000120a1031363337303834313536363233383630', 'hex')); const resp = await pubsub.topic('nicetopic').publish(Buffer.from('data')); expect(resp).toEqual('1637084156623860'); - // @ts-ignore see "Why @ts-ignore" note + // @ts-expect-error see "Why @ts-expect-error" note expect(SentryNode.fakeTransaction.startChild).toBeCalledWith({ op: 'grpc.pubsub', origin: 'auto.grpc.serverless', diff --git a/packages/serverless/test/google-cloud-http.test.ts b/packages/serverless/test/google-cloud-http.test.ts index b2201f1728d7..7327ba01b97e 100644 --- a/packages/serverless/test/google-cloud-http.test.ts +++ b/packages/serverless/test/google-cloud-http.test.ts @@ -7,7 +7,7 @@ import * as path from 'path'; import { GoogleCloudHttp } from '../src/google-cloud-http'; /** - * Why @ts-ignore some Sentry.X calls + * Why @ts-expect-error some Sentry.X calls * * A hack-ish way to contain everything related to mocks in the same __mocks__ file. * Thanks to this, we don't have to do more magic than necessary. Just add and export desired method and assert on it. @@ -23,7 +23,7 @@ describe('GoogleCloudHttp tracing', () => { .reply(200, '{"access_token":"a.b.c","expires_in":3599,"token_type":"Bearer"}'); }); afterEach(() => { - // @ts-ignore see "Why @ts-ignore" note + // @ts-expect-error see "Why @ts-expect-error" note SentryNode.resetMocks(); }); afterAll(() => { @@ -57,13 +57,13 @@ describe('GoogleCloudHttp tracing', () => { ); const resp = await bigquery.query('SELECT true AS foo'); expect(resp).toEqual([[{ foo: true }]]); - // @ts-ignore see "Why @ts-ignore" note + // @ts-expect-error see "Why @ts-expect-error" note expect(SentryNode.fakeTransaction.startChild).toBeCalledWith({ op: 'http.client.bigquery', origin: 'auto.http.serverless', description: 'POST /jobs', }); - // @ts-ignore see "Why @ts-ignore" note + // @ts-expect-error see "Why @ts-expect-error" note expect(SentryNode.fakeTransaction.startChild).toBeCalledWith({ op: 'http.client.bigquery', origin: 'auto.http.serverless', diff --git a/packages/sveltekit/src/vite/sourceMaps.ts b/packages/sveltekit/src/vite/sourceMaps.ts index 6f2b7086786a..c3a4c86e70ad 100644 --- a/packages/sveltekit/src/vite/sourceMaps.ts +++ b/packages/sveltekit/src/vite/sourceMaps.ts @@ -5,7 +5,7 @@ import { sentryVitePlugin } from '@sentry/vite-plugin'; import * as child_process from 'child_process'; import * as fs from 'fs'; import * as path from 'path'; -// @ts-ignore -sorcery has no types :( +// @ts-expect-error -sorcery has no types :( // eslint-disable-next-line import/default import * as sorcery from 'sorcery'; import type { Plugin } from 'vite'; @@ -115,7 +115,7 @@ export async function makeCustomSentryVitePlugin(options?: CustomSentryVitePlugi moduleSideEffects: true, }; } - // @ts-ignore - this hook exists on the plugin! + // @ts-expect-error - this hook exists on the plugin! return sentryPlugin.resolveId(id, _importer, _ref); }, @@ -146,7 +146,7 @@ export async function makeCustomSentryVitePlugin(options?: CustomSentryVitePlugi const globalValuesImport = `; import "${VIRTUAL_GLOBAL_VALUES_FILE}";`; modifiedCode = `${code}\n${globalValuesImport}\n`; } - // @ts-ignore - this hook exists on the plugin! + // @ts-expect-error - this hook exists on the plugin! return sentryPlugin.transform(modifiedCode, id); }, @@ -204,7 +204,7 @@ export async function makeCustomSentryVitePlugin(options?: CustomSentryVitePlugi }); try { - // @ts-ignore - this hook exists on the plugin! + // @ts-expect-error - this hook exists on the plugin! await sentryPlugin.writeBundle(); } catch (_) { // eslint-disable-next-line no-console @@ -230,7 +230,7 @@ function getFiles(dir: string): string[] { } const dirents = fs.readdirSync(dir, { withFileTypes: true }); // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore + // @ts-expect-error const files: string[] = dirents.map(dirent => { const resFileOrDir = path.resolve(dir, dirent.name); return dirent.isDirectory() ? getFiles(resFileOrDir) : resFileOrDir; diff --git a/packages/sveltekit/src/vite/svelteConfig.ts b/packages/sveltekit/src/vite/svelteConfig.ts index 4e69ad8ef3b0..1384769be52f 100644 --- a/packages/sveltekit/src/vite/svelteConfig.ts +++ b/packages/sveltekit/src/vite/svelteConfig.ts @@ -22,7 +22,7 @@ export async function loadSvelteConfig(): Promise { if (!fs.existsSync(configFile)) { return {}; } - // @ts-ignore - we explicitly want to import the svelte config here. + // @ts-expect-error - we explicitly want to import the svelte config here. const svelteConfigModule = await import(`${url.pathToFileURL(configFile).href}`); // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access @@ -83,7 +83,7 @@ async function getNodeAdapterOutputDir(svelteConfig: Config): Promise { outputDir = dest.replace(/\/client.*/, ''); throw new Error('We got what we came for, throwing to exit the adapter'); }, - // @ts-ignore - No need to implement the other methods + // @ts-expect-error - No need to implement the other methods log: { // eslint-disable-next-line @typescript-eslint/no-empty-function -- this should be a noop minor() {}, @@ -96,7 +96,7 @@ async function getNodeAdapterOutputDir(svelteConfig: Config): Promise { config: { kit: { - // @ts-ignore - the builder expects a validated config but for our purpose it's fine to just pass this partial config + // @ts-expect-error - the builder expects a validated config but for our purpose it's fine to just pass this partial config paths: { base: svelteConfig.kit?.paths?.base || '', }, diff --git a/packages/sveltekit/test/client/router.test.ts b/packages/sveltekit/test/client/router.test.ts index 65bc39d5cdbb..37ebebd8d837 100644 --- a/packages/sveltekit/test/client/router.test.ts +++ b/packages/sveltekit/test/client/router.test.ts @@ -64,7 +64,7 @@ describe('sveltekitRoutingInstrumentation', () => { }); // We emit an update to the `page` store to simulate the SvelteKit router lifecycle - // @ts-ignore This is fine because we testUtils/stores.ts defines `page` as a writable store + // @ts-expect-error This is fine because we testUtils/stores.ts defines `page` as a writable store page.set({ route: { id: 'testRoute' } }); // This should update the transaction name with the parameterized route: @@ -81,7 +81,7 @@ describe('sveltekitRoutingInstrumentation', () => { svelteKitRoutingInstrumentation(mockedStartTransaction, false, false); // We emit an update to the `navigating` store to simulate the SvelteKit navigation lifecycle - // @ts-ignore This is fine because we testUtils/stores.ts defines `navigating` as a writable store + // @ts-expect-error This is fine because we testUtils/stores.ts defines `navigating` as a writable store navigating.set({ from: { route: { id: '/users' }, url: { pathname: '/users' } }, to: { route: { id: '/users/[id]' }, url: { pathname: '/users/7762' } }, @@ -95,7 +95,7 @@ describe('sveltekitRoutingInstrumentation', () => { svelteKitRoutingInstrumentation(mockedStartTransaction, false, true); // We emit an update to the `navigating` store to simulate the SvelteKit navigation lifecycle - // @ts-ignore This is fine because we testUtils/stores.ts defines `navigating` as a writable store + // @ts-expect-error This is fine because we testUtils/stores.ts defines `navigating` as a writable store navigating.set({ from: { route: { id: '/users' }, url: { pathname: '/users' } }, to: { route: { id: '/users/[id]' }, url: { pathname: '/users/7762' } }, @@ -124,7 +124,7 @@ describe('sveltekitRoutingInstrumentation', () => { expect(returnedTransaction?.setTag).toHaveBeenCalledWith('from', '/users'); // We emit `null` here to simulate the end of the navigation lifecycle - // @ts-ignore this is fine + // @ts-expect-error this is fine navigating.set(null); expect(routingSpanFinishSpy).toHaveBeenCalledTimes(1); @@ -135,7 +135,7 @@ describe('sveltekitRoutingInstrumentation', () => { svelteKitRoutingInstrumentation(mockedStartTransaction, false, true); // We emit an update to the `navigating` store to simulate the SvelteKit navigation lifecycle - // @ts-ignore This is fine because we testUtils/stores.ts defines `navigating` as a writable store + // @ts-expect-error This is fine because we testUtils/stores.ts defines `navigating` as a writable store navigating.set({ from: { route: { id: '/users/[id]' }, url: { pathname: '/users/7762' } }, to: { route: { id: '/users/[id]' }, url: { pathname: '/users/7762' } }, @@ -147,7 +147,7 @@ describe('sveltekitRoutingInstrumentation', () => { it('starts a navigation transaction if the raw navigation origin and destination are not equal', () => { svelteKitRoutingInstrumentation(mockedStartTransaction, false, true); - // @ts-ignore This is fine + // @ts-expect-error This is fine navigating.set({ from: { route: { id: '/users/[id]' }, url: { pathname: '/users/7762' } }, to: { route: { id: '/users/[id]' }, url: { pathname: '/users/223412' } }, @@ -180,7 +180,7 @@ describe('sveltekitRoutingInstrumentation', () => { // window.location.pathame is "/" in tests - // @ts-ignore This is fine + // @ts-expect-error This is fine navigating.set({ to: { route: {}, url: { pathname: '/' } }, }); diff --git a/packages/sveltekit/test/client/sdk.test.ts b/packages/sveltekit/test/client/sdk.test.ts index 5ff3b9f9e846..b965e37538da 100644 --- a/packages/sveltekit/test/client/sdk.test.ts +++ b/packages/sveltekit/test/client/sdk.test.ts @@ -41,12 +41,12 @@ describe('Sentry client SDK', () => { it('sets the runtime tag on the scope', () => { const currentScope = getCurrentHub().getScope(); - // @ts-ignore need access to protected _tags attribute + // @ts-expect-error need access to protected _tags attribute expect(currentScope._tags).toEqual({}); init({ dsn: 'https://public@dsn.ingest.sentry.io/1337' }); - // @ts-ignore need access to protected _tags attribute + // @ts-expect-error need access to protected _tags attribute expect(currentScope._tags).toEqual({ runtime: 'browser' }); }); @@ -88,7 +88,7 @@ describe('Sentry client SDK', () => { // This is the closest we can get to unit-testing the `__SENTRY_TRACING__` tree-shaking guard // IRL, the code to add the integration would most likely be removed by the bundler. - // @ts-ignore this is fine in the test + // @ts-expect-error this is fine in the test globalThis.__SENTRY_TRACING__ = false; init({ @@ -102,7 +102,7 @@ describe('Sentry client SDK', () => { expect(integrationsToInit).not.toContainEqual(expect.objectContaining({ name: 'BrowserTracing' })); expect(browserTracing).toBeUndefined(); - // @ts-ignore this is fine in the test + // @ts-expect-error this is fine in the test delete globalThis.__SENTRY_TRACING__; }); diff --git a/packages/sveltekit/test/server/load.test.ts b/packages/sveltekit/test/server/load.test.ts index 6e27829f4004..c2b35bb7d2e9 100644 --- a/packages/sveltekit/test/server/load.test.ts +++ b/packages/sveltekit/test/server/load.test.ts @@ -364,7 +364,7 @@ describe('wrapServerLoadWithSentry calls trace', () => { it('falls back to the raw url if `event.route.id` is not available', async () => { const event = getServerOnlyArgs(); - // @ts-ignore - this is fine (just tests here) + // @ts-expect-error - this is fine (just tests here) delete event.route; const wrappedLoad = wrapServerLoadWithSentry(serverLoad); await wrappedLoad(event); diff --git a/packages/sveltekit/test/server/sdk.test.ts b/packages/sveltekit/test/server/sdk.test.ts index c68be548c91c..ce936901110d 100644 --- a/packages/sveltekit/test/server/sdk.test.ts +++ b/packages/sveltekit/test/server/sdk.test.ts @@ -39,12 +39,12 @@ describe('Sentry server SDK', () => { it('sets the runtime tag on the scope', () => { const currentScope = getCurrentHub().getScope(); - // @ts-ignore need access to protected _tags attribute + // @ts-expect-error need access to protected _tags attribute expect(currentScope._tags).toEqual({}); init({ dsn: 'https://public@dsn.ingest.sentry.io/1337' }); - // @ts-ignore need access to protected _tags attribute + // @ts-expect-error need access to protected _tags attribute expect(currentScope._tags).toEqual({ runtime: 'node' }); }); }); diff --git a/packages/sveltekit/test/server/utils.test.ts b/packages/sveltekit/test/server/utils.test.ts index 2fd9b0492013..272dba8330ce 100644 --- a/packages/sveltekit/test/server/utils.test.ts +++ b/packages/sveltekit/test/server/utils.test.ts @@ -80,7 +80,7 @@ describe('rewriteFramesIteratee', () => { }; const originalRewriteFrames = new RewriteFrames(); - // @ts-ignore this property exists + // @ts-expect-error this property exists const defaultIteratee = originalRewriteFrames._iteratee; const defaultResult = defaultIteratee({ ...frame }); diff --git a/packages/sveltekit/test/vite/autoInstrument.test.ts b/packages/sveltekit/test/vite/autoInstrument.test.ts index 13ee56eef3c6..f10c828b48c3 100644 --- a/packages/sveltekit/test/vite/autoInstrument.test.ts +++ b/packages/sveltekit/test/vite/autoInstrument.test.ts @@ -12,10 +12,10 @@ let fileContent: string | undefined; vi.mock('fs', async () => { const actual = await vi.importActual('fs'); return { - // @ts-ignore this exists, I promise! + // @ts-expect-error this exists, I promise! ...actual, promises: { - // @ts-ignore this also exists, I promise! + // @ts-expect-error this also exists, I promise! ...actual.promises, readFile: vi.fn().mockImplementation(() => { return fileContent || DEFAULT_CONTENT; @@ -61,7 +61,7 @@ describe('makeAutoInstrumentationPlugin()', () => { ])('transform %s files', (path: string) => { it('wraps universal load if `load` option is `true`', async () => { const plugin = makeAutoInstrumentationPlugin({ debug: false, load: true, serverLoad: true }); - // @ts-ignore this exists + // @ts-expect-error this exists const loadResult = await plugin.load(path); expect(loadResult).toEqual( 'import { wrapLoadWithSentry } from "@sentry/sveltekit";' + @@ -77,7 +77,7 @@ describe('makeAutoInstrumentationPlugin()', () => { load: false, serverLoad: false, }); - // @ts-ignore this exists + // @ts-expect-error this exists const loadResult = await plugin.load(path); expect(loadResult).toEqual(null); }); @@ -95,7 +95,7 @@ describe('makeAutoInstrumentationPlugin()', () => { ])('transform %s files', (path: string) => { it('wraps universal load if `load` option is `true`', async () => { const plugin = makeAutoInstrumentationPlugin({ debug: false, load: false, serverLoad: true }); - // @ts-ignore this exists + // @ts-expect-error this exists const loadResult = await plugin.load(path); expect(loadResult).toEqual( 'import { wrapServerLoadWithSentry } from "@sentry/sveltekit";' + @@ -111,7 +111,7 @@ describe('makeAutoInstrumentationPlugin()', () => { load: false, serverLoad: false, }); - // @ts-ignore this exists + // @ts-expect-error this exists const loadResult = await plugin.load(path); expect(loadResult).toEqual(null); }); diff --git a/packages/sveltekit/test/vite/injectGlobalValues.test.ts b/packages/sveltekit/test/vite/injectGlobalValues.test.ts index 0fe6b07dc989..7250cfb07d80 100644 --- a/packages/sveltekit/test/vite/injectGlobalValues.test.ts +++ b/packages/sveltekit/test/vite/injectGlobalValues.test.ts @@ -3,7 +3,7 @@ import { getGlobalValueInjectionCode } from '../../src/vite/injectGlobalValues'; describe('getGlobalValueInjectionCode', () => { it('returns code that injects values into the global object', () => { const injectionCode = getGlobalValueInjectionCode({ - // @ts-ignore - just want to test this with multiple values + // @ts-expect-error - just want to test this with multiple values something: 'else', __sentry_sveltekit_output_dir: '.svelte-kit/output', }); diff --git a/packages/sveltekit/test/vite/sentrySvelteKitPlugins.test.ts b/packages/sveltekit/test/vite/sentrySvelteKitPlugins.test.ts index 923005b2e9f9..a05a0c672804 100644 --- a/packages/sveltekit/test/vite/sentrySvelteKitPlugins.test.ts +++ b/packages/sveltekit/test/vite/sentrySvelteKitPlugins.test.ts @@ -7,10 +7,10 @@ import * as sourceMaps from '../../src/vite/sourceMaps'; vi.mock('fs', async () => { const actual = await vi.importActual('fs'); return { - // @ts-ignore this exists, I promise! + // @ts-expect-error this exists, I promise! ...actual, promises: { - // @ts-ignore this also exists, I promise! + // @ts-expect-error this also exists, I promise! ...actual.promises, readFile: vi.fn().mockReturnValue('foo'), }, diff --git a/packages/sveltekit/test/vite/sourceMaps.test.ts b/packages/sveltekit/test/vite/sourceMaps.test.ts index 9d565aceab58..c312396675a1 100644 --- a/packages/sveltekit/test/vite/sourceMaps.test.ts +++ b/packages/sveltekit/test/vite/sourceMaps.test.ts @@ -43,7 +43,7 @@ describe('makeCustomSentryVitePlugin()', () => { describe('Custom sentry vite plugin', () => { it('enables source map generation', async () => { const plugin = await makeCustomSentryVitePlugin(); - // @ts-ignore this function exists! + // @ts-expect-error this function exists! const sentrifiedConfig = plugin.config({ build: { foo: {} }, test: {} }); expect(sentrifiedConfig).toEqual({ build: { @@ -56,7 +56,7 @@ describe('makeCustomSentryVitePlugin()', () => { it('injects the output dir into the server hooks file', async () => { const plugin = await makeCustomSentryVitePlugin(); - // @ts-ignore this function exists! + // @ts-expect-error this function exists! const transformedCode = await plugin.transform('foo', '/src/hooks.server.ts'); const expectedtransformedCode = 'foo\n; import "\0sentry-inject-global-values-file";\n'; expect(mockedSentryVitePlugin.transform).toHaveBeenCalledWith(expectedtransformedCode, '/src/hooks.server.ts'); @@ -65,9 +65,9 @@ describe('makeCustomSentryVitePlugin()', () => { it('uploads source maps during the SSR build', async () => { const plugin = await makeCustomSentryVitePlugin(); - // @ts-ignore this function exists! + // @ts-expect-error this function exists! plugin.configResolved({ build: { ssr: true } }); - // @ts-ignore this function exists! + // @ts-expect-error this function exists! plugin.closeBundle(); expect(mockedSentryVitePlugin.writeBundle).toHaveBeenCalledTimes(1); }); @@ -75,9 +75,9 @@ describe('makeCustomSentryVitePlugin()', () => { it("doesn't upload source maps during the non-SSR builds", async () => { const plugin = await makeCustomSentryVitePlugin(); - // @ts-ignore this function exists! + // @ts-expect-error this function exists! plugin.configResolved({ build: { ssr: false } }); - // @ts-ignore this function exists! + // @ts-expect-error this function exists! plugin.closeBundle(); expect(mockedSentryVitePlugin.writeBundle).not.toHaveBeenCalled(); }); @@ -93,12 +93,12 @@ describe('makeCustomSentryVitePlugin()', () => { const plugin = await makeCustomSentryVitePlugin(); - // @ts-ignore this function exists! + // @ts-expect-error this function exists! expect(plugin.closeBundle).not.toThrow(); - // @ts-ignore this function exists! + // @ts-expect-error this function exists! plugin.configResolved({ build: { ssr: true } }); - // @ts-ignore this function exists! + // @ts-expect-error this function exists! plugin.closeBundle(); expect(consoleWarnSpy).toHaveBeenCalledWith(expect.stringContaining('Failed to upload source maps')); diff --git a/packages/sveltekit/test/vitest.setup.ts b/packages/sveltekit/test/vitest.setup.ts index 57fdb8baef87..34955fbd4bd4 100644 --- a/packages/sveltekit/test/vitest.setup.ts +++ b/packages/sveltekit/test/vitest.setup.ts @@ -13,8 +13,8 @@ export function setup() { } if (!globalThis.fetch) { - // @ts-ignore - Needed for vitest to work with our fetch instrumentation + // @ts-expect-error - Needed for vitest to work with our fetch instrumentation globalThis.Request = class Request {}; - // @ts-ignore - Needed for vitest to work with our fetch instrumentation + // @ts-expect-error - Needed for vitest to work with our fetch instrumentation globalThis.Response = class Response {}; } diff --git a/packages/tracing-internal/src/browser/metrics/index.ts b/packages/tracing-internal/src/browser/metrics/index.ts index 56fc793d21f7..59f5895847ee 100644 --- a/packages/tracing-internal/src/browser/metrics/index.ts +++ b/packages/tracing-internal/src/browser/metrics/index.ts @@ -22,7 +22,7 @@ function msToSec(time: number): number { } function getBrowserPerformanceAPI(): Performance | undefined { - // @ts-ignore we want to make sure all of these are available, even if TS is sure they are + // @ts-expect-error we want to make sure all of these are available, even if TS is sure they are return WINDOW && WINDOW.addEventListener && WINDOW.performance; } @@ -40,7 +40,7 @@ let _clsEntry: LayoutShift | undefined; export function startTrackingWebVitals(): () => void { const performance = getBrowserPerformanceAPI(); if (performance && browserPerformanceTimeOrigin) { - // @ts-ignore we want to make sure all of these are available, even if TS is sure they are + // @ts-expect-error we want to make sure all of these are available, even if TS is sure they are if (performance.mark) { WINDOW.performance.mark('sentry-tracing-init'); } diff --git a/packages/tracing-internal/test/browser/backgroundtab.test.ts b/packages/tracing-internal/test/browser/backgroundtab.test.ts index 5ce370e6690f..3903f2eb2406 100644 --- a/packages/tracing-internal/test/browser/backgroundtab.test.ts +++ b/packages/tracing-internal/test/browser/backgroundtab.test.ts @@ -11,7 +11,7 @@ conditionalTest({ min: 10 })('registerBackgroundTabDetection', () => { let hub: Hub; beforeEach(() => { const dom = new JSDOM(); - // @ts-ignore need to override global document + // @ts-expect-error need to override global document global.document = dom.window.document; const options = getDefaultBrowserClientOptions({ tracesSampleRate: 1 }); @@ -22,7 +22,7 @@ conditionalTest({ min: 10 })('registerBackgroundTabDetection', () => { // eslint-disable-next-line deprecation/deprecation addExtensionMethods(); - // @ts-ignore need to override global document + // @ts-expect-error need to override global document global.document.addEventListener = jest.fn((event, callback) => { events[event] = callback; }); @@ -34,7 +34,7 @@ conditionalTest({ min: 10 })('registerBackgroundTabDetection', () => { }); it('does not creates an event listener if global document is undefined', () => { - // @ts-ignore need to override global document + // @ts-expect-error need to override global document global.document = undefined; registerBackgroundTabDetection(); expect(events).toMatchObject({}); @@ -51,7 +51,7 @@ conditionalTest({ min: 10 })('registerBackgroundTabDetection', () => { hub.configureScope(scope => scope.setSpan(transaction)); // Simulate document visibility hidden event - // @ts-ignore need to override global document + // @ts-expect-error need to override global document global.document.hidden = true; events.visibilitychange(); diff --git a/packages/tracing-internal/test/browser/browsertracing.test.ts b/packages/tracing-internal/test/browser/browsertracing.test.ts index e6a9eff3fb82..8a65f6cf8fbe 100644 --- a/packages/tracing-internal/test/browser/browsertracing.test.ts +++ b/packages/tracing-internal/test/browser/browsertracing.test.ts @@ -50,11 +50,10 @@ jest.mock('./../../src/browser/request', () => { beforeAll(() => { const dom = new JSDOM(); - // @ts-ignore need to override global document + // @ts-expect-error need to override global document WINDOW.document = dom.window.document; - // @ts-ignore need to override global document + // @ts-expect-error need to override global document WINDOW.window = dom.window; - // @ts-ignore need to override global document WINDOW.location = dom.window.location; }); @@ -307,7 +306,7 @@ conditionalTest({ min: 10 })('BrowserTracing', () => { ...options, }); - // @ts-ignore accessing private property + // @ts-expect-error accessing private property expect(inst._hasSetTracePropagationTargets).toBe(hasSet); }, ); diff --git a/packages/tracing-internal/test/browser/request.test.ts b/packages/tracing-internal/test/browser/request.test.ts index 17c3abfb05b5..2db0ec839372 100644 --- a/packages/tracing-internal/test/browser/request.test.ts +++ b/packages/tracing-internal/test/browser/request.test.ts @@ -18,7 +18,7 @@ import { TestClient } from '../utils/TestClient'; beforeAll(() => { addExtensionMethods(); - // @ts-ignore need to override global Request because it's not in the jest environment (even with an + // @ts-expect-error need to override global Request because it's not in the jest environment (even with an // `@jest-environment jsdom` directive, for some reason) global.Request = {}; }); diff --git a/packages/tracing-internal/test/browser/router.test.ts b/packages/tracing-internal/test/browser/router.test.ts index aa123154caa3..2b57ad4bd57f 100644 --- a/packages/tracing-internal/test/browser/router.test.ts +++ b/packages/tracing-internal/test/browser/router.test.ts @@ -22,11 +22,11 @@ conditionalTest({ min: 16 })('instrumentRoutingWithDefaults', () => { const customStartTransaction = jest.fn().mockReturnValue({ finish: mockFinish }); beforeEach(() => { const dom = new JSDOM(); - // @ts-ignore need to override global document + // @ts-expect-error need to override global document global.document = dom.window.document; - // @ts-ignore need to override global document + // @ts-expect-error need to override global document global.window = dom.window; - // @ts-ignore need to override global document + // @ts-expect-error need to override global document global.location = dom.window.location; customStartTransaction.mockClear(); @@ -34,7 +34,7 @@ conditionalTest({ min: 16 })('instrumentRoutingWithDefaults', () => { }); it('does not start transactions if global location is undefined', () => { - // @ts-ignore need to override global document + // @ts-expect-error need to override global document global.location = undefined; instrumentRoutingWithDefaults(customStartTransaction); expect(customStartTransaction).toHaveBeenCalledTimes(0); diff --git a/packages/tracing/test/idletransaction.test.ts b/packages/tracing/test/idletransaction.test.ts index 88a2fb4f6d1d..32be1c2e421a 100644 --- a/packages/tracing/test/idletransaction.test.ts +++ b/packages/tracing/test/idletransaction.test.ts @@ -87,7 +87,7 @@ describe('IdleTransaction', () => { ); transaction.initSpanRecorder(10); - // @ts-ignore need to pass in hub + // @ts-expect-error need to pass in hub const otherTransaction = new Transaction({ name: 'bar' }, hub); hub.getScope().setSpan(otherTransaction); diff --git a/packages/tracing/test/transaction.test.ts b/packages/tracing/test/transaction.test.ts index d07c52d34681..fed6a63ea684 100644 --- a/packages/tracing/test/transaction.test.ts +++ b/packages/tracing/test/transaction.test.ts @@ -71,7 +71,7 @@ describe('`Transaction` class', () => { key2: 'val2', }); - // @ts-ignore accessing private property + // @ts-expect-error accessing private property expect(transaction._contexts).toEqual({ foo: { key: 'val', @@ -90,7 +90,7 @@ describe('`Transaction` class', () => { key3: 'val3', }); - // @ts-ignore accessing private property + // @ts-expect-error accessing private property expect(transaction._contexts).toEqual({ foo: { key3: 'val3', @@ -108,7 +108,7 @@ describe('`Transaction` class', () => { anotherKey: 'anotherVal', }); - // @ts-ignore accessing private property + // @ts-expect-error accessing private property expect(transaction._contexts).toEqual({ foo: { key: 'val', @@ -128,7 +128,7 @@ describe('`Transaction` class', () => { }); transaction.setContext('foo', null); - // @ts-ignore accessing private property + // @ts-expect-error accessing private property expect(transaction._contexts).toEqual({}); }); diff --git a/packages/utils/src/env.ts b/packages/utils/src/env.ts index 95dfc698dabc..0a3308a88561 100644 --- a/packages/utils/src/env.ts +++ b/packages/utils/src/env.ts @@ -30,6 +30,6 @@ export function isBrowserBundle(): boolean { * Get source of SDK. */ export function getSDKSource(): SdkSource { - // @ts-ignore __SENTRY_SDK_SOURCE__ is injected by rollup during build process + // @ts-expect-error __SENTRY_SDK_SOURCE__ is injected by rollup during build process return __SENTRY_SDK_SOURCE__; } diff --git a/packages/utils/src/supports.ts b/packages/utils/src/supports.ts index b491a297349d..ebaa633acd7b 100644 --- a/packages/utils/src/supports.ts +++ b/packages/utils/src/supports.ts @@ -31,7 +31,7 @@ export function supportsDOMError(): boolean { try { // Chrome: VM89:1 Uncaught TypeError: Failed to construct 'DOMError': // 1 argument required, but only 0 present. - // @ts-ignore It really needs 1 argument, not 0. + // @ts-expect-error It really needs 1 argument, not 0. new DOMError(''); return true; } catch (e) { diff --git a/packages/utils/test/browser.test.ts b/packages/utils/test/browser.test.ts index a8c3798073cc..dd9941a04b12 100644 --- a/packages/utils/test/browser.test.ts +++ b/packages/utils/test/browser.test.ts @@ -4,7 +4,7 @@ import { getDomElement, htmlTreeAsString } from '../src/browser'; beforeAll(() => { const dom = new JSDOM(); - // @ts-ignore need to override global document + // @ts-expect-error need to override global document global.document = dom.window.document; }); diff --git a/packages/utils/test/is.test.ts b/packages/utils/test/is.test.ts index a8f9984e5c12..6a4172e4a595 100644 --- a/packages/utils/test/is.test.ts +++ b/packages/utils/test/is.test.ts @@ -25,7 +25,7 @@ if (supportsDOMError()) { describe('isDOMError()', () => { test('should work as advertised', () => { expect(isDOMError(new Error())).toEqual(false); - // @ts-ignore See: src/supports.ts for details + // @ts-expect-error See: src/supports.ts for details expect(isDOMError(new DOMError(''))).toEqual(true); }); }); @@ -105,7 +105,7 @@ describe('isInstanceOf()', () => { } expect(isInstanceOf(new Error('wat'), Error)).toEqual(true); expect(isInstanceOf(new Date(), Date)).toEqual(true); - // @ts-ignore Foo implicity has any type, doesn't have constructor + // @ts-expect-error Foo implicity has any type, doesn't have constructor expect(isInstanceOf(new Foo(), Foo)).toEqual(true); expect(isInstanceOf(new Error('wat'), Foo)).toEqual(false); diff --git a/packages/utils/test/normalize.test.ts b/packages/utils/test/normalize.test.ts index c1c90a7ec07d..d13631d43a9a 100644 --- a/packages/utils/test/normalize.test.ts +++ b/packages/utils/test/normalize.test.ts @@ -368,7 +368,7 @@ describe('normalize()', () => { B.prototype.toJSON = () => 2; const c: any = []; c.toJSON = () => 3; - // @ts-ignore target lacks a construct signature + // @ts-expect-error target lacks a construct signature expect(normalize([{ a }, { b: new B() }, c])).toEqual([{ a: 1 }, { b: 2 }, 3]); }); diff --git a/packages/utils/test/object.test.ts b/packages/utils/test/object.test.ts index 3e5d8eb03e36..3dad523ae4ce 100644 --- a/packages/utils/test/object.test.ts +++ b/packages/utils/test/object.test.ts @@ -67,7 +67,7 @@ describe('fill()', () => { foo: (): number => 42, } as any; const name = 'foo'; - // @ts-ignore cb has any type + // @ts-expect-error cb has any type const replacement = cb => cb; fill(source, name, replacement); @@ -85,7 +85,7 @@ describe('fill()', () => { const bar = {}; source.foo.prototype = bar; const name = 'foo'; - // @ts-ignore cb has any type + // @ts-expect-error cb has any type const replacement = cb => cb; fill(source, name, replacement); diff --git a/packages/utils/test/syncpromise.test.ts b/packages/utils/test/syncpromise.test.ts index b2b98542b6bd..7c896cbce360 100644 --- a/packages/utils/test/syncpromise.test.ts +++ b/packages/utils/test/syncpromise.test.ts @@ -83,10 +83,9 @@ describe('SyncPromise', () => { return ( c - // @ts-ignore Argument of type 'PromiseLike' is not assignable to parameter of type 'SyncPromise' + // @ts-expect-error Argument of type 'PromiseLike' is not assignable to parameter of type 'SyncPromise' .then(val => f(resolvedSyncPromise('x'), val)) .then(val => f(b, val)) - // @ts-ignore Argument of type 'SyncPromise' is not assignable to parameter of type 'string' .then(val => f(a, val)) .then(val => { expect(val).toBe(res); diff --git a/packages/utils/test/worldwide.test.ts b/packages/utils/test/worldwide.test.ts index 52203a248d69..3b85eb06fdd1 100644 --- a/packages/utils/test/worldwide.test.ts +++ b/packages/utils/test/worldwide.test.ts @@ -3,7 +3,7 @@ import { GLOBAL_OBJ } from '../src/worldwide'; describe('GLOBAL_OBJ', () => { test('should return the same object', () => { const backup = global.process; - // @ts-ignore for testing + // @ts-expect-error for testing delete global.process; const first = GLOBAL_OBJ; const second = GLOBAL_OBJ; diff --git a/packages/vue/test/errorHandler.test.ts b/packages/vue/test/errorHandler.test.ts index a85b368967fc..9098e7307f28 100644 --- a/packages/vue/test/errorHandler.test.ts +++ b/packages/vue/test/errorHandler.test.ts @@ -354,10 +354,10 @@ const testHarness = ({ if (enableConsole) { // I need to re-assign the whole console // because at some point it can be set to undefined - // @ts-ignore for the sake of testing + // @ts-expect-error for the sake of testing console = { error: consoleErrorSpy }; } else { - // @ts-ignore for the sake of testing + // @ts-expect-error for the sake of testing console = undefined; } /* eslint-enable no-global-assign */ From b19001557b77fda73b0f499f436b6980da402dcd Mon Sep 17 00:00:00 2001 From: Luca Forstner Date: Fri, 8 Sep 2023 14:34:11 +0100 Subject: [PATCH 12/35] fix(core): Always use event message and exception values for `ignoreErrors` (#8986) --- .../core/src/integrations/inboundfilters.ts | 35 +++++++++++++------ .../lib/integrations/inboundfilters.test.ts | 23 ++++++++++++ 2 files changed, 48 insertions(+), 10 deletions(-) diff --git a/packages/core/src/integrations/inboundfilters.ts b/packages/core/src/integrations/inboundfilters.ts index b00eda944ebe..0f5604884d1a 100644 --- a/packages/core/src/integrations/inboundfilters.ts +++ b/packages/core/src/integrations/inboundfilters.ts @@ -169,20 +169,35 @@ function _isAllowedUrl(event: Event, allowUrls?: Array): boolea } function _getPossibleEventMessages(event: Event): string[] { + const possibleMessages: string[] = []; + if (event.message) { - return [event.message]; + possibleMessages.push(event.message); } - if (event.exception) { - const { values } = event.exception; - try { - const { type = '', value = '' } = (values && values[values.length - 1]) || {}; - return [`${value}`, `${type}: ${value}`]; - } catch (oO) { - __DEBUG_BUILD__ && logger.error(`Cannot extract message for event ${getEventDescription(event)}`); - return []; + + let lastException; + try { + // @ts-ignore Try catching to save bundle size + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + lastException = event.exception.values[event.exception.values.length - 1]; + } catch (e) { + // try catching to save bundle size checking existence of variables + } + + if (lastException) { + if (lastException.value) { + possibleMessages.push(lastException.value); + if (lastException.type) { + possibleMessages.push(`${lastException.type}: ${lastException.value}`); + } } } - return []; + + if (__DEBUG_BUILD__ && possibleMessages.length === 0) { + logger.error(`Could not extract message for event ${getEventDescription(event)}`); + } + + return possibleMessages; } function _isSentryError(event: Event): boolean { diff --git a/packages/core/test/lib/integrations/inboundfilters.test.ts b/packages/core/test/lib/integrations/inboundfilters.test.ts index 88ab23f65909..9888a59eedff 100644 --- a/packages/core/test/lib/integrations/inboundfilters.test.ts +++ b/packages/core/test/lib/integrations/inboundfilters.test.ts @@ -125,6 +125,18 @@ const EXCEPTION_EVENT: Event = { }, }; +const EXCEPTION_EVENT_WITH_MESSAGE_AND_VALUE: Event = { + message: 'ChunkError', + exception: { + values: [ + { + type: 'SyntaxError', + value: 'unidentified ? at line 1337', + }, + ], + }, +}; + const EXCEPTION_EVENT_WITH_FRAMES: Event = { exception: { values: [ @@ -336,6 +348,17 @@ describe('InboundFilters', () => { }); expect(eventProcessor(EXCEPTION_EVENT, {})).toBe(null); }); + + it('should consider both `event.message` and the last exceptions `type` and `value`', () => { + const messageEventProcessor = createInboundFiltersEventProcessor({ + ignoreErrors: [/^ChunkError/], + }); + const valueEventProcessor = createInboundFiltersEventProcessor({ + ignoreErrors: [/^SyntaxError/], + }); + expect(messageEventProcessor(EXCEPTION_EVENT_WITH_MESSAGE_AND_VALUE, {})).toBe(null); + expect(valueEventProcessor(EXCEPTION_EVENT_WITH_MESSAGE_AND_VALUE, {})).toBe(null); + }); }); }); From 6f97b5fc9bbd866a211a324f9fd2758127e0b024 Mon Sep 17 00:00:00 2001 From: Abhijeet Prasad Date: Fri, 8 Sep 2023 10:14:24 -0400 Subject: [PATCH 13/35] ref: Cleanup lint warnings (#8972) This pollutes the console when running the linter, hopefully also speeds up CI a little bit. --- packages/nextjs/src/common/utils/wrapperUtils.ts | 1 + .../nextjs/src/common/wrapApiHandlerWithSentryVercelCrons.ts | 2 ++ packages/nextjs/src/config/loaders/wrappingLoader.ts | 3 +++ packages/nextjs/src/config/templates/pageWrapperTemplate.ts | 1 + packages/nextjs/src/index.types.ts | 1 + packages/nextjs/src/server/index.ts | 1 + packages/node/test/transports/http.test.ts | 2 +- packages/node/test/transports/https.test.ts | 2 +- packages/remix/src/utils/vendor/response.ts | 1 + 9 files changed, 12 insertions(+), 2 deletions(-) diff --git a/packages/nextjs/src/common/utils/wrapperUtils.ts b/packages/nextjs/src/common/utils/wrapperUtils.ts index a6d51ceacebb..9ae2cde39f83 100644 --- a/packages/nextjs/src/common/utils/wrapperUtils.ts +++ b/packages/nextjs/src/common/utils/wrapperUtils.ts @@ -188,6 +188,7 @@ export function withTracedServerSideDataFetcher Pr * We only do the following until we move transaction creation into this function: When called, the wrapped function * will also update the name of the active transaction with a parameterized route provided via the `options` argument. */ +// eslint-disable-next-line @typescript-eslint/no-explicit-any export async function callDataFetcherTraced Promise | any>( origFunction: F, origFunctionArgs: Parameters, diff --git a/packages/nextjs/src/common/wrapApiHandlerWithSentryVercelCrons.ts b/packages/nextjs/src/common/wrapApiHandlerWithSentryVercelCrons.ts index d324cc2de7c3..43672a1acf28 100644 --- a/packages/nextjs/src/common/wrapApiHandlerWithSentryVercelCrons.ts +++ b/packages/nextjs/src/common/wrapApiHandlerWithSentryVercelCrons.ts @@ -11,11 +11,13 @@ type EdgeRequest = { /** * Wraps a function with Sentry crons instrumentation by automaticaly sending check-ins for the given Vercel crons config. */ +// eslint-disable-next-line @typescript-eslint/no-explicit-any export function wrapApiHandlerWithSentryVercelCrons any>( handler: F, vercelCronsConfig: VercelCronsConfig, ): F { return new Proxy(handler, { + // eslint-disable-next-line @typescript-eslint/no-explicit-any apply: (originalFunction, thisArg, args: any[]) => { return runWithAsyncContext(() => { if (!args || !args[0]) { diff --git a/packages/nextjs/src/config/loaders/wrappingLoader.ts b/packages/nextjs/src/config/loaders/wrappingLoader.ts index fc3ced90f384..e6452f815184 100644 --- a/packages/nextjs/src/config/loaders/wrappingLoader.ts +++ b/packages/nextjs/src/config/loaders/wrappingLoader.ts @@ -72,6 +72,7 @@ function moduleExists(id: string): boolean { export default function wrappingLoader( this: LoaderThis, userCode: string, + // eslint-disable-next-line @typescript-eslint/no-explicit-any userModuleSourceMap: any, ): void { // We know one or the other will be defined, depending on the version of webpack being used @@ -276,7 +277,9 @@ export default function wrappingLoader( async function wrapUserCode( wrapperCode: string, userModuleCode: string, + // eslint-disable-next-line @typescript-eslint/no-explicit-any userModuleSourceMap: any, + // eslint-disable-next-line @typescript-eslint/no-explicit-any ): Promise<{ code: string; map?: any }> { const wrap = (withDefaultExport: boolean): Promise => rollup({ diff --git a/packages/nextjs/src/config/templates/pageWrapperTemplate.ts b/packages/nextjs/src/config/templates/pageWrapperTemplate.ts index 2e1b0d7b6537..16cce1a6cc39 100644 --- a/packages/nextjs/src/config/templates/pageWrapperTemplate.ts +++ b/packages/nextjs/src/config/templates/pageWrapperTemplate.ts @@ -27,6 +27,7 @@ const origGetInitialProps = pageComponent ? pageComponent.getInitialProps : unde const origGetStaticProps = userPageModule ? userPageModule.getStaticProps : undefined; const origGetServerSideProps = userPageModule ? userPageModule.getServerSideProps : undefined; +// eslint-disable-next-line @typescript-eslint/no-explicit-any const getInitialPropsWrappers: Record = { '/_app': Sentry.wrapAppGetInitialPropsWithSentry, '/_document': Sentry.wrapDocumentGetInitialPropsWithSentry, diff --git a/packages/nextjs/src/index.types.ts b/packages/nextjs/src/index.types.ts index 5fbcf683ec38..479760633b54 100644 --- a/packages/nextjs/src/index.types.ts +++ b/packages/nextjs/src/index.types.ts @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable import/export */ // We export everything from both the client part of the SDK and from the server part. Some of the exports collide, diff --git a/packages/nextjs/src/server/index.ts b/packages/nextjs/src/server/index.ts index 0e43291d019d..23f3bc61e4a3 100644 --- a/packages/nextjs/src/server/index.ts +++ b/packages/nextjs/src/server/index.ts @@ -36,6 +36,7 @@ export const ErrorBoundary = (props: React.PropsWithChildren): React.Re * A passthrough error boundary wrapper for the server that doesn't depend on any react. Error boundaries don't catch * SSR errors so they should simply be a passthrough. */ +// eslint-disable-next-line @typescript-eslint/no-explicit-any export function withErrorBoundary

>( WrappedComponent: React.ComponentType

, ): React.FC

{ diff --git a/packages/node/test/transports/http.test.ts b/packages/node/test/transports/http.test.ts index 22d091a8583d..219d0c3bde77 100644 --- a/packages/node/test/transports/http.test.ts +++ b/packages/node/test/transports/http.test.ts @@ -212,7 +212,7 @@ describe('makeNewHttpTransport()', () => { describe('proxy', () => { const proxyAgentSpy = jest .spyOn(httpProxyAgent, 'HttpsProxyAgent') - // @ts-expect-error + // @ts-expect-error using http agent as https proxy agent .mockImplementation(() => new http.Agent({ keepAlive: false, maxSockets: 30, timeout: 2000 })); it('can be configured through option', () => { diff --git a/packages/node/test/transports/https.test.ts b/packages/node/test/transports/https.test.ts index b4ae670b7542..485b582d8e92 100644 --- a/packages/node/test/transports/https.test.ts +++ b/packages/node/test/transports/https.test.ts @@ -183,7 +183,7 @@ describe('makeNewHttpsTransport()', () => { describe('proxy', () => { const proxyAgentSpy = jest .spyOn(httpProxyAgent, 'HttpsProxyAgent') - // @ts-expect-error + // @ts-expect-error using http agent as https proxy agent .mockImplementation(() => new http.Agent({ keepAlive: false, maxSockets: 30, timeout: 2000 })); it('can be configured through option', () => { diff --git a/packages/remix/src/utils/vendor/response.ts b/packages/remix/src/utils/vendor/response.ts index fed25dd0f534..fbcffedf0fdd 100644 --- a/packages/remix/src/utils/vendor/response.ts +++ b/packages/remix/src/utils/vendor/response.ts @@ -128,6 +128,7 @@ export function getRequestMatch(url: URL, matches: RouteMatch[]): R /** * https://github.com/remix-run/remix/blob/3e589152bc717d04e2054c31bea5a1056080d4b9/packages/remix-server-runtime/responses.ts#L75-L85 */ +// eslint-disable-next-line @typescript-eslint/no-explicit-any export function isDeferredData(value: any): value is DeferredData { const deferred: DeferredData = value; return ( From e7417e032cd1b3d55d1b5c38c33b77019be2a742 Mon Sep 17 00:00:00 2001 From: Abhijeet Prasad Date: Fri, 8 Sep 2023 11:18:11 -0400 Subject: [PATCH 14/35] chore: Update migration docs to indicate sentry-migr8 is experimental. (#8988) There was some confusion about this because the migration doc in this repo conflicts with the `highly experimental` tag we put on the README. Also added Node 18+ requirements. We can change this in migr8 for a future release, but for now let's merge this in to reduce confusion and unblock our users. --- MIGRATION.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MIGRATION.md b/MIGRATION.md index 5aec615751c7..6dc1886f7174 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -1,6 +1,6 @@ # Deprecations in 7.x -You can use [@sentry/migr8](https://www.npmjs.com/package/@sentry/migr8) to automatically update your SDK usage and fix most deprecations: +You can use the **Experimental** [@sentry/migr8](https://www.npmjs.com/package/@sentry/migr8) to automatically update your SDK usage and fix most deprecations. This requires Node 18+. ```bash npx @sentry/migr8@latest From d3c3462c690a46ce667a94c7d28868ce65b0213f Mon Sep 17 00:00:00 2001 From: Abhijeet Prasad Date: Mon, 11 Sep 2023 03:21:56 -0400 Subject: [PATCH 15/35] feat(eslint): Enforce that ts-expect-error is used (#8987) Enforce that `@ts-expect-error` is used instead of `@ts-ignore`. This is done by updating the `typescript-eslint/ban-ts-comment` rule. https://typescript-eslint.io/rules/ban-ts-comment/ --- packages/core/src/integrations/inboundfilters.ts | 2 +- packages/eslint-config-sdk/src/index.js | 10 +++------- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/packages/core/src/integrations/inboundfilters.ts b/packages/core/src/integrations/inboundfilters.ts index 0f5604884d1a..37ea8bac06e9 100644 --- a/packages/core/src/integrations/inboundfilters.ts +++ b/packages/core/src/integrations/inboundfilters.ts @@ -177,7 +177,7 @@ function _getPossibleEventMessages(event: Event): string[] { let lastException; try { - // @ts-ignore Try catching to save bundle size + // @ts-expect-error Try catching to save bundle size // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access lastException = event.exception.values[event.exception.values.length - 1]; } catch (e) { diff --git a/packages/eslint-config-sdk/src/index.js b/packages/eslint-config-sdk/src/index.js index 2d60f44f05f7..e9d72743f99a 100644 --- a/packages/eslint-config-sdk/src/index.js +++ b/packages/eslint-config-sdk/src/index.js @@ -26,13 +26,9 @@ module.exports = { // Unused variables should be removed unless they are marked with and underscore (ex. _varName). '@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }], - // Make sure that all ts-ignore comments are given a description. - '@typescript-eslint/ban-ts-comment': [ - 'warn', - { - 'ts-ignore': 'allow-with-description', - }, - ], + // Do not use ts-ignore, use ts-expect-error instead. + // Also make sure that all ts-expect-error comments are given a description. + '@typescript-eslint/ban-ts-comment': 'error', // Types usage should be explicit as possible, so we prevent usage of inferrable types. // This is especially important because we have a public API, so usage needs to be as From 2ab1f50fda83de29678d8bfb32e506dcaf22afa0 Mon Sep 17 00:00:00 2001 From: Abhijeet Prasad Date: Mon, 11 Sep 2023 03:23:31 -0400 Subject: [PATCH 16/35] fix(overhead-metrics): Remove unneeded @ts-expect-error (#8990) Should fix failing CI on develop rn. --- packages/overhead-metrics/src/perf/sampler.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/overhead-metrics/src/perf/sampler.ts b/packages/overhead-metrics/src/perf/sampler.ts index 9ba693c5cc91..1c5c631f231a 100644 --- a/packages/overhead-metrics/src/perf/sampler.ts +++ b/packages/overhead-metrics/src/perf/sampler.ts @@ -23,7 +23,6 @@ export class TimeBasedMap extends Map { * */ public toJSON(): JsonObject { - // @ts-expect-error this actually exists return Object.fromEntries(this.entries()); } } From 434507dc0995f0e7264fdcfd3adc3259c9651dcb Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Mon, 11 Sep 2023 09:36:25 +0200 Subject: [PATCH 17/35] fix(node-experimental): Ensure we only create HTTP spans when outgoing (#8966) This is a fork of https://github.com/getsentry/sentry-javascript/pull/8937, with only the "uncontroversial" stuff, mainly fixing that we only create HTTP breadcrumbs for _outgoing_ requests. In addition, this also migrates to using `requestHook` and `responseHook` instead of `applyCustomAttributesOnSpan`. We may have to revisit this later, but these hooks seem to have a better context awareness (=they are called in a more reasonable OTEL context, which gives the callbacks there better access to scope data etc). However that means we cannot (easily) pass both request and response as breadcrumb hints - not sure how important that is to us... For now I'd say that's OK. Note that also `requestHook` is only called when the request finishes, so we already have all the response OTEL span attributes correctly set there. --- .../src/integrations/http.ts | 105 ++++++++++-------- .../src/utils/getRequestSpanData.ts | 7 +- 2 files changed, 58 insertions(+), 54 deletions(-) diff --git a/packages/node-experimental/src/integrations/http.ts b/packages/node-experimental/src/integrations/http.ts index 5f45334da290..d6e6a825d8a1 100644 --- a/packages/node-experimental/src/integrations/http.ts +++ b/packages/node-experimental/src/integrations/http.ts @@ -95,8 +95,11 @@ export class Http implements Integration { new HttpInstrumentation({ requireParentforOutgoingSpans: true, requireParentforIncomingSpans: false, - applyCustomAttributesOnSpan: (span, req, res) => { - this._onSpan(span as unknown as OtelSpan, req, res); + requestHook: (span, req) => { + this._updateSentrySpan(span as unknown as OtelSpan, req); + }, + responseHook: (span, res) => { + this._addRequestBreadcrumb(span as unknown as OtelSpan, res); }, }), ], @@ -136,64 +139,70 @@ export class Http implements Integration { return; }; - /** Handle an emitted span from the HTTP instrumentation. */ - private _onSpan( - span: OtelSpan, - request: ClientRequest | IncomingMessage, - response: IncomingMessage | ServerResponse, - ): void { - const data = getRequestSpanData(span, request, response); + /** Update the Sentry span data based on the OTEL span. */ + private _updateSentrySpan(span: OtelSpan, request: ClientRequest | IncomingMessage): void { + const data = getRequestSpanData(span); const { attributes } = span; const sentrySpan = _INTERNAL_getSentrySpan(span.spanContext().spanId); - if (sentrySpan) { - sentrySpan.origin = 'auto.http.otel.http'; + if (!sentrySpan) { + return; + } - const additionalData: Record = { - url: data.url, - }; + sentrySpan.origin = 'auto.http.otel.http'; - if (sentrySpan instanceof Transaction && span.kind === SpanKind.SERVER) { - sentrySpan.setMetadata({ request }); - } + const additionalData: Record = { + url: data.url, + }; - if (attributes[SemanticAttributes.HTTP_STATUS_CODE]) { - const statusCode = attributes[SemanticAttributes.HTTP_STATUS_CODE] as string; - additionalData['http.response.status_code'] = statusCode; + if (sentrySpan instanceof Transaction && span.kind === SpanKind.SERVER) { + sentrySpan.setMetadata({ request }); + } - sentrySpan.setTag('http.status_code', statusCode); - } + if (attributes[SemanticAttributes.HTTP_STATUS_CODE]) { + const statusCode = attributes[SemanticAttributes.HTTP_STATUS_CODE] as string; + additionalData['http.response.status_code'] = statusCode; - if (data['http.query']) { - additionalData['http.query'] = data['http.query'].slice(1); - } - if (data['http.fragment']) { - additionalData['http.fragment'] = data['http.fragment'].slice(1); - } + sentrySpan.setTag('http.status_code', statusCode); + } - Object.keys(additionalData).forEach(prop => { - const value = additionalData[prop]; - sentrySpan.setData(prop, value); - }); + if (data['http.query']) { + additionalData['http.query'] = data['http.query'].slice(1); + } + if (data['http.fragment']) { + additionalData['http.fragment'] = data['http.fragment'].slice(1); } - if (this._breadcrumbs) { - getCurrentHub().addBreadcrumb( - { - category: 'http', - data: { - status_code: response.statusCode, - ...data, - }, - type: 'http', - }, - { - event: 'response', - request, - response, - }, - ); + Object.keys(additionalData).forEach(prop => { + const value = additionalData[prop]; + sentrySpan.setData(prop, value); + }); + } + + /** Add a breadcrumb for outgoing requests. */ + private _addRequestBreadcrumb(span: OtelSpan, response: IncomingMessage | ServerResponse): void { + if (!this._breadcrumbs || span.kind !== SpanKind.CLIENT) { + return; } + + const data = getRequestSpanData(span); + getCurrentHub().addBreadcrumb( + { + category: 'http', + data: { + status_code: response.statusCode, + ...data, + }, + type: 'http', + }, + { + event: 'response', + // TODO FN: Do we need access to `request` here? + // If we do, we'll have to use the `applyCustomAttributesOnSpan` hook instead, + // but this has worse context semantics than request/responseHook. + response, + }, + ); } } diff --git a/packages/node-experimental/src/utils/getRequestSpanData.ts b/packages/node-experimental/src/utils/getRequestSpanData.ts index 8d5d45f5f907..d238077bd9df 100644 --- a/packages/node-experimental/src/utils/getRequestSpanData.ts +++ b/packages/node-experimental/src/utils/getRequestSpanData.ts @@ -2,16 +2,11 @@ import type { Span as OtelSpan } from '@opentelemetry/sdk-trace-base'; import { SemanticAttributes } from '@opentelemetry/semantic-conventions'; import type { SanitizedRequestData } from '@sentry/types'; import { getSanitizedUrlString, parseUrl } from '@sentry/utils'; -import type { ClientRequest, IncomingMessage, ServerResponse } from 'http'; /** * Get sanitizied request data from an OTEL span. */ -export function getRequestSpanData( - span: OtelSpan, - _request: ClientRequest | IncomingMessage, - _response: IncomingMessage | ServerResponse, -): SanitizedRequestData { +export function getRequestSpanData(span: OtelSpan): SanitizedRequestData { const data: SanitizedRequestData = { url: span.attributes[SemanticAttributes.HTTP_URL] as string, 'http.method': (span.attributes[SemanticAttributes.HTTP_METHOD] as string) || 'GET', From 30c4540bd420705a1c9ccac58a92edb03155fbae Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Mon, 11 Sep 2023 10:56:09 +0200 Subject: [PATCH 18/35] fix(replay): Fully stop & restart session when it expires (#8834) This PR changes the behavior when a session is expired to fully stop & restart the replay. This means we just re-sample based on sample rates and start a completely new session in that case. --- packages/replay/src/replay.ts | 118 +- packages/replay/src/session/Session.ts | 2 - .../replay/src/session/loadOrCreateSession.ts | 19 +- .../replay/src/session/maybeRefreshSession.ts | 50 - .../src/session/shouldRefreshSession.ts | 20 + packages/replay/src/types/replay.ts | 6 - .../test/integration/errorSampleRate.test.ts | 1546 ++++++++--------- .../replay/test/integration/session.test.ts | 38 +- .../test/unit/session/fetchSession.test.ts | 2 - .../unit/session/loadOrCreateSession.test.ts | 161 +- .../unit/session/maybeRefreshSession.test.ts | 267 --- 11 files changed, 938 insertions(+), 1291 deletions(-) delete mode 100644 packages/replay/src/session/maybeRefreshSession.ts create mode 100644 packages/replay/src/session/shouldRefreshSession.ts delete mode 100644 packages/replay/test/unit/session/maybeRefreshSession.test.ts diff --git a/packages/replay/src/replay.ts b/packages/replay/src/replay.ts index 72a9dd88d7aa..aa500382cbc3 100644 --- a/packages/replay/src/replay.ts +++ b/packages/replay/src/replay.ts @@ -18,8 +18,8 @@ import { setupPerformanceObserver } from './coreHandlers/performanceObserver'; import { createEventBuffer } from './eventBuffer'; import { clearSession } from './session/clearSession'; import { loadOrCreateSession } from './session/loadOrCreateSession'; -import { maybeRefreshSession } from './session/maybeRefreshSession'; import { saveSession } from './session/saveSession'; +import { shouldRefreshSession } from './session/shouldRefreshSession'; import type { AddEventResult, AddUpdateCallback, @@ -217,7 +217,7 @@ export class ReplayContainer implements ReplayContainerInterface { * Initializes the plugin based on sampling configuration. Should not be * called outside of constructor. */ - public initializeSampling(): void { + public initializeSampling(previousSessionId?: string): void { const { errorSampleRate, sessionSampleRate } = this._options; // If neither sample rate is > 0, then do nothing - user will need to call one of @@ -228,7 +228,7 @@ export class ReplayContainer implements ReplayContainerInterface { // Otherwise if there is _any_ sample rate set, try to load an existing // session, or create a new one. - this._initializeSessionForSampling(); + this._initializeSessionForSampling(previousSessionId); if (!this.session) { // This should not happen, something wrong has occurred @@ -273,7 +273,6 @@ export class ReplayContainer implements ReplayContainerInterface { logInfoNextTick('[Replay] Starting replay in session mode', this._options._experiments.traceInternals); const session = loadOrCreateSession( - this.session, { maxReplayDuration: this._options.maxReplayDuration, sessionIdleExpire: this.timeouts.sessionIdleExpire, @@ -304,7 +303,6 @@ export class ReplayContainer implements ReplayContainerInterface { logInfoNextTick('[Replay] Starting replay in buffer mode', this._options._experiments.traceInternals); const session = loadOrCreateSession( - this.session, { sessionIdleExpire: this.timeouts.sessionIdleExpire, maxReplayDuration: this._options.maxReplayDuration, @@ -373,15 +371,16 @@ export class ReplayContainer implements ReplayContainerInterface { return; } + // We can't move `_isEnabled` after awaiting a flush, otherwise we can + // enter into an infinite loop when `stop()` is called while flushing. + this._isEnabled = false; + try { logInfo( `[Replay] Stopping Replay${reason ? ` triggered by ${reason}` : ''}`, this._options._experiments.traceInternals, ); - // We can't move `_isEnabled` after awaiting a flush, otherwise we can - // enter into an infinite loop when `stop()` is called while flushing. - this._isEnabled = false; this._removeListeners(); this.stopRecording(); @@ -475,16 +474,6 @@ export class ReplayContainer implements ReplayContainerInterface { // Once this session ends, we do not want to refresh it if (this.session) { - this.session.shouldRefresh = false; - - // It's possible that the session lifespan is > max session lifespan - // because we have been buffering beyond max session lifespan (we ignore - // expiration given that `shouldRefresh` is true). Since we flip - // `shouldRefresh`, the session could be considered expired due to - // lifespan, which is not what we want. Update session start date to be - // the current timestamp, so that session is not considered to be - // expired. This means that max replay duration can be MAX_REPLAY_DURATION + - // (length of buffer), which we are ok with. this._updateUserActivity(activityTime); this._updateSessionActivity(activityTime); this._maybeSaveSession(); @@ -612,8 +601,6 @@ export class ReplayContainer implements ReplayContainerInterface { * @hidden */ public checkAndHandleExpiredSession(): boolean | void { - const oldSessionId = this.getSessionId(); - // Prevent starting a new session if the last user activity is older than // SESSION_IDLE_PAUSE_DURATION. Otherwise non-user activity can trigger a new // session+recording. This creates noisy replays that do not have much @@ -635,24 +622,11 @@ export class ReplayContainer implements ReplayContainerInterface { // --- There is recent user activity --- // // This will create a new session if expired, based on expiry length if (!this._checkSession()) { - return; - } - - // Session was expired if session ids do not match - const expired = oldSessionId !== this.getSessionId(); - - if (!expired) { - return true; - } - - // Session is expired, trigger a full snapshot (which will create a new session) - if (this.isPaused()) { - this.resume(); - } else { - this._triggerFullSnapshot(); + // Check session handles the refreshing itself + return false; } - return false; + return true; } /** @@ -740,6 +714,7 @@ export class ReplayContainer implements ReplayContainerInterface { // Need to set as enabled before we start recording, as `record()` can trigger a flush with a new checkout this._isEnabled = true; + this._isPaused = false; this.startRecording(); } @@ -756,17 +731,17 @@ export class ReplayContainer implements ReplayContainerInterface { /** * Loads (or refreshes) the current session. */ - private _initializeSessionForSampling(): void { + private _initializeSessionForSampling(previousSessionId?: string): void { // Whenever there is _any_ error sample rate, we always allow buffering // Because we decide on sampling when an error occurs, we need to buffer at all times if sampling for errors const allowBuffering = this._options.errorSampleRate > 0; const session = loadOrCreateSession( - this.session, { sessionIdleExpire: this.timeouts.sessionIdleExpire, maxReplayDuration: this._options.maxReplayDuration, traceInternals: this._options._experiments.traceInternals, + previousSessionId, }, { stickySession: this._options.stickySession, @@ -791,37 +766,32 @@ export class ReplayContainer implements ReplayContainerInterface { const currentSession = this.session; - const newSession = maybeRefreshSession( - currentSession, - { + if ( + shouldRefreshSession(currentSession, { sessionIdleExpire: this.timeouts.sessionIdleExpire, - traceInternals: this._options._experiments.traceInternals, maxReplayDuration: this._options.maxReplayDuration, - }, - { - stickySession: Boolean(this._options.stickySession), - sessionSampleRate: this._options.sessionSampleRate, - allowBuffering: this._options.errorSampleRate > 0, - }, - ); - - const isNew = newSession.id !== currentSession.id; - - // If session was newly created (i.e. was not loaded from storage), then - // enable flag to create the root replay - if (isNew) { - this.setInitialState(); - this.session = newSession; - } - - if (!this.session.sampled) { - void this.stop({ reason: 'session not refreshed' }); + }) + ) { + void this._refreshSession(currentSession); return false; } return true; } + /** + * Refresh a session with a new one. + * This stops the current session (without forcing a flush, as that would never work since we are expired), + * and then does a new sampling based on the refreshed session. + */ + private async _refreshSession(session: Session): Promise { + if (!this._isEnabled) { + return; + } + await this.stop({ reason: 'refresh session' }); + this.initializeSampling(session.id); + } + /** * Adds listeners to record events for the replay */ @@ -933,10 +903,14 @@ export class ReplayContainer implements ReplayContainerInterface { const expired = isSessionExpired(this.session, { maxReplayDuration: this._options.maxReplayDuration, - ...this.timeouts, + sessionIdleExpire: this.timeouts.sessionIdleExpire, }); - if (breadcrumb && !expired) { + if (expired) { + return; + } + + if (breadcrumb) { this._createCustomBreadcrumb(breadcrumb); } @@ -1081,7 +1055,9 @@ export class ReplayContainer implements ReplayContainerInterface { * Should never be called directly, only by `flush` */ private async _runFlush(): Promise { - if (!this.session || !this.eventBuffer) { + const replayId = this.getSessionId(); + + if (!this.session || !this.eventBuffer || !replayId) { __DEBUG_BUILD__ && logger.error('[Replay] No session or eventBuffer found to flush.'); return; } @@ -1101,13 +1077,15 @@ export class ReplayContainer implements ReplayContainerInterface { return; } + // if this changed in the meanwhile, e.g. because the session was refreshed or similar, we abort here + if (replayId !== this.getSessionId()) { + return; + } + try { // This uses the data from the eventBuffer, so we need to call this before `finish() this._updateInitialTimestampFromEventBuffer(); - // Note this empties the event buffer regardless of outcome of sending replay - const recordingData = await this.eventBuffer.finish(); - const timestamp = Date.now(); // Check total duration again, to avoid sending outdated stuff @@ -1117,14 +1095,14 @@ export class ReplayContainer implements ReplayContainerInterface { throw new Error('Session is too long, not sending replay'); } - // NOTE: Copy values from instance members, as it's possible they could - // change before the flush finishes. - const replayId = this.session.id; const eventContext = this._popEventContext(); // Always increment segmentId regardless of outcome of sending replay const segmentId = this.session.segmentId++; this._maybeSaveSession(); + // Note this empties the event buffer regardless of outcome of sending replay + const recordingData = await this.eventBuffer.finish(); + await sendReplay({ replayId, recordingData, diff --git a/packages/replay/src/session/Session.ts b/packages/replay/src/session/Session.ts index 80b32aed345a..be2bcdac1506 100644 --- a/packages/replay/src/session/Session.ts +++ b/packages/replay/src/session/Session.ts @@ -13,7 +13,6 @@ export function makeSession(session: Partial & { sampled: Sampled }): S const lastActivity = session.lastActivity || now; const segmentId = session.segmentId || 0; const sampled = session.sampled; - const shouldRefresh = typeof session.shouldRefresh === 'boolean' ? session.shouldRefresh : true; const previousSessionId = session.previousSessionId; return { @@ -22,7 +21,6 @@ export function makeSession(session: Partial & { sampled: Sampled }): S lastActivity, segmentId, sampled, - shouldRefresh, previousSessionId, }; } diff --git a/packages/replay/src/session/loadOrCreateSession.ts b/packages/replay/src/session/loadOrCreateSession.ts index 0766c537a4d2..1e1ac7664d40 100644 --- a/packages/replay/src/session/loadOrCreateSession.ts +++ b/packages/replay/src/session/loadOrCreateSession.ts @@ -2,33 +2,38 @@ import type { Session, SessionOptions } from '../types'; import { logInfoNextTick } from '../util/log'; import { createSession } from './createSession'; import { fetchSession } from './fetchSession'; -import { maybeRefreshSession } from './maybeRefreshSession'; +import { shouldRefreshSession } from './shouldRefreshSession'; /** * Get or create a session, when initializing the replay. * Returns a session that may be unsampled. */ export function loadOrCreateSession( - currentSession: Session | undefined, { traceInternals, sessionIdleExpire, maxReplayDuration, + previousSessionId, }: { sessionIdleExpire: number; maxReplayDuration: number; traceInternals?: boolean; + previousSessionId?: string; }, sessionOptions: SessionOptions, ): Session { - // If session exists and is passed, use it instead of always hitting session storage - const existingSession = currentSession || (sessionOptions.stickySession && fetchSession(traceInternals)); + const existingSession = sessionOptions.stickySession && fetchSession(traceInternals); // No session exists yet, just create a new one if (!existingSession) { - logInfoNextTick('[Replay] Created new session', traceInternals); - return createSession(sessionOptions); + logInfoNextTick('[Replay] Creating new session', traceInternals); + return createSession(sessionOptions, { previousSessionId }); } - return maybeRefreshSession(existingSession, { sessionIdleExpire, traceInternals, maxReplayDuration }, sessionOptions); + if (!shouldRefreshSession(existingSession, { sessionIdleExpire, maxReplayDuration })) { + return existingSession; + } + + logInfoNextTick('[Replay] Session in sessionStorage is expired, creating new one...'); + return createSession(sessionOptions, { previousSessionId: existingSession.id }); } diff --git a/packages/replay/src/session/maybeRefreshSession.ts b/packages/replay/src/session/maybeRefreshSession.ts deleted file mode 100644 index 14bc7f9534fa..000000000000 --- a/packages/replay/src/session/maybeRefreshSession.ts +++ /dev/null @@ -1,50 +0,0 @@ -import type { Session, SessionOptions } from '../types'; -import { isSessionExpired } from '../util/isSessionExpired'; -import { logInfoNextTick } from '../util/log'; -import { createSession } from './createSession'; -import { makeSession } from './Session'; - -/** - * Check a session, and either return it or a refreshed version of it. - * The refreshed version may be unsampled. - * You can check if the session has changed by comparing the session IDs. - */ -export function maybeRefreshSession( - session: Session, - { - traceInternals, - maxReplayDuration, - sessionIdleExpire, - }: { - sessionIdleExpire: number; - maxReplayDuration: number; - traceInternals?: boolean; - }, - sessionOptions: SessionOptions, -): Session { - // If not expired, all good, just keep the session - if (!isSessionExpired(session, { sessionIdleExpire, maxReplayDuration })) { - return session; - } - - const isBuffering = session.sampled === 'buffer'; - - // If we are buffering & the session may be refreshed, just return it - if (isBuffering && session.shouldRefresh) { - return session; - } - - // If we are buffering & the session may not be refreshed (=it was converted to session previously already) - // We return an unsampled new session - if (isBuffering) { - logInfoNextTick('[Replay] Session should not be refreshed', traceInternals); - return makeSession({ sampled: false }); - } - - // Else, we are not buffering, and the session is expired, so we need to create a new one - logInfoNextTick('[Replay] Session has expired, creating new one...', traceInternals); - - const newSession = createSession(sessionOptions, { previousSessionId: session.id }); - - return newSession; -} diff --git a/packages/replay/src/session/shouldRefreshSession.ts b/packages/replay/src/session/shouldRefreshSession.ts new file mode 100644 index 000000000000..0b37574cd3db --- /dev/null +++ b/packages/replay/src/session/shouldRefreshSession.ts @@ -0,0 +1,20 @@ +import type { Session } from '../types'; +import { isSessionExpired } from '../util/isSessionExpired'; + +/** If the session should be refreshed or not. */ +export function shouldRefreshSession( + session: Session, + { sessionIdleExpire, maxReplayDuration }: { sessionIdleExpire: number; maxReplayDuration: number }, +): boolean { + // If not expired, all good, just keep the session + if (!isSessionExpired(session, { sessionIdleExpire, maxReplayDuration })) { + return false; + } + + // If we are buffering & haven't ever flushed yet, always continue + if (session.sampled === 'buffer' && session.segmentId === 0) { + return false; + } + + return true; +} diff --git a/packages/replay/src/types/replay.ts b/packages/replay/src/types/replay.ts index b5c2318008a0..8ad157606c53 100644 --- a/packages/replay/src/types/replay.ts +++ b/packages/replay/src/types/replay.ts @@ -373,12 +373,6 @@ export interface Session { * Is the session sampled? `false` if not sampled, otherwise, `session` or `buffer` */ sampled: Sampled; - - /** - * If this is false, the session should not be refreshed when it was inactive. - * This can be the case if you had a buffered session which is now recording because an error happened. - */ - shouldRefresh: boolean; } export type EventBufferType = 'sync' | 'worker'; diff --git a/packages/replay/test/integration/errorSampleRate.test.ts b/packages/replay/test/integration/errorSampleRate.test.ts index a2dc7d9db2f4..ad3543ec810a 100644 --- a/packages/replay/test/integration/errorSampleRate.test.ts +++ b/packages/replay/test/integration/errorSampleRate.test.ts @@ -37,405 +37,400 @@ async function waitForFlush() { } describe('Integration | errorSampleRate', () => { - let replay: ReplayContainer; - let mockRecord: RecordMock; - let domHandler: DomHandler; - - beforeEach(async () => { - ({ mockRecord, domHandler, replay } = await resetSdkMock({ - replayOptions: { - stickySession: true, - }, - sentryOptions: { - replaysSessionSampleRate: 0.0, - replaysOnErrorSampleRate: 1.0, - }, - })); - }); + describe('basic', () => { + let replay: ReplayContainer; + let mockRecord: RecordMock; + let domHandler: DomHandler; + + beforeEach(async () => { + ({ mockRecord, domHandler, replay } = await resetSdkMock({ + replayOptions: { + stickySession: true, + }, + sentryOptions: { + replaysSessionSampleRate: 0.0, + replaysOnErrorSampleRate: 1.0, + }, + })); + }); - afterEach(async () => { - clearSession(replay); - replay.stop(); - }); + afterEach(async () => { + clearSession(replay); + replay.stop(); + }); - it('uploads a replay when `Sentry.captureException` is called and continues recording', async () => { - const TEST_EVENT = getTestEventIncremental({ timestamp: BASE_TIMESTAMP }); - mockRecord._emitter(TEST_EVENT); - const optionsEvent = createOptionsEvent(replay); + it('uploads a replay when `Sentry.captureException` is called and continues recording', async () => { + const TEST_EVENT = getTestEventIncremental({ timestamp: BASE_TIMESTAMP }); + mockRecord._emitter(TEST_EVENT); + const optionsEvent = createOptionsEvent(replay); - expect(mockRecord.takeFullSnapshot).not.toHaveBeenCalled(); - expect(replay).not.toHaveLastSentReplay(); + expect(mockRecord.takeFullSnapshot).not.toHaveBeenCalled(); + expect(replay).not.toHaveLastSentReplay(); - // Does not capture on mouse click - domHandler({ - name: 'click', - }); - jest.runAllTimers(); - await new Promise(process.nextTick); - expect(replay).not.toHaveLastSentReplay(); + // Does not capture on mouse click + domHandler({ + name: 'click', + }); + jest.runAllTimers(); + await new Promise(process.nextTick); + expect(replay).not.toHaveLastSentReplay(); - captureException(new Error('testing')); + captureException(new Error('testing')); - await waitForBufferFlush(); + await waitForBufferFlush(); - expect(replay).toHaveLastSentReplay({ - recordingPayloadHeader: { segment_id: 0 }, - replayEventPayload: expect.objectContaining({ - replay_type: 'buffer', - }), - recordingData: JSON.stringify([ - { data: { isCheckout: true }, timestamp: BASE_TIMESTAMP, type: 2 }, - optionsEvent, - TEST_EVENT, - { - type: 5, - timestamp: BASE_TIMESTAMP, - data: { - tag: 'breadcrumb', - payload: { - timestamp: BASE_TIMESTAMP / 1000, - type: 'default', - category: 'ui.click', - message: '', - data: {}, + expect(replay).toHaveLastSentReplay({ + recordingPayloadHeader: { segment_id: 0 }, + replayEventPayload: expect.objectContaining({ + replay_type: 'buffer', + }), + recordingData: JSON.stringify([ + { data: { isCheckout: true }, timestamp: BASE_TIMESTAMP, type: 2 }, + optionsEvent, + TEST_EVENT, + { + type: 5, + timestamp: BASE_TIMESTAMP, + data: { + tag: 'breadcrumb', + payload: { + timestamp: BASE_TIMESTAMP / 1000, + type: 'default', + category: 'ui.click', + message: '', + data: {}, + }, }, }, - }, - ]), - }); + ]), + }); - await waitForFlush(); + await waitForFlush(); - // This is from when we stop recording and start a session recording - expect(replay).toHaveLastSentReplay({ - recordingPayloadHeader: { segment_id: 1 }, - replayEventPayload: expect.objectContaining({ - replay_type: 'buffer', - }), - recordingData: JSON.stringify([{ data: { isCheckout: true }, timestamp: BASE_TIMESTAMP + 40, type: 2 }]), - }); + // This is from when we stop recording and start a session recording + expect(replay).toHaveLastSentReplay({ + recordingPayloadHeader: { segment_id: 1 }, + replayEventPayload: expect.objectContaining({ + replay_type: 'buffer', + }), + recordingData: JSON.stringify([{ data: { isCheckout: true }, timestamp: BASE_TIMESTAMP + 40, type: 2 }]), + }); - jest.advanceTimersByTime(DEFAULT_FLUSH_MIN_DELAY); + jest.advanceTimersByTime(DEFAULT_FLUSH_MIN_DELAY); - // Check that click will get captured - domHandler({ - name: 'click', - }); + // Check that click will get captured + domHandler({ + name: 'click', + }); - await waitForFlush(); + await waitForFlush(); - expect(replay).toHaveLastSentReplay({ - recordingData: JSON.stringify([ - { - type: 5, - timestamp: BASE_TIMESTAMP + 10000 + 80, - data: { - tag: 'breadcrumb', - payload: { - timestamp: (BASE_TIMESTAMP + 10000 + 80) / 1000, - type: 'default', - category: 'ui.click', - message: '', - data: {}, + expect(replay).toHaveLastSentReplay({ + recordingData: JSON.stringify([ + { + type: 5, + timestamp: BASE_TIMESTAMP + 10000 + 80, + data: { + tag: 'breadcrumb', + payload: { + timestamp: (BASE_TIMESTAMP + 10000 + 80) / 1000, + type: 'default', + category: 'ui.click', + message: '', + data: {}, + }, }, }, - }, - ]), + ]), + }); }); - }); - it('manually flushes replay and does not continue to record', async () => { - const TEST_EVENT = getTestEventIncremental({ timestamp: BASE_TIMESTAMP }); - mockRecord._emitter(TEST_EVENT); - const optionsEvent = createOptionsEvent(replay); + it('manually flushes replay and does not continue to record', async () => { + const TEST_EVENT = getTestEventIncremental({ timestamp: BASE_TIMESTAMP }); + mockRecord._emitter(TEST_EVENT); + const optionsEvent = createOptionsEvent(replay); - expect(mockRecord.takeFullSnapshot).not.toHaveBeenCalled(); - expect(replay).not.toHaveLastSentReplay(); + expect(mockRecord.takeFullSnapshot).not.toHaveBeenCalled(); + expect(replay).not.toHaveLastSentReplay(); - // Does not capture on mouse click - domHandler({ - name: 'click', - }); - jest.runAllTimers(); - await new Promise(process.nextTick); - expect(replay).not.toHaveLastSentReplay(); + // Does not capture on mouse click + domHandler({ + name: 'click', + }); + jest.runAllTimers(); + await new Promise(process.nextTick); + expect(replay).not.toHaveLastSentReplay(); - replay.sendBufferedReplayOrFlush({ continueRecording: false }); + replay.sendBufferedReplayOrFlush({ continueRecording: false }); - await waitForBufferFlush(); + await waitForBufferFlush(); - expect(replay).toHaveSentReplay({ - recordingPayloadHeader: { segment_id: 0 }, - replayEventPayload: expect.objectContaining({ - replay_type: 'buffer', - }), - recordingData: JSON.stringify([ - { data: { isCheckout: true }, timestamp: BASE_TIMESTAMP, type: 2 }, - optionsEvent, - TEST_EVENT, - { - type: 5, - timestamp: BASE_TIMESTAMP, - data: { - tag: 'breadcrumb', - payload: { - timestamp: BASE_TIMESTAMP / 1000, - type: 'default', - category: 'ui.click', - message: '', - data: {}, + expect(replay).toHaveSentReplay({ + recordingPayloadHeader: { segment_id: 0 }, + replayEventPayload: expect.objectContaining({ + replay_type: 'buffer', + }), + recordingData: JSON.stringify([ + { data: { isCheckout: true }, timestamp: BASE_TIMESTAMP, type: 2 }, + optionsEvent, + TEST_EVENT, + { + type: 5, + timestamp: BASE_TIMESTAMP, + data: { + tag: 'breadcrumb', + payload: { + timestamp: BASE_TIMESTAMP / 1000, + type: 'default', + category: 'ui.click', + message: '', + data: {}, + }, }, }, - }, - ]), - }); + ]), + }); - jest.advanceTimersByTime(DEFAULT_FLUSH_MIN_DELAY); - // Check that click will not get captured - domHandler({ - name: 'click', - }); + jest.advanceTimersByTime(DEFAULT_FLUSH_MIN_DELAY); + // Check that click will not get captured + domHandler({ + name: 'click', + }); - await waitForFlush(); + await waitForFlush(); - // This is still the last replay sent since we passed `continueRecording: - // false`. - expect(replay).toHaveLastSentReplay({ - recordingPayloadHeader: { segment_id: 0 }, - replayEventPayload: expect.objectContaining({ - replay_type: 'buffer', - }), - recordingData: JSON.stringify([ - { data: { isCheckout: true }, timestamp: BASE_TIMESTAMP, type: 2 }, - optionsEvent, - TEST_EVENT, - { - type: 5, - timestamp: BASE_TIMESTAMP, - data: { - tag: 'breadcrumb', - payload: { - timestamp: BASE_TIMESTAMP / 1000, - type: 'default', - category: 'ui.click', - message: '', - data: {}, + // This is still the last replay sent since we passed `continueRecording: + // false`. + expect(replay).toHaveLastSentReplay({ + recordingPayloadHeader: { segment_id: 0 }, + replayEventPayload: expect.objectContaining({ + replay_type: 'buffer', + }), + recordingData: JSON.stringify([ + { data: { isCheckout: true }, timestamp: BASE_TIMESTAMP, type: 2 }, + optionsEvent, + TEST_EVENT, + { + type: 5, + timestamp: BASE_TIMESTAMP, + data: { + tag: 'breadcrumb', + payload: { + timestamp: BASE_TIMESTAMP / 1000, + type: 'default', + category: 'ui.click', + message: '', + data: {}, + }, }, }, - }, - ]), + ]), + }); }); - }); - it('handles multiple simultaneous flushes', async () => { - const TEST_EVENT = getTestEventIncremental({ timestamp: BASE_TIMESTAMP }); - mockRecord._emitter(TEST_EVENT); - const optionsEvent = createOptionsEvent(replay); + it('handles multiple simultaneous flushes', async () => { + const TEST_EVENT = getTestEventIncremental({ timestamp: BASE_TIMESTAMP }); + mockRecord._emitter(TEST_EVENT); + const optionsEvent = createOptionsEvent(replay); - expect(mockRecord.takeFullSnapshot).not.toHaveBeenCalled(); - expect(replay).not.toHaveLastSentReplay(); + expect(mockRecord.takeFullSnapshot).not.toHaveBeenCalled(); + expect(replay).not.toHaveLastSentReplay(); - // Does not capture on mouse click - domHandler({ - name: 'click', - }); - jest.runAllTimers(); - await new Promise(process.nextTick); - expect(replay).not.toHaveLastSentReplay(); + // Does not capture on mouse click + domHandler({ + name: 'click', + }); + jest.runAllTimers(); + await new Promise(process.nextTick); + expect(replay).not.toHaveLastSentReplay(); - replay.sendBufferedReplayOrFlush({ continueRecording: true }); - replay.sendBufferedReplayOrFlush({ continueRecording: true }); + replay.sendBufferedReplayOrFlush({ continueRecording: true }); + replay.sendBufferedReplayOrFlush({ continueRecording: true }); - await waitForBufferFlush(); + await waitForBufferFlush(); - expect(replay).toHaveSentReplay({ - recordingPayloadHeader: { segment_id: 0 }, - replayEventPayload: expect.objectContaining({ - replay_type: 'buffer', - }), - recordingData: JSON.stringify([ - { data: { isCheckout: true }, timestamp: BASE_TIMESTAMP, type: 2 }, - optionsEvent, - TEST_EVENT, - { - type: 5, - timestamp: BASE_TIMESTAMP, - data: { - tag: 'breadcrumb', - payload: { - timestamp: BASE_TIMESTAMP / 1000, - type: 'default', - category: 'ui.click', - message: '', - data: {}, + expect(replay).toHaveSentReplay({ + recordingPayloadHeader: { segment_id: 0 }, + replayEventPayload: expect.objectContaining({ + replay_type: 'buffer', + }), + recordingData: JSON.stringify([ + { data: { isCheckout: true }, timestamp: BASE_TIMESTAMP, type: 2 }, + optionsEvent, + TEST_EVENT, + { + type: 5, + timestamp: BASE_TIMESTAMP, + data: { + tag: 'breadcrumb', + payload: { + timestamp: BASE_TIMESTAMP / 1000, + type: 'default', + category: 'ui.click', + message: '', + data: {}, + }, }, }, - }, - ]), - }); + ]), + }); - jest.advanceTimersByTime(DEFAULT_FLUSH_MIN_DELAY); - // Check that click will not get captured - domHandler({ - name: 'click', - }); + jest.advanceTimersByTime(DEFAULT_FLUSH_MIN_DELAY); + // Check that click will not get captured + domHandler({ + name: 'click', + }); - await waitForFlush(); + await waitForFlush(); - // This is still the last replay sent since we passed `continueRecording: - // false`. - expect(replay).toHaveLastSentReplay({ - recordingPayloadHeader: { segment_id: 1 }, - replayEventPayload: expect.objectContaining({ - replay_type: 'buffer', - }), + // This is still the last replay sent since we passed `continueRecording: + // false`. + expect(replay).toHaveLastSentReplay({ + recordingPayloadHeader: { segment_id: 1 }, + replayEventPayload: expect.objectContaining({ + replay_type: 'buffer', + }), + }); }); - }); - - // This tests a regression where we were calling flush indiscriminantly in `stop()` - it('does not upload a replay event if error is not sampled', async () => { - // We are trying to replicate the case where error rate is 0 and session - // rate is > 0, we can't set them both to 0 otherwise - // `_initializeSessionForSampling` is not called when initializing the plugin. - replay.stop(); - replay['_options']['errorSampleRate'] = 0; - replay['_initializeSessionForSampling'](); - replay.setInitialState(); - jest.runAllTimers(); - await new Promise(process.nextTick); - expect(mockRecord.takeFullSnapshot).not.toHaveBeenCalled(); - expect(replay).not.toHaveLastSentReplay(); - }); + // This tests a regression where we were calling flush indiscriminantly in `stop()` + it('does not upload a replay event if error is not sampled', async () => { + // We are trying to replicate the case where error rate is 0 and session + // rate is > 0, we can't set them both to 0 otherwise + // `_initializeSessionForSampling` is not called when initializing the plugin. + replay.stop(); + replay['_options']['errorSampleRate'] = 0; + replay['_initializeSessionForSampling'](); + replay.setInitialState(); - it('does not send a replay when triggering a full dom snapshot when document becomes visible after [SESSION_IDLE_EXPIRE_DURATION]ms', async () => { - Object.defineProperty(document, 'visibilityState', { - configurable: true, - get: function () { - return 'visible'; - }, + jest.runAllTimers(); + await new Promise(process.nextTick); + expect(mockRecord.takeFullSnapshot).not.toHaveBeenCalled(); + expect(replay).not.toHaveLastSentReplay(); }); - jest.advanceTimersByTime(SESSION_IDLE_EXPIRE_DURATION + 1); + it('does not send a replay when triggering a full dom snapshot when document becomes visible after [SESSION_IDLE_EXPIRE_DURATION]ms', async () => { + Object.defineProperty(document, 'visibilityState', { + configurable: true, + get: function () { + return 'visible'; + }, + }); - document.dispatchEvent(new Event('visibilitychange')); + jest.advanceTimersByTime(SESSION_IDLE_EXPIRE_DURATION + 1); - jest.runAllTimers(); - await new Promise(process.nextTick); + document.dispatchEvent(new Event('visibilitychange')); - expect(replay).not.toHaveLastSentReplay(); - }); + jest.runAllTimers(); + await new Promise(process.nextTick); - it('does not send a replay if user hides the tab and comes back within 60 seconds', async () => { - Object.defineProperty(document, 'visibilityState', { - configurable: true, - get: function () { - return 'hidden'; - }, + expect(replay).not.toHaveLastSentReplay(); }); - document.dispatchEvent(new Event('visibilitychange')); - jest.runAllTimers(); - await new Promise(process.nextTick); + it('does not send a replay if user hides the tab and comes back within 60 seconds', async () => { + Object.defineProperty(document, 'visibilityState', { + configurable: true, + get: function () { + return 'hidden'; + }, + }); + document.dispatchEvent(new Event('visibilitychange')); - expect(replay).not.toHaveLastSentReplay(); + jest.runAllTimers(); + await new Promise(process.nextTick); - // User comes back before `SESSION_IDLE_EXPIRE_DURATION` elapses - jest.advanceTimersByTime(SESSION_IDLE_EXPIRE_DURATION - 100); - Object.defineProperty(document, 'visibilityState', { - configurable: true, - get: function () { - return 'visible'; - }, - }); - document.dispatchEvent(new Event('visibilitychange')); + expect(replay).not.toHaveLastSentReplay(); - jest.runAllTimers(); - await new Promise(process.nextTick); + // User comes back before `SESSION_IDLE_EXPIRE_DURATION` elapses + jest.advanceTimersByTime(SESSION_IDLE_EXPIRE_DURATION - 100); + Object.defineProperty(document, 'visibilityState', { + configurable: true, + get: function () { + return 'visible'; + }, + }); + document.dispatchEvent(new Event('visibilitychange')); - expect(mockRecord.takeFullSnapshot).not.toHaveBeenCalled(); - expect(replay).not.toHaveLastSentReplay(); - }); + jest.runAllTimers(); + await new Promise(process.nextTick); - it('does not upload a replay event when document becomes hidden', async () => { - Object.defineProperty(document, 'visibilityState', { - configurable: true, - get: function () { - return 'hidden'; - }, + expect(mockRecord.takeFullSnapshot).not.toHaveBeenCalled(); + expect(replay).not.toHaveLastSentReplay(); }); - // Pretend 5 seconds have passed - const ELAPSED = 5000; - jest.advanceTimersByTime(ELAPSED); + it('does not upload a replay event when document becomes hidden', async () => { + Object.defineProperty(document, 'visibilityState', { + configurable: true, + get: function () { + return 'hidden'; + }, + }); - const TEST_EVENT = getTestEventCheckout({ timestamp: BASE_TIMESTAMP }); - addEvent(replay, TEST_EVENT); + // Pretend 5 seconds have passed + const ELAPSED = 5000; + jest.advanceTimersByTime(ELAPSED); - document.dispatchEvent(new Event('visibilitychange')); + const TEST_EVENT = getTestEventCheckout({ timestamp: BASE_TIMESTAMP }); + addEvent(replay, TEST_EVENT); - jest.runAllTimers(); - await new Promise(process.nextTick); + document.dispatchEvent(new Event('visibilitychange')); - expect(mockRecord.takeFullSnapshot).not.toHaveBeenCalled(); - expect(replay).not.toHaveLastSentReplay(); - }); + jest.runAllTimers(); + await new Promise(process.nextTick); - it('does not upload a replay event if 5 seconds have elapsed since the last replay event occurred', async () => { - const TEST_EVENT = getTestEventIncremental({ timestamp: BASE_TIMESTAMP }); - mockRecord._emitter(TEST_EVENT); - // Pretend 5 seconds have passed - const ELAPSED = 5000; - await advanceTimers(ELAPSED); + expect(mockRecord.takeFullSnapshot).not.toHaveBeenCalled(); + expect(replay).not.toHaveLastSentReplay(); + }); - expect(mockRecord.takeFullSnapshot).not.toHaveBeenCalled(); + it('does not upload a replay event if 5 seconds have elapsed since the last replay event occurred', async () => { + const TEST_EVENT = getTestEventIncremental({ timestamp: BASE_TIMESTAMP }); + mockRecord._emitter(TEST_EVENT); + // Pretend 5 seconds have passed + const ELAPSED = 5000; + await advanceTimers(ELAPSED); - jest.runAllTimers(); - await new Promise(process.nextTick); + expect(mockRecord.takeFullSnapshot).not.toHaveBeenCalled(); - expect(replay).not.toHaveLastSentReplay(); - }); + jest.runAllTimers(); + await new Promise(process.nextTick); - it('does not upload a replay event if 15 seconds have elapsed since the last replay upload', async () => { - const TEST_EVENT = getTestEventIncremental({ timestamp: BASE_TIMESTAMP }); - // Fire a new event every 4 seconds, 4 times - [...Array(4)].forEach(() => { - mockRecord._emitter(TEST_EVENT); - jest.advanceTimersByTime(4000); + expect(replay).not.toHaveLastSentReplay(); }); - // We are at time = +16seconds now (relative to BASE_TIMESTAMP) - // The next event should cause an upload immediately - mockRecord._emitter(TEST_EVENT); - await new Promise(process.nextTick); + it('does not upload a replay event if 15 seconds have elapsed since the last replay upload', async () => { + const TEST_EVENT = getTestEventIncremental({ timestamp: BASE_TIMESTAMP }); + // Fire a new event every 4 seconds, 4 times + [...Array(4)].forEach(() => { + mockRecord._emitter(TEST_EVENT); + jest.advanceTimersByTime(4000); + }); - expect(replay).not.toHaveLastSentReplay(); + // We are at time = +16seconds now (relative to BASE_TIMESTAMP) + // The next event should cause an upload immediately + mockRecord._emitter(TEST_EVENT); + await new Promise(process.nextTick); - // There should also not be another attempt at an upload 5 seconds after the last replay event - await waitForFlush(); - expect(replay).not.toHaveLastSentReplay(); + expect(replay).not.toHaveLastSentReplay(); - // Let's make sure it continues to work - mockRecord._emitter(TEST_EVENT); - await waitForFlush(); - jest.runAllTimers(); - await new Promise(process.nextTick); - expect(replay).not.toHaveLastSentReplay(); - }); + // There should also not be another attempt at an upload 5 seconds after the last replay event + await waitForFlush(); + expect(replay).not.toHaveLastSentReplay(); + + // Let's make sure it continues to work + mockRecord._emitter(TEST_EVENT); + await waitForFlush(); + jest.runAllTimers(); + await new Promise(process.nextTick); + expect(replay).not.toHaveLastSentReplay(); + }); - // When the error session records as a normal session, we want to stop - // recording after the session ends. Otherwise, we get into a state where the - // new session is a session type replay (this could conflict with the session - // sample rate of 0.0), or an error session that has no errors. Instead we - // simply stop the session replay completely and wait for a new page load to - // resample. - it.each([ - ['MAX_REPLAY_DURATION', MAX_REPLAY_DURATION], - ['SESSION_IDLE_DURATION', SESSION_IDLE_EXPIRE_DURATION], - ])( - 'stops replay if session had an error and exceeds %s and does not start a new session thereafter', - async (_label, waitTime) => { - expect(replay.session?.shouldRefresh).toBe(true); + // When the error session records as a normal session, we want to refresh + // sampling after the session ends. + it.each([ + ['MAX_REPLAY_DURATION', MAX_REPLAY_DURATION], + ['SESSION_IDLE_DURATION', SESSION_IDLE_EXPIRE_DURATION], + ])('refreshes replay if session had an error and exceeds %s', async (_label, waitTime) => { + expect(replay.session?.segmentId).toBe(0); captureException(new Error('testing')); @@ -457,7 +452,9 @@ describe('Integration | errorSampleRate', () => { replay_type: 'buffer', }), }); - expect(replay.session?.shouldRefresh).toBe(false); + expect(replay.session?.segmentId).toBeGreaterThan(0); + + const sessionId = replay.getSessionId(); // Idle for given time jest.advanceTimersByTime(waitTime + 1); @@ -482,586 +479,583 @@ describe('Integration | errorSampleRate', () => { }), }); - expect(replay.isEnabled()).toBe(false); - - domHandler({ - name: 'click', - }); - - // Remains disabled! - expect(replay.isEnabled()).toBe(false); - }, - ); + expect(replay.isEnabled()).toBe(true); + expect(replay.getSessionId()).not.toBe(sessionId); + }); - it.each([ - ['MAX_REPLAY_DURATION', MAX_REPLAY_DURATION], - ['SESSION_IDLE_EXPIRE_DURATION', SESSION_IDLE_EXPIRE_DURATION], - ])('continues buffering replay if session had no error and exceeds %s', async (_label, waitTime) => { - const oldSessionId = replay.session?.id; - expect(oldSessionId).toBeDefined(); + it.each([ + ['MAX_REPLAY_DURATION', MAX_REPLAY_DURATION], + ['SESSION_IDLE_EXPIRE_DURATION', SESSION_IDLE_EXPIRE_DURATION], + ])('continues buffering replay if session had no error and exceeds %s', async (_label, waitTime) => { + const oldSessionId = replay.session?.id; + expect(oldSessionId).toBeDefined(); - expect(replay).not.toHaveLastSentReplay(); + expect(replay).not.toHaveLastSentReplay(); - // Idle for given time - jest.advanceTimersByTime(waitTime + 1); - await new Promise(process.nextTick); + // Idle for given time + jest.advanceTimersByTime(waitTime + 1); + await new Promise(process.nextTick); - const TEST_EVENT = getTestEventIncremental({ - data: { name: 'lost event' }, - timestamp: BASE_TIMESTAMP, - }); - mockRecord._emitter(TEST_EVENT); + const TEST_EVENT = getTestEventIncremental({ + data: { name: 'lost event' }, + timestamp: BASE_TIMESTAMP, + }); + mockRecord._emitter(TEST_EVENT); - jest.runAllTimers(); - await new Promise(process.nextTick); + jest.runAllTimers(); + await new Promise(process.nextTick); - // in production, this happens at a time interval, here we mock this - mockRecord.takeFullSnapshot(true); + // in production, this happens at a time interval, here we mock this + mockRecord.takeFullSnapshot(true); - // still no new replay sent - expect(replay).not.toHaveLastSentReplay(); + // still no new replay sent + expect(replay).not.toHaveLastSentReplay(); - expect(replay.isEnabled()).toBe(true); - expect(replay.isPaused()).toBe(false); - expect(replay.recordingMode).toBe('buffer'); + expect(replay.isEnabled()).toBe(true); + expect(replay.isPaused()).toBe(false); + expect(replay.recordingMode).toBe('buffer'); - domHandler({ - name: 'click', - }); + domHandler({ + name: 'click', + }); - await waitForFlush(); + await waitForFlush(); - expect(replay).not.toHaveLastSentReplay(); - expect(replay.isEnabled()).toBe(true); - expect(replay.isPaused()).toBe(false); - expect(replay.recordingMode).toBe('buffer'); + expect(replay).not.toHaveLastSentReplay(); + expect(replay.isEnabled()).toBe(true); + expect(replay.isPaused()).toBe(false); + expect(replay.recordingMode).toBe('buffer'); - // should still react to errors later on - captureException(new Error('testing')); + // should still react to errors later on + captureException(new Error('testing')); - await waitForBufferFlush(); + await waitForBufferFlush(); - expect(replay.session?.id).toBe(oldSessionId); + expect(replay.session?.id).toBe(oldSessionId); - expect(replay).toHaveLastSentReplay({ - recordingPayloadHeader: { segment_id: 0 }, - replayEventPayload: expect.objectContaining({ - replay_type: 'buffer', - }), + expect(replay).toHaveLastSentReplay({ + recordingPayloadHeader: { segment_id: 0 }, + replayEventPayload: expect.objectContaining({ + replay_type: 'buffer', + }), + }); + + expect(replay.isEnabled()).toBe(true); + expect(replay.isPaused()).toBe(false); + expect(replay.recordingMode).toBe('session'); + expect(replay.session?.sampled).toBe('buffer'); + expect(replay.session?.segmentId).toBeGreaterThan(0); }); - expect(replay.isEnabled()).toBe(true); - expect(replay.isPaused()).toBe(false); - expect(replay.recordingMode).toBe('session'); - expect(replay.session?.sampled).toBe('buffer'); - expect(replay.session?.shouldRefresh).toBe(false); - }); + // Should behave the same as above test + it('stops replay if user has been idle for more than SESSION_IDLE_EXPIRE_DURATION and does not start a new session thereafter', async () => { + const oldSessionId = replay.session?.id; + expect(oldSessionId).toBeDefined(); - // Should behave the same as above test - it('stops replay if user has been idle for more than SESSION_IDLE_EXPIRE_DURATION and does not start a new session thereafter', async () => { - const oldSessionId = replay.session?.id; - expect(oldSessionId).toBeDefined(); + // Idle for 15 minutes + jest.advanceTimersByTime(SESSION_IDLE_EXPIRE_DURATION + 1); - // Idle for 15 minutes - jest.advanceTimersByTime(SESSION_IDLE_EXPIRE_DURATION + 1); + const TEST_EVENT = getTestEventIncremental({ + data: { name: 'lost event' }, + timestamp: BASE_TIMESTAMP, + }); + mockRecord._emitter(TEST_EVENT); + expect(replay).not.toHaveLastSentReplay(); - const TEST_EVENT = getTestEventIncremental({ - data: { name: 'lost event' }, - timestamp: BASE_TIMESTAMP, - }); - mockRecord._emitter(TEST_EVENT); - expect(replay).not.toHaveLastSentReplay(); + jest.runAllTimers(); + await new Promise(process.nextTick); - jest.runAllTimers(); - await new Promise(process.nextTick); + // We stop recording after SESSION_IDLE_EXPIRE_DURATION of inactivity in error mode + expect(replay).not.toHaveLastSentReplay(); + expect(replay.isEnabled()).toBe(true); + expect(replay.isPaused()).toBe(false); + expect(replay.recordingMode).toBe('buffer'); - // We stop recording after SESSION_IDLE_EXPIRE_DURATION of inactivity in error mode - expect(replay).not.toHaveLastSentReplay(); - expect(replay.isEnabled()).toBe(true); - expect(replay.isPaused()).toBe(false); - expect(replay.recordingMode).toBe('buffer'); + // should still react to errors later on + captureException(new Error('testing')); - // should still react to errors later on - captureException(new Error('testing')); + await new Promise(process.nextTick); + jest.advanceTimersByTime(DEFAULT_FLUSH_MIN_DELAY); + await new Promise(process.nextTick); + expect(replay.session?.id).toBe(oldSessionId); - await new Promise(process.nextTick); - jest.advanceTimersByTime(DEFAULT_FLUSH_MIN_DELAY); - await new Promise(process.nextTick); - expect(replay.session?.id).toBe(oldSessionId); + // buffered events + expect(replay).toHaveSentReplay({ + recordingPayloadHeader: { segment_id: 0 }, + replayEventPayload: expect.objectContaining({ + replay_type: 'buffer', + }), + }); - // buffered events - expect(replay).toHaveSentReplay({ - recordingPayloadHeader: { segment_id: 0 }, - replayEventPayload: expect.objectContaining({ - replay_type: 'buffer', - }), - }); + // `startRecording` full checkout + expect(replay).toHaveLastSentReplay({ + recordingPayloadHeader: { segment_id: 0 }, + replayEventPayload: expect.objectContaining({ + replay_type: 'buffer', + }), + }); - // `startRecording` full checkout - expect(replay).toHaveLastSentReplay({ - recordingPayloadHeader: { segment_id: 0 }, - replayEventPayload: expect.objectContaining({ - replay_type: 'buffer', - }), + expect(replay.isEnabled()).toBe(true); + expect(replay.isPaused()).toBe(false); + expect(replay.recordingMode).toBe('session'); + expect(replay.session?.sampled).toBe('buffer'); + expect(replay.session?.segmentId).toBeGreaterThan(0); }); - expect(replay.isEnabled()).toBe(true); - expect(replay.isPaused()).toBe(false); - expect(replay.recordingMode).toBe('session'); - expect(replay.session?.sampled).toBe('buffer'); - expect(replay.session?.shouldRefresh).toBe(false); - }); - - it('has the correct timestamps with deferred root event and last replay update', async () => { - const TEST_EVENT = getTestEventIncremental({ timestamp: BASE_TIMESTAMP }); - mockRecord._emitter(TEST_EVENT); - const optionsEvent = createOptionsEvent(replay); + it('has the correct timestamps with deferred root event and last replay update', async () => { + const TEST_EVENT = getTestEventIncremental({ timestamp: BASE_TIMESTAMP }); + mockRecord._emitter(TEST_EVENT); + const optionsEvent = createOptionsEvent(replay); - expect(mockRecord.takeFullSnapshot).not.toHaveBeenCalled(); - expect(replay).not.toHaveLastSentReplay(); + expect(mockRecord.takeFullSnapshot).not.toHaveBeenCalled(); + expect(replay).not.toHaveLastSentReplay(); - jest.runAllTimers(); - await new Promise(process.nextTick); + jest.runAllTimers(); + await new Promise(process.nextTick); - jest.advanceTimersByTime(DEFAULT_FLUSH_MIN_DELAY); + jest.advanceTimersByTime(DEFAULT_FLUSH_MIN_DELAY); - captureException(new Error('testing')); + captureException(new Error('testing')); - await new Promise(process.nextTick); - jest.advanceTimersByTime(DEFAULT_FLUSH_MIN_DELAY); - await new Promise(process.nextTick); + await new Promise(process.nextTick); + jest.advanceTimersByTime(DEFAULT_FLUSH_MIN_DELAY); + await new Promise(process.nextTick); - expect(replay).toHaveSentReplay({ - recordingData: JSON.stringify([ - { data: { isCheckout: true }, timestamp: BASE_TIMESTAMP, type: 2 }, - optionsEvent, - TEST_EVENT, - ]), - replayEventPayload: expect.objectContaining({ - replay_start_timestamp: BASE_TIMESTAMP / 1000, - // the exception happens roughly 10 seconds after BASE_TIMESTAMP - // (advance timers + waiting for flush after the checkout) and - // extra time is likely due to async of `addMemoryEntry()` - - timestamp: (BASE_TIMESTAMP + DEFAULT_FLUSH_MIN_DELAY + DEFAULT_FLUSH_MIN_DELAY + 40) / 1000, - error_ids: [expect.any(String)], - trace_ids: [], - urls: ['http://localhost/'], - replay_id: expect.any(String), - }), - recordingPayloadHeader: { segment_id: 0 }, + expect(replay).toHaveSentReplay({ + recordingData: JSON.stringify([ + { data: { isCheckout: true }, timestamp: BASE_TIMESTAMP, type: 2 }, + optionsEvent, + TEST_EVENT, + ]), + replayEventPayload: expect.objectContaining({ + replay_start_timestamp: BASE_TIMESTAMP / 1000, + // the exception happens roughly 10 seconds after BASE_TIMESTAMP + // (advance timers + waiting for flush after the checkout) and + // extra time is likely due to async of `addMemoryEntry()` + + timestamp: (BASE_TIMESTAMP + DEFAULT_FLUSH_MIN_DELAY + DEFAULT_FLUSH_MIN_DELAY + 40) / 1000, + error_ids: [expect.any(String)], + trace_ids: [], + urls: ['http://localhost/'], + replay_id: expect.any(String), + }), + recordingPayloadHeader: { segment_id: 0 }, + }); }); - }); - it('has correct timestamps when error occurs much later than initial pageload/checkout', async () => { - const ELAPSED = BUFFER_CHECKOUT_TIME; - const TICK = 20; - const TEST_EVENT = getTestEventIncremental({ timestamp: BASE_TIMESTAMP }); - mockRecord._emitter(TEST_EVENT); + it('has correct timestamps when error occurs much later than initial pageload/checkout', async () => { + const ELAPSED = BUFFER_CHECKOUT_TIME; + const TICK = 20; + const TEST_EVENT = getTestEventIncremental({ timestamp: BASE_TIMESTAMP }); + mockRecord._emitter(TEST_EVENT); - // add a mock performance event - replay.performanceEvents.push(PerformanceEntryResource()); + // add a mock performance event + replay.performanceEvents.push(PerformanceEntryResource()); - jest.runAllTimers(); - await new Promise(process.nextTick); + jest.runAllTimers(); + await new Promise(process.nextTick); - expect(mockRecord.takeFullSnapshot).not.toHaveBeenCalled(); - expect(replay).not.toHaveLastSentReplay(); + expect(mockRecord.takeFullSnapshot).not.toHaveBeenCalled(); + expect(replay).not.toHaveLastSentReplay(); - jest.advanceTimersByTime(ELAPSED); + jest.advanceTimersByTime(ELAPSED); - // in production, this happens at a time interval - // session started time should be updated to this current timestamp - mockRecord.takeFullSnapshot(true); - const optionsEvent = createOptionsEvent(replay); + // in production, this happens at a time interval + // session started time should be updated to this current timestamp + mockRecord.takeFullSnapshot(true); + const optionsEvent = createOptionsEvent(replay); - jest.runAllTimers(); - await new Promise(process.nextTick); + jest.runAllTimers(); + await new Promise(process.nextTick); - expect(replay).not.toHaveLastSentReplay(); + expect(replay).not.toHaveLastSentReplay(); - captureException(new Error('testing')); + captureException(new Error('testing')); - await waitForBufferFlush(); + await waitForBufferFlush(); - // This is still the timestamp from the full snapshot we took earlier - expect(replay.session?.started).toBe(BASE_TIMESTAMP + ELAPSED + TICK); + // This is still the timestamp from the full snapshot we took earlier + expect(replay.session?.started).toBe(BASE_TIMESTAMP + ELAPSED + TICK); - // Does not capture mouse click - expect(replay).toHaveSentReplay({ - recordingPayloadHeader: { segment_id: 0 }, - replayEventPayload: expect.objectContaining({ - // Make sure the old performance event is thrown out - replay_start_timestamp: (BASE_TIMESTAMP + ELAPSED + TICK) / 1000, - }), - recordingData: JSON.stringify([ - { - data: { isCheckout: true }, - timestamp: BASE_TIMESTAMP + ELAPSED + TICK, - type: 2, - }, - optionsEvent, - ]), + // Does not capture mouse click + expect(replay).toHaveSentReplay({ + recordingPayloadHeader: { segment_id: 0 }, + replayEventPayload: expect.objectContaining({ + // Make sure the old performance event is thrown out + replay_start_timestamp: (BASE_TIMESTAMP + ELAPSED + TICK) / 1000, + }), + recordingData: JSON.stringify([ + { + data: { isCheckout: true }, + timestamp: BASE_TIMESTAMP + ELAPSED + TICK, + type: 2, + }, + optionsEvent, + ]), + }); }); - }); - it('stops replay when user goes idle', async () => { - jest.setSystemTime(BASE_TIMESTAMP); + it('refreshes replay when user goes idle', async () => { + jest.setSystemTime(BASE_TIMESTAMP); - const TEST_EVENT = getTestEventIncremental({ timestamp: BASE_TIMESTAMP }); - mockRecord._emitter(TEST_EVENT); + const TEST_EVENT = getTestEventIncremental({ timestamp: BASE_TIMESTAMP }); + mockRecord._emitter(TEST_EVENT); - expect(mockRecord.takeFullSnapshot).not.toHaveBeenCalled(); - expect(replay).not.toHaveLastSentReplay(); + expect(mockRecord.takeFullSnapshot).not.toHaveBeenCalled(); + expect(replay).not.toHaveLastSentReplay(); - jest.runAllTimers(); - await new Promise(process.nextTick); + jest.runAllTimers(); + await new Promise(process.nextTick); - captureException(new Error('testing')); + captureException(new Error('testing')); - await waitForBufferFlush(); + await waitForBufferFlush(); - expect(replay).toHaveLastSentReplay(); + expect(replay).toHaveLastSentReplay(); - // Flush from calling `stopRecording` - await waitForFlush(); + // Flush from calling `stopRecording` + await waitForFlush(); - // Now wait after session expires - should stop recording - mockRecord.takeFullSnapshot.mockClear(); - (getCurrentHub().getClient()!.getTransport()!.send as unknown as jest.SpyInstance).mockClear(); + // Now wait after session expires - should stop recording + mockRecord.takeFullSnapshot.mockClear(); + (getCurrentHub().getClient()!.getTransport()!.send as unknown as jest.SpyInstance).mockClear(); - expect(replay).not.toHaveLastSentReplay(); + expect(replay).not.toHaveLastSentReplay(); - // Go idle - jest.advanceTimersByTime(SESSION_IDLE_EXPIRE_DURATION + 1); - await new Promise(process.nextTick); + const sessionId = replay.getSessionId(); - mockRecord._emitter(TEST_EVENT); + // Go idle + jest.advanceTimersByTime(SESSION_IDLE_EXPIRE_DURATION + 1); + await new Promise(process.nextTick); - expect(replay).not.toHaveLastSentReplay(); + mockRecord._emitter(TEST_EVENT); - await waitForFlush(); + expect(replay).not.toHaveLastSentReplay(); - expect(replay).not.toHaveLastSentReplay(); - expect(mockRecord.takeFullSnapshot).toHaveBeenCalledTimes(0); - expect(replay.isEnabled()).toBe(false); - }); + await waitForFlush(); - it('stops replay when session exceeds max length after latest captured error', async () => { - const sessionId = replay.session?.id; - jest.setSystemTime(BASE_TIMESTAMP); + expect(replay).not.toHaveLastSentReplay(); + expect(mockRecord.takeFullSnapshot).toHaveBeenCalledTimes(0); + expect(replay.isEnabled()).toBe(true); + expect(replay.getSessionId()).not.toBe(sessionId); + }); - const TEST_EVENT = getTestEventIncremental({ timestamp: BASE_TIMESTAMP }); - mockRecord._emitter(TEST_EVENT); + it('refreshes replay when session exceeds max length after latest captured error', async () => { + const sessionId = replay.session?.id; + jest.setSystemTime(BASE_TIMESTAMP); - expect(mockRecord.takeFullSnapshot).not.toHaveBeenCalled(); - expect(replay).not.toHaveLastSentReplay(); + const TEST_EVENT = getTestEventIncremental({ timestamp: BASE_TIMESTAMP }); + mockRecord._emitter(TEST_EVENT); - jest.runAllTimers(); - await new Promise(process.nextTick); + expect(mockRecord.takeFullSnapshot).not.toHaveBeenCalled(); + expect(replay).not.toHaveLastSentReplay(); - jest.advanceTimersByTime(2 * MAX_REPLAY_DURATION); + jest.runAllTimers(); + await new Promise(process.nextTick); - // in production, this happens at a time interval, here we mock this - mockRecord.takeFullSnapshot(true); + jest.advanceTimersByTime(2 * MAX_REPLAY_DURATION); - captureException(new Error('testing')); + // in production, this happens at a time interval, here we mock this + mockRecord.takeFullSnapshot(true); - // Flush due to exception - await new Promise(process.nextTick); - await waitForFlush(); + captureException(new Error('testing')); - expect(replay.session?.id).toBe(sessionId); - expect(replay).toHaveLastSentReplay({ - recordingPayloadHeader: { segment_id: 0 }, - }); + // Flush due to exception + await new Promise(process.nextTick); + await waitForFlush(); - // This comes from `startRecording()` in `sendBufferedReplayOrFlush()` - await waitForFlush(); - expect(replay).toHaveLastSentReplay({ - recordingPayloadHeader: { segment_id: 1 }, - recordingData: JSON.stringify([ - { - data: { - isCheckout: true, + expect(replay.session?.id).toBe(sessionId); + expect(replay).toHaveLastSentReplay({ + recordingPayloadHeader: { segment_id: 0 }, + }); + + // This comes from `startRecording()` in `sendBufferedReplayOrFlush()` + await waitForFlush(); + expect(replay).toHaveLastSentReplay({ + recordingPayloadHeader: { segment_id: 1 }, + recordingData: JSON.stringify([ + { + data: { + isCheckout: true, + }, + timestamp: BASE_TIMESTAMP + 2 * MAX_REPLAY_DURATION + DEFAULT_FLUSH_MIN_DELAY + 40, + type: 2, }, - timestamp: BASE_TIMESTAMP + 2 * MAX_REPLAY_DURATION + DEFAULT_FLUSH_MIN_DELAY + 40, - type: 2, - }, - ]), - }); + ]), + }); - // Now wait after session expires - should stop recording - mockRecord.takeFullSnapshot.mockClear(); - (getCurrentHub().getClient()!.getTransport()!.send as unknown as jest.SpyInstance).mockClear(); + // Now wait after session expires - should stop recording + mockRecord.takeFullSnapshot.mockClear(); + (getCurrentHub().getClient()!.getTransport()!.send as unknown as jest.SpyInstance).mockClear(); - jest.advanceTimersByTime(MAX_REPLAY_DURATION); - await new Promise(process.nextTick); + jest.advanceTimersByTime(MAX_REPLAY_DURATION); + await new Promise(process.nextTick); - mockRecord._emitter(TEST_EVENT); - jest.runAllTimers(); - await new Promise(process.nextTick); + mockRecord._emitter(TEST_EVENT); + jest.runAllTimers(); + await new Promise(process.nextTick); - expect(replay).not.toHaveLastSentReplay(); - expect(mockRecord.takeFullSnapshot).toHaveBeenCalledTimes(0); - expect(replay.isEnabled()).toBe(false); + expect(replay).not.toHaveLastSentReplay(); + expect(mockRecord.takeFullSnapshot).toHaveBeenCalledTimes(0); + expect(replay.isEnabled()).toBe(true); + expect(replay.getSessionId()).not.toBe(sessionId); - // Once the session is stopped after capturing a replay already - // (buffer-mode), another error will not trigger a new replay - captureException(new Error('testing')); + // Once the session is stopped after capturing a replay already + // (buffer-mode), another error will trigger a new replay + captureException(new Error('testing')); - await new Promise(process.nextTick); - jest.advanceTimersByTime(DEFAULT_FLUSH_MIN_DELAY); - await new Promise(process.nextTick); - expect(replay).not.toHaveLastSentReplay(); - }); + await new Promise(process.nextTick); + jest.advanceTimersByTime(DEFAULT_FLUSH_MIN_DELAY); + await new Promise(process.nextTick); + expect(replay).toHaveLastSentReplay(); + }); - it('does not stop replay based on earliest event in buffer', async () => { - jest.setSystemTime(BASE_TIMESTAMP); + it('does not refresh replay based on earliest event in buffer', async () => { + jest.setSystemTime(BASE_TIMESTAMP); - const TEST_EVENT = getTestEventIncremental({ timestamp: BASE_TIMESTAMP - 60000 }); - mockRecord._emitter(TEST_EVENT); + const TEST_EVENT = getTestEventIncremental({ timestamp: BASE_TIMESTAMP - 60000 }); + mockRecord._emitter(TEST_EVENT); - expect(mockRecord.takeFullSnapshot).not.toHaveBeenCalled(); - expect(replay).not.toHaveLastSentReplay(); + expect(mockRecord.takeFullSnapshot).not.toHaveBeenCalled(); + expect(replay).not.toHaveLastSentReplay(); - jest.runAllTimers(); - await new Promise(process.nextTick); + jest.runAllTimers(); + await new Promise(process.nextTick); - expect(replay).not.toHaveLastSentReplay(); - captureException(new Error('testing')); + expect(replay).not.toHaveLastSentReplay(); + captureException(new Error('testing')); - await waitForBufferFlush(); + await waitForBufferFlush(); - expect(replay).toHaveLastSentReplay(); + expect(replay).toHaveLastSentReplay(); - // Flush from calling `stopRecording` - await waitForFlush(); + // Flush from calling `stopRecording` + await waitForFlush(); - // Now wait after session expires - should stop recording - mockRecord.takeFullSnapshot.mockClear(); - (getCurrentHub().getClient()!.getTransport()!.send as unknown as jest.SpyInstance).mockClear(); + // Now wait after session expires - should stop recording + mockRecord.takeFullSnapshot.mockClear(); + (getCurrentHub().getClient()!.getTransport()!.send as unknown as jest.SpyInstance).mockClear(); - expect(replay).not.toHaveLastSentReplay(); + expect(replay).not.toHaveLastSentReplay(); - const TICKS = 80; + const TICKS = 80; - // We advance time so that we are on the border of expiring, taking into - // account that TEST_EVENT timestamp is 60000 ms before BASE_TIMESTAMP. The - // 3 DEFAULT_FLUSH_MIN_DELAY is to account for the `waitForFlush` that has - // happened, and for the next two that will happen. The first following - // `waitForFlush` does not expire session, but the following one will. - jest.advanceTimersByTime(SESSION_IDLE_EXPIRE_DURATION - 60000 - 3 * DEFAULT_FLUSH_MIN_DELAY - TICKS); - await new Promise(process.nextTick); + // We advance time so that we are on the border of expiring, taking into + // account that TEST_EVENT timestamp is 60000 ms before BASE_TIMESTAMP. The + // 3 DEFAULT_FLUSH_MIN_DELAY is to account for the `waitForFlush` that has + // happened, and for the next two that will happen. The first following + // `waitForFlush` does not expire session, but the following one will. + jest.advanceTimersByTime(SESSION_IDLE_EXPIRE_DURATION - 60000 - 3 * DEFAULT_FLUSH_MIN_DELAY - TICKS); + await new Promise(process.nextTick); - mockRecord._emitter(TEST_EVENT); - expect(replay).not.toHaveLastSentReplay(); - await waitForFlush(); + mockRecord._emitter(TEST_EVENT); + expect(replay).not.toHaveLastSentReplay(); + await waitForFlush(); - expect(replay).not.toHaveLastSentReplay(); - expect(mockRecord.takeFullSnapshot).toHaveBeenCalledTimes(0); - expect(replay.isEnabled()).toBe(true); + expect(replay).not.toHaveLastSentReplay(); + expect(mockRecord.takeFullSnapshot).toHaveBeenCalledTimes(0); + expect(replay.isEnabled()).toBe(true); - mockRecord._emitter(TEST_EVENT); - expect(replay).not.toHaveLastSentReplay(); - await waitForFlush(); + mockRecord._emitter(TEST_EVENT); + expect(replay).not.toHaveLastSentReplay(); + await waitForFlush(); - expect(replay).not.toHaveLastSentReplay(); - expect(mockRecord.takeFullSnapshot).toHaveBeenCalledTimes(0); - expect(replay.isEnabled()).toBe(true); + expect(replay).not.toHaveLastSentReplay(); + expect(mockRecord.takeFullSnapshot).toHaveBeenCalledTimes(0); + expect(replay.isEnabled()).toBe(true); - // It's hard to test, but if we advance the below time less 1 ms, it should - // be enabled, but we can't trigger a session check via flush without - // incurring another DEFAULT_FLUSH_MIN_DELAY timeout. - jest.advanceTimersByTime(60000 - DEFAULT_FLUSH_MIN_DELAY); - mockRecord._emitter(TEST_EVENT); - expect(replay).not.toHaveLastSentReplay(); - await waitForFlush(); + const sessionId = replay.getSessionId(); - expect(replay).not.toHaveLastSentReplay(); - expect(mockRecord.takeFullSnapshot).toHaveBeenCalledTimes(0); - expect(replay.isEnabled()).toBe(false); - }); + // It's hard to test, but if we advance the below time less 1 ms, it should + // be enabled, but we can't trigger a session check via flush without + // incurring another DEFAULT_FLUSH_MIN_DELAY timeout. + jest.advanceTimersByTime(60000 - DEFAULT_FLUSH_MIN_DELAY); + mockRecord._emitter(TEST_EVENT); + expect(replay).not.toHaveLastSentReplay(); + await waitForFlush(); - it('handles very long active buffer session', async () => { - const stepDuration = 10_000; - const steps = 5_000; + expect(replay).not.toHaveLastSentReplay(); + expect(mockRecord.takeFullSnapshot).toHaveBeenCalledTimes(0); + expect(replay.isEnabled()).toBe(true); + expect(replay.getSessionId()).not.toBe(sessionId); + }); - jest.setSystemTime(BASE_TIMESTAMP); + it('handles very long active buffer session', async () => { + const stepDuration = 10_000; + const steps = 5_000; - expect(replay).not.toHaveLastSentReplay(); + jest.setSystemTime(BASE_TIMESTAMP); - let optionsEvent = createOptionsEvent(replay); + expect(replay).not.toHaveLastSentReplay(); - for (let i = 1; i <= steps; i++) { - jest.advanceTimersByTime(stepDuration); - optionsEvent = createOptionsEvent(replay); - mockRecord._emitter({ data: { step: i }, timestamp: BASE_TIMESTAMP + stepDuration * i, type: 2 }, true); - mockRecord._emitter({ data: { step: i }, timestamp: BASE_TIMESTAMP + stepDuration * i + 5, type: 3 }); - } + let optionsEvent = createOptionsEvent(replay); - expect(replay).not.toHaveLastSentReplay(); + for (let i = 1; i <= steps; i++) { + jest.advanceTimersByTime(stepDuration); + optionsEvent = createOptionsEvent(replay); + mockRecord._emitter({ data: { step: i }, timestamp: BASE_TIMESTAMP + stepDuration * i, type: 2 }, true); + mockRecord._emitter({ data: { step: i }, timestamp: BASE_TIMESTAMP + stepDuration * i + 5, type: 3 }); + } - expect(replay.isEnabled()).toBe(true); - expect(replay.isPaused()).toBe(false); - expect(replay.recordingMode).toBe('buffer'); + expect(replay).not.toHaveLastSentReplay(); - // Now capture an error - captureException(new Error('testing')); - await waitForBufferFlush(); + expect(replay.isEnabled()).toBe(true); + expect(replay.isPaused()).toBe(false); + expect(replay.recordingMode).toBe('buffer'); - expect(replay).toHaveLastSentReplay({ - recordingData: JSON.stringify([ - { data: { step: steps }, timestamp: BASE_TIMESTAMP + stepDuration * steps, type: 2 }, - optionsEvent, - { data: { step: steps }, timestamp: BASE_TIMESTAMP + stepDuration * steps + 5, type: 3 }, - ]), - replayEventPayload: expect.objectContaining({ - replay_start_timestamp: (BASE_TIMESTAMP + stepDuration * steps) / 1000, - error_ids: [expect.any(String)], - trace_ids: [], - urls: ['http://localhost/'], - replay_id: expect.any(String), - }), - recordingPayloadHeader: { segment_id: 0 }, - }); - }); -}); + // Now capture an error + captureException(new Error('testing')); + await waitForBufferFlush(); -/** - * If an error happens, we switch the recordingMode to `session`, set `shouldRefresh=false` on the session, - * but keep `sampled=buffer`. - * This test should verify that if we load such a session from sessionStorage, the session is eventually correctly ended. - */ -it('handles buffer sessions that previously had an error', async () => { - // Pretend that a session is already saved before loading replay - WINDOW.sessionStorage.setItem( - REPLAY_SESSION_KEY, - `{"segmentId":0,"id":"fd09adfc4117477abc8de643e5a5798a","sampled":"buffer","started":${BASE_TIMESTAMP},"lastActivity":${BASE_TIMESTAMP},"shouldRefresh":false}`, - ); - const { mockRecord, replay, integration } = await resetSdkMock({ - replayOptions: { - stickySession: true, - }, - sentryOptions: { - replaysOnErrorSampleRate: 1.0, - }, - autoStart: false, + expect(replay).toHaveLastSentReplay({ + recordingData: JSON.stringify([ + { data: { step: steps }, timestamp: BASE_TIMESTAMP + stepDuration * steps, type: 2 }, + optionsEvent, + { data: { step: steps }, timestamp: BASE_TIMESTAMP + stepDuration * steps + 5, type: 3 }, + ]), + replayEventPayload: expect.objectContaining({ + replay_start_timestamp: (BASE_TIMESTAMP + stepDuration * steps) / 1000, + error_ids: [expect.any(String)], + trace_ids: [], + urls: ['http://localhost/'], + replay_id: expect.any(String), + }), + recordingPayloadHeader: { segment_id: 0 }, + }); + }); }); - integration['_initialize'](); - - jest.runAllTimers(); - - await new Promise(process.nextTick); - const TEST_EVENT = getTestEventIncremental({ timestamp: BASE_TIMESTAMP }); - mockRecord._emitter(TEST_EVENT); - expect(replay).not.toHaveLastSentReplay(); + /** + * If an error happens, we switch the recordingMode to `session`, + * but keep `sampled=buffer`. + * This test should verify that if we load such a session from sessionStorage, the session is eventually correctly ended. + */ + it('handles buffer sessions that previously had an error', async () => { + // Pretend that a session is already saved before loading replay + WINDOW.sessionStorage.setItem( + REPLAY_SESSION_KEY, + `{"segmentId":1,"id":"fd09adfc4117477abc8de643e5a5798a","sampled":"buffer","started":${BASE_TIMESTAMP},"lastActivity":${BASE_TIMESTAMP}}`, + ); + const { mockRecord, replay, integration } = await resetSdkMock({ + replayOptions: { + stickySession: true, + }, + sentryOptions: { + replaysOnErrorSampleRate: 1.0, + }, + autoStart: false, + }); + integration['_initialize'](); - // Waiting for max life should eventually stop recording - // We simulate a full checkout which would otherwise be done automatically - for (let i = 0; i < MAX_REPLAY_DURATION / 60_000; i++) { - jest.advanceTimersByTime(60_000); - await new Promise(process.nextTick); - mockRecord.takeFullSnapshot(true); - } + expect(replay.recordingMode).toBe('session'); + const sessionId = replay.getSessionId(); - expect(replay).not.toHaveLastSentReplay(); - expect(replay.isEnabled()).toBe(false); -}); + // Waiting for max life should eventually refresh the session + // We simulate a full checkout which would otherwise be done automatically + for (let i = 0; i < MAX_REPLAY_DURATION / 60_000; i++) { + jest.advanceTimersByTime(60_000); + await new Promise(process.nextTick); + mockRecord.takeFullSnapshot(true); + } -it('handles buffer sessions that never had an error', async () => { - // Pretend that a session is already saved before loading replay - WINDOW.sessionStorage.setItem( - REPLAY_SESSION_KEY, - `{"segmentId":0,"id":"fd09adfc4117477abc8de643e5a5798a","sampled":"buffer","started":${BASE_TIMESTAMP},"lastActivity":${BASE_TIMESTAMP}}`, - ); - const { mockRecord, replay, integration } = await resetSdkMock({ - replayOptions: { - stickySession: true, - }, - sentryOptions: { - replaysOnErrorSampleRate: 1.0, - }, - autoStart: false, + expect(replay.isEnabled()).toBe(true); + // New sessionId indicates that we refreshed the session + expect(replay.getSessionId()).not.toEqual(sessionId); }); - integration['_initialize'](); - - jest.runAllTimers(); - await new Promise(process.nextTick); - const TEST_EVENT = getTestEventIncremental({ timestamp: BASE_TIMESTAMP }); - mockRecord._emitter(TEST_EVENT); + it('handles buffer sessions that never had an error', async () => { + // Pretend that a session is already saved before loading replay + WINDOW.sessionStorage.setItem( + REPLAY_SESSION_KEY, + `{"segmentId":0,"id":"fd09adfc4117477abc8de643e5a5798a","sampled":"buffer","started":${BASE_TIMESTAMP},"lastActivity":${BASE_TIMESTAMP}}`, + ); + const { mockRecord, replay, integration } = await resetSdkMock({ + replayOptions: { + stickySession: true, + }, + sentryOptions: { + replaysOnErrorSampleRate: 1.0, + }, + autoStart: false, + }); + integration['_initialize'](); - expect(replay).not.toHaveLastSentReplay(); + jest.runAllTimers(); - // Waiting for max life should eventually stop recording - // We simulate a full checkout which would otherwise be done automatically - for (let i = 0; i < MAX_REPLAY_DURATION / 60_000; i++) { - jest.advanceTimersByTime(60_000); await new Promise(process.nextTick); - mockRecord.takeFullSnapshot(true); - } + const TEST_EVENT = getTestEventIncremental({ timestamp: BASE_TIMESTAMP }); + mockRecord._emitter(TEST_EVENT); - expect(replay).not.toHaveLastSentReplay(); - expect(replay.isEnabled()).toBe(true); -}); + expect(replay).not.toHaveLastSentReplay(); -/** - * This is testing a case that should only happen with error-only sessions. - * Previously we had assumed that loading a session from session storage meant - * that the session was not new. However, this is not the case with error-only - * sampling since we can load a saved session that did not have an error (and - * thus no replay was created). - */ -it('sends a replay after loading the session from storage', async () => { - // Pretend that a session is already saved before loading replay - WINDOW.sessionStorage.setItem( - REPLAY_SESSION_KEY, - `{"segmentId":0,"id":"fd09adfc4117477abc8de643e5a5798a","sampled":"buffer","started":${BASE_TIMESTAMP},"lastActivity":${BASE_TIMESTAMP}}`, - ); - const { mockRecord, replay, integration } = await resetSdkMock({ - replayOptions: { - stickySession: true, - }, - sentryOptions: { - replaysOnErrorSampleRate: 1.0, - }, - autoStart: false, + // Waiting for max life should eventually stop recording + // We simulate a full checkout which would otherwise be done automatically + for (let i = 0; i < MAX_REPLAY_DURATION / 60_000; i++) { + jest.advanceTimersByTime(60_000); + await new Promise(process.nextTick); + mockRecord.takeFullSnapshot(true); + } + + expect(replay).not.toHaveLastSentReplay(); + expect(replay.isEnabled()).toBe(true); }); - integration['_initialize'](); - const optionsEvent = createOptionsEvent(replay); - jest.runAllTimers(); + /** + * This is testing a case that should only happen with error-only sessions. + * Previously we had assumed that loading a session from session storage meant + * that the session was not new. However, this is not the case with error-only + * sampling since we can load a saved session that did not have an error (and + * thus no replay was created). + */ + it('sends a replay after loading the session from storage', async () => { + // Pretend that a session is already saved before loading replay + WINDOW.sessionStorage.setItem( + REPLAY_SESSION_KEY, + `{"segmentId":0,"id":"fd09adfc4117477abc8de643e5a5798a","sampled":"buffer","started":${BASE_TIMESTAMP},"lastActivity":${BASE_TIMESTAMP}}`, + ); + const { mockRecord, replay, integration } = await resetSdkMock({ + replayOptions: { + stickySession: true, + }, + sentryOptions: { + replaysOnErrorSampleRate: 1.0, + }, + autoStart: false, + }); + integration['_initialize'](); + const optionsEvent = createOptionsEvent(replay); - await new Promise(process.nextTick); - const TEST_EVENT = getTestEventIncremental({ timestamp: BASE_TIMESTAMP }); - mockRecord._emitter(TEST_EVENT); + jest.runAllTimers(); - expect(replay).not.toHaveLastSentReplay(); + await new Promise(process.nextTick); + const TEST_EVENT = getTestEventIncremental({ timestamp: BASE_TIMESTAMP }); + mockRecord._emitter(TEST_EVENT); - captureException(new Error('testing')); + expect(replay).not.toHaveLastSentReplay(); - // 2 ticks to send replay from an error - await waitForBufferFlush(); + captureException(new Error('testing')); - // Buffered events before error - expect(replay).toHaveSentReplay({ - recordingPayloadHeader: { segment_id: 0 }, - recordingData: JSON.stringify([ - { data: { isCheckout: true }, timestamp: BASE_TIMESTAMP, type: 2 }, - optionsEvent, - TEST_EVENT, - ]), - }); + // 2 ticks to send replay from an error + await waitForBufferFlush(); - // `startRecording()` after switching to session mode to continue recording - await waitForFlush(); + // Buffered events before error + expect(replay).toHaveSentReplay({ + recordingPayloadHeader: { segment_id: 0 }, + recordingData: JSON.stringify([ + { data: { isCheckout: true }, timestamp: BASE_TIMESTAMP, type: 2 }, + optionsEvent, + TEST_EVENT, + ]), + }); + + // `startRecording()` after switching to session mode to continue recording + await waitForFlush(); - // Latest checkout when we call `startRecording` again after uploading segment - // after an error occurs (e.g. when we switch to session replay recording) - expect(replay).toHaveLastSentReplay({ - recordingPayloadHeader: { segment_id: 1 }, - recordingData: JSON.stringify([{ data: { isCheckout: true }, timestamp: BASE_TIMESTAMP + 40, type: 2 }]), + // Latest checkout when we call `startRecording` again after uploading segment + // after an error occurs (e.g. when we switch to session replay recording) + expect(replay).toHaveLastSentReplay({ + recordingPayloadHeader: { segment_id: 1 }, + recordingData: JSON.stringify([{ data: { isCheckout: true }, timestamp: BASE_TIMESTAMP + 40, type: 2 }]), + }); }); }); diff --git a/packages/replay/test/integration/session.test.ts b/packages/replay/test/integration/session.test.ts index 80d09124401a..3716c8b33bc7 100644 --- a/packages/replay/test/integration/session.test.ts +++ b/packages/replay/test/integration/session.test.ts @@ -187,6 +187,10 @@ describe('Integration | session', () => { name: 'click', }); + const optionsEvent = createOptionsEvent(replay); + + await new Promise(process.nextTick); + // This is not called because we have to start recording expect(mockRecord.takeFullSnapshot).not.toHaveBeenCalled(); expect(mockRecord).toHaveBeenCalledTimes(2); @@ -197,9 +201,8 @@ describe('Integration | session', () => { // Replay does not send immediately because checkout was due to expired session expect(replay).not.toHaveLastSentReplay(); - const optionsEvent = createOptionsEvent(replay); - await advanceTimers(DEFAULT_FLUSH_MIN_DELAY); + await new Promise(process.nextTick); const newTimestamp = BASE_TIMESTAMP + ELAPSED + 20; @@ -208,20 +211,7 @@ describe('Integration | session', () => { recordingData: JSON.stringify([ { data: { isCheckout: true }, timestamp: newTimestamp, type: 2 }, optionsEvent, - { - type: 5, - timestamp: newTimestamp, - data: { - tag: 'breadcrumb', - payload: { - timestamp: newTimestamp / 1000, - type: 'default', - category: 'ui.click', - message: '', - data: {}, - }, - }, - }, + // the click is lost, but that's OK ]), }); @@ -364,25 +354,29 @@ describe('Integration | session', () => { }); mockRecord._emitter(TEST_EVENT); + const optionsEvent = createOptionsEvent(replay); + const timestampAtRefresh = BASE_TIMESTAMP + ELAPSED; + + jest.runAllTimers(); + await new Promise(process.nextTick); + expect(replay).not.toHaveSameSession(initialSession); - expect(mockRecord.takeFullSnapshot).toHaveBeenCalled(); expect(replay).not.toHaveLastSentReplay(); - // @ts-expect-error private - expect(replay._stopRecording).toBeDefined(); + expect(replay['_stopRecording']).toBeDefined(); // Now do a click domHandler({ name: 'click', }); - const newTimestamp = BASE_TIMESTAMP + ELAPSED; + // 20 is for the process.nextTick + const newTimestamp = timestampAtRefresh + 20; const NEW_TEST_EVENT = getTestEventIncremental({ data: { name: 'test' }, timestamp: newTimestamp + DEFAULT_FLUSH_MIN_DELAY + 20, }); mockRecord._emitter(NEW_TEST_EVENT); - const optionsEvent = createOptionsEvent(replay); jest.runAllTimers(); await advanceTimers(DEFAULT_FLUSH_MIN_DELAY); @@ -414,7 +408,7 @@ describe('Integration | session', () => { expect(replay.getContext()).toEqual( expect.objectContaining({ initialUrl: 'http://dummy/', - initialTimestamp: newTimestamp, + initialTimestamp: timestampAtRefresh, }), ); }); diff --git a/packages/replay/test/unit/session/fetchSession.test.ts b/packages/replay/test/unit/session/fetchSession.test.ts index cf1856e53356..526c9c7969d1 100644 --- a/packages/replay/test/unit/session/fetchSession.test.ts +++ b/packages/replay/test/unit/session/fetchSession.test.ts @@ -28,7 +28,6 @@ describe('Unit | session | fetchSession', () => { segmentId: 0, sampled: 'session', started: 1648827162630, - shouldRefresh: true, }); }); @@ -44,7 +43,6 @@ describe('Unit | session | fetchSession', () => { segmentId: 0, sampled: false, started: 1648827162630, - shouldRefresh: true, }); }); diff --git a/packages/replay/test/unit/session/loadOrCreateSession.test.ts b/packages/replay/test/unit/session/loadOrCreateSession.test.ts index 8f6e7a071c9c..417b9703d479 100644 --- a/packages/replay/test/unit/session/loadOrCreateSession.test.ts +++ b/packages/replay/test/unit/session/loadOrCreateSession.test.ts @@ -13,7 +13,13 @@ jest.mock('@sentry/utils', () => { }; }); -const SAMPLE_OPTIONS: SessionOptions = { +const OPTIONS_STICKY: SessionOptions = { + stickySession: true, + sessionSampleRate: 1.0, + allowBuffering: false, +}; + +const OPTIONS_NON_SICKY: SessionOptions = { stickySession: false, sessionSampleRate: 1.0, allowBuffering: false, @@ -31,7 +37,6 @@ function createMockSession(when: number = Date.now(), id = 'test_session_id') { lastActivity: when, started: when, sampled: 'session', - shouldRefresh: true, }); } @@ -49,16 +54,12 @@ describe('Unit | session | loadOrCreateSession', () => { }); describe('stickySession: false', () => { - it('creates new session if none is passed in', function () { + it('creates new session', function () { const session = loadOrCreateSession( - undefined, { ...DEFAULT_OPTIONS, }, - { - ...SAMPLE_OPTIONS, - stickySession: false, - }, + OPTIONS_NON_SICKY, ); expect(FetchSession.fetchSession).not.toHaveBeenCalled(); @@ -70,7 +71,6 @@ describe('Unit | session | loadOrCreateSession', () => { lastActivity: expect.any(Number), sampled: 'session', started: expect.any(Number), - shouldRefresh: true, }); // Should not have anything in storage @@ -82,15 +82,11 @@ describe('Unit | session | loadOrCreateSession', () => { saveSession(sessionInStorage); const session = loadOrCreateSession( - undefined, { sessionIdleExpire: 1000, maxReplayDuration: MAX_REPLAY_DURATION, }, - { - ...SAMPLE_OPTIONS, - stickySession: false, - }, + OPTIONS_NON_SICKY, ); expect(FetchSession.fetchSession).not.toHaveBeenCalled(); @@ -102,46 +98,42 @@ describe('Unit | session | loadOrCreateSession', () => { lastActivity: expect.any(Number), sampled: 'session', started: expect.any(Number), - shouldRefresh: true, }); // Should not have anything in storage expect(FetchSession.fetchSession()).toEqual(sessionInStorage); }); - it('uses passed in session', function () { - const now = Date.now(); - const currentSession = createMockSession(now - 2000); - + it('uses passed in previousSessionId', function () { const session = loadOrCreateSession( - currentSession, { ...DEFAULT_OPTIONS, + previousSessionId: 'previous_session_id', }, - { - ...SAMPLE_OPTIONS, - stickySession: false, - }, + OPTIONS_NON_SICKY, ); expect(FetchSession.fetchSession).not.toHaveBeenCalled(); - expect(CreateSession.createSession).not.toHaveBeenCalled(); + expect(CreateSession.createSession).toHaveBeenCalled(); - expect(session).toEqual(currentSession); + expect(session).toEqual({ + id: 'test_session_uuid', + segmentId: 0, + lastActivity: expect.any(Number), + sampled: 'session', + started: expect.any(Number), + previousSessionId: 'previous_session_id', + }); }); }); describe('stickySession: true', () => { it('creates new session if none exists', function () { const session = loadOrCreateSession( - undefined, { ...DEFAULT_OPTIONS, }, - { - ...SAMPLE_OPTIONS, - stickySession: true, - }, + OPTIONS_STICKY, ); expect(FetchSession.fetchSession).toHaveBeenCalled(); @@ -153,7 +145,6 @@ describe('Unit | session | loadOrCreateSession', () => { lastActivity: expect.any(Number), sampled: 'session', started: expect.any(Number), - shouldRefresh: true, }; expect(session).toEqual(expectedSession); @@ -167,15 +158,11 @@ describe('Unit | session | loadOrCreateSession', () => { saveSession(createMockSession(date, 'test_old_session_uuid')); const session = loadOrCreateSession( - undefined, { sessionIdleExpire: 1000, maxReplayDuration: MAX_REPLAY_DURATION, }, - { - ...SAMPLE_OPTIONS, - stickySession: true, - }, + OPTIONS_STICKY, ); expect(FetchSession.fetchSession).toHaveBeenCalled(); @@ -187,7 +174,6 @@ describe('Unit | session | loadOrCreateSession', () => { lastActivity: expect.any(Number), sampled: 'session', started: expect.any(Number), - shouldRefresh: true, previousSessionId: 'test_old_session_uuid', }; expect(session).toEqual(expectedSession); @@ -201,15 +187,11 @@ describe('Unit | session | loadOrCreateSession', () => { saveSession(createMockSession(date, 'test_old_session_uuid')); const session = loadOrCreateSession( - undefined, { sessionIdleExpire: 5000, maxReplayDuration: MAX_REPLAY_DURATION, }, - { - ...SAMPLE_OPTIONS, - stickySession: true, - }, + OPTIONS_STICKY, ); expect(FetchSession.fetchSession).toHaveBeenCalled(); @@ -221,31 +203,53 @@ describe('Unit | session | loadOrCreateSession', () => { lastActivity: date, sampled: 'session', started: date, - shouldRefresh: true, }); }); - it('uses passed in session instead of fetching from sessionStorage', function () { + it('ignores previousSessionId when loading from sessionStorage', function () { const now = Date.now(); - saveSession(createMockSession(now - 10000, 'test_storage_session_uuid')); - const currentSession = createMockSession(now - 2000); + const currentSession = createMockSession(now - 10000, 'test_storage_session_uuid'); + saveSession(currentSession); const session = loadOrCreateSession( - currentSession, { ...DEFAULT_OPTIONS, + previousSessionId: 'previous_session_id', }, - { - ...SAMPLE_OPTIONS, - stickySession: true, - }, + OPTIONS_STICKY, ); - expect(FetchSession.fetchSession).not.toHaveBeenCalled(); + expect(FetchSession.fetchSession).toHaveBeenCalled(); expect(CreateSession.createSession).not.toHaveBeenCalled(); expect(session).toEqual(currentSession); }); + + it('uses previousSessionId when creating new session', function () { + const session = loadOrCreateSession( + { + ...DEFAULT_OPTIONS, + previousSessionId: 'previous_session_id', + }, + OPTIONS_STICKY, + ); + + expect(FetchSession.fetchSession).toHaveBeenCalled(); + expect(CreateSession.createSession).toHaveBeenCalled(); + + const expectedSession = { + id: 'test_session_uuid', + segmentId: 0, + lastActivity: expect.any(Number), + sampled: 'session', + started: expect.any(Number), + previousSessionId: 'previous_session_id', + }; + expect(session).toEqual(expectedSession); + + // Should also be stored in storage + expect(FetchSession.fetchSession()).toEqual(expectedSession); + }); }); describe('buffering', () => { @@ -257,81 +261,64 @@ describe('Unit | session | loadOrCreateSession', () => { started: now - 2000, segmentId: 0, sampled: 'buffer', - shouldRefresh: true, }); + saveSession(currentSession); const session = loadOrCreateSession( - currentSession, { sessionIdleExpire: 1000, maxReplayDuration: MAX_REPLAY_DURATION, }, - { - ...SAMPLE_OPTIONS, - }, + OPTIONS_STICKY, ); - expect(FetchSession.fetchSession).not.toHaveBeenCalled(); - expect(CreateSession.createSession).not.toHaveBeenCalled(); - expect(session).toEqual(currentSession); }); - it('returns new unsampled session when buffering & expired, if shouldRefresh===false', function () { + it('returns new session when buffering & expired, if segmentId>0', function () { const now = Date.now(); const currentSession = makeSession({ id: 'test_session_uuid_2', lastActivity: now - 2000, started: now - 2000, - segmentId: 0, + segmentId: 1, sampled: 'buffer', - shouldRefresh: false, }); + saveSession(currentSession); const session = loadOrCreateSession( - currentSession, { sessionIdleExpire: 1000, maxReplayDuration: MAX_REPLAY_DURATION, }, - { - ...SAMPLE_OPTIONS, - }, + OPTIONS_STICKY, ); - expect(FetchSession.fetchSession).not.toHaveBeenCalled(); - expect(CreateSession.createSession).not.toHaveBeenCalled(); - expect(session).not.toEqual(currentSession); - expect(session.sampled).toBe(false); + expect(session.sampled).toBe('session'); expect(session.started).toBeGreaterThanOrEqual(now); + expect(session.segmentId).toBe(0); }); - it('returns existing session when buffering & not expired, if shouldRefresh===false', function () { + it('returns existing session when buffering & not expired, if segmentId>0', function () { const now = Date.now(); const currentSession = makeSession({ id: 'test_session_uuid_2', lastActivity: now - 2000, started: now - 2000, - segmentId: 0, + segmentId: 1, sampled: 'buffer', - shouldRefresh: false, }); + saveSession(currentSession); const session = loadOrCreateSession( - currentSession, { sessionIdleExpire: 5000, maxReplayDuration: MAX_REPLAY_DURATION, }, - { - ...SAMPLE_OPTIONS, - }, + OPTIONS_STICKY, ); - expect(FetchSession.fetchSession).not.toHaveBeenCalled(); - expect(CreateSession.createSession).not.toHaveBeenCalled(); - expect(session).toEqual(currentSession); }); }); @@ -339,12 +326,11 @@ describe('Unit | session | loadOrCreateSession', () => { describe('sampling', () => { it('returns unsampled session if sample rates are 0', function () { const session = loadOrCreateSession( - undefined, { ...DEFAULT_OPTIONS, }, { - ...SAMPLE_OPTIONS, + stickySession: false, sessionSampleRate: 0, allowBuffering: false, }, @@ -356,19 +342,17 @@ describe('Unit | session | loadOrCreateSession', () => { lastActivity: expect.any(Number), sampled: false, started: expect.any(Number), - shouldRefresh: true, }; expect(session).toEqual(expectedSession); }); it('returns `session` session if sessionSampleRate===1', function () { const session = loadOrCreateSession( - undefined, { ...DEFAULT_OPTIONS, }, { - ...SAMPLE_OPTIONS, + stickySession: false, sessionSampleRate: 1.0, allowBuffering: false, }, @@ -379,12 +363,11 @@ describe('Unit | session | loadOrCreateSession', () => { it('returns `buffer` session if allowBuffering===true', function () { const session = loadOrCreateSession( - undefined, { ...DEFAULT_OPTIONS, }, { - ...SAMPLE_OPTIONS, + stickySession: false, sessionSampleRate: 0.0, allowBuffering: true, }, diff --git a/packages/replay/test/unit/session/maybeRefreshSession.test.ts b/packages/replay/test/unit/session/maybeRefreshSession.test.ts deleted file mode 100644 index c4399a5e1188..000000000000 --- a/packages/replay/test/unit/session/maybeRefreshSession.test.ts +++ /dev/null @@ -1,267 +0,0 @@ -import { MAX_REPLAY_DURATION, SESSION_IDLE_EXPIRE_DURATION, WINDOW } from '../../../src/constants'; -import * as CreateSession from '../../../src/session/createSession'; -import { maybeRefreshSession } from '../../../src/session/maybeRefreshSession'; -import { makeSession } from '../../../src/session/Session'; -import type { SessionOptions } from '../../../src/types'; - -jest.mock('@sentry/utils', () => { - return { - ...(jest.requireActual('@sentry/utils') as { string: unknown }), - uuid4: jest.fn(() => 'test_session_uuid'), - }; -}); - -const SAMPLE_OPTIONS: SessionOptions = { - stickySession: false, - sessionSampleRate: 1.0, - allowBuffering: false, -}; - -const DEFAULT_OPTIONS = { - sessionIdleExpire: SESSION_IDLE_EXPIRE_DURATION, - maxReplayDuration: MAX_REPLAY_DURATION, -}; - -function createMockSession(when: number = Date.now(), id = 'test_session_id') { - return makeSession({ - id, - segmentId: 0, - lastActivity: when, - started: when, - sampled: 'session', - shouldRefresh: true, - }); -} - -describe('Unit | session | maybeRefreshSession', () => { - beforeAll(() => { - jest.spyOn(CreateSession, 'createSession'); - }); - - afterEach(() => { - WINDOW.sessionStorage.clear(); - (CreateSession.createSession as jest.MockedFunction).mockClear(); - }); - - it('returns session if not expired', function () { - const now = Date.now(); - const currentSession = createMockSession(now - 2000); - - const session = maybeRefreshSession( - currentSession, - { - ...DEFAULT_OPTIONS, - }, - { - ...SAMPLE_OPTIONS, - }, - ); - - expect(CreateSession.createSession).not.toHaveBeenCalled(); - - expect(session).toEqual(currentSession); - }); - - it('creates new session if expired', function () { - const now = Date.now(); - const currentSession = createMockSession(now - 2000, 'test_old_session_uuid'); - - const session = maybeRefreshSession( - currentSession, - { - sessionIdleExpire: 1000, - maxReplayDuration: MAX_REPLAY_DURATION, - }, - { - ...SAMPLE_OPTIONS, - }, - ); - - expect(CreateSession.createSession).toHaveBeenCalled(); - - expect(session).not.toEqual(currentSession); - const expectedSession = { - id: 'test_session_uuid', - segmentId: 0, - lastActivity: expect.any(Number), - sampled: 'session', - started: expect.any(Number), - shouldRefresh: true, - previousSessionId: 'test_old_session_uuid', - }; - expect(session).toEqual(expectedSession); - expect(session.lastActivity).toBeGreaterThanOrEqual(now); - expect(session.started).toBeGreaterThanOrEqual(now); - }); - - describe('buffering', () => { - it('returns session when buffering, even if expired', function () { - const now = Date.now(); - const currentSession = makeSession({ - id: 'test_session_uuid_2', - lastActivity: now - 2000, - started: now - 2000, - segmentId: 0, - sampled: 'buffer', - shouldRefresh: true, - }); - - const session = maybeRefreshSession( - currentSession, - { - sessionIdleExpire: 1000, - maxReplayDuration: MAX_REPLAY_DURATION, - }, - { - ...SAMPLE_OPTIONS, - }, - ); - - expect(CreateSession.createSession).not.toHaveBeenCalled(); - - expect(session).toEqual(currentSession); - }); - - it('returns new unsampled session when buffering & expired, if shouldRefresh===false', function () { - const now = Date.now(); - const currentSession = makeSession({ - id: 'test_session_uuid_2', - lastActivity: now - 2000, - started: now - 2000, - segmentId: 0, - sampled: 'buffer', - shouldRefresh: false, - }); - - const session = maybeRefreshSession( - currentSession, - { - sessionIdleExpire: 1000, - maxReplayDuration: MAX_REPLAY_DURATION, - }, - { - ...SAMPLE_OPTIONS, - }, - ); - - expect(CreateSession.createSession).not.toHaveBeenCalled(); - - expect(session).not.toEqual(currentSession); - expect(session.sampled).toBe(false); - expect(session.started).toBeGreaterThanOrEqual(now); - }); - - it('returns existing session when buffering & not expired, if shouldRefresh===false', function () { - const now = Date.now(); - const currentSession = makeSession({ - id: 'test_session_uuid_2', - lastActivity: now - 2000, - started: now - 2000, - segmentId: 0, - sampled: 'buffer', - shouldRefresh: false, - }); - - const session = maybeRefreshSession( - currentSession, - { - sessionIdleExpire: 5000, - maxReplayDuration: MAX_REPLAY_DURATION, - }, - { - ...SAMPLE_OPTIONS, - }, - ); - - expect(CreateSession.createSession).not.toHaveBeenCalled(); - - expect(session).toEqual(currentSession); - }); - }); - - describe('sampling', () => { - it('creates unsampled session if sample rates are 0', function () { - const now = Date.now(); - const currentSession = makeSession({ - id: 'test_session_uuid_2', - lastActivity: now - 2000, - started: now - 2000, - segmentId: 0, - sampled: 'session', - shouldRefresh: true, - }); - - const session = maybeRefreshSession( - currentSession, - { - sessionIdleExpire: 1000, - maxReplayDuration: MAX_REPLAY_DURATION, - }, - { - ...SAMPLE_OPTIONS, - sessionSampleRate: 0, - allowBuffering: false, - }, - ); - - expect(session.id).toBe('test_session_uuid'); - expect(session.sampled).toBe(false); - }); - - it('creates `session` session if sessionSampleRate===1', function () { - const now = Date.now(); - const currentSession = makeSession({ - id: 'test_session_uuid_2', - lastActivity: now - 2000, - started: now - 2000, - segmentId: 0, - sampled: 'session', - shouldRefresh: true, - }); - - const session = maybeRefreshSession( - currentSession, - { - sessionIdleExpire: 1000, - maxReplayDuration: MAX_REPLAY_DURATION, - }, - { - ...SAMPLE_OPTIONS, - sessionSampleRate: 1.0, - allowBuffering: false, - }, - ); - - expect(session.id).toBe('test_session_uuid'); - expect(session.sampled).toBe('session'); - }); - - it('creates `buffer` session if allowBuffering===true', function () { - const now = Date.now(); - const currentSession = makeSession({ - id: 'test_session_uuid_2', - lastActivity: now - 2000, - started: now - 2000, - segmentId: 0, - sampled: 'session', - shouldRefresh: true, - }); - - const session = maybeRefreshSession( - currentSession, - { - sessionIdleExpire: 1000, - maxReplayDuration: MAX_REPLAY_DURATION, - }, - { - ...SAMPLE_OPTIONS, - sessionSampleRate: 0.0, - allowBuffering: true, - }, - ); - - expect(session.id).toBe('test_session_uuid'); - expect(session.sampled).toBe('buffer'); - }); - }); -}); From a2ba075e8f88eb438bb1bb82042232d542e634dc Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Mon, 11 Sep 2023 12:56:40 +0200 Subject: [PATCH 19/35] fix(replay): Ensure `handleRecordingEmit` aborts when event is not added (#8938) I noticed that in `handleRecordingEmit`, due to the async nature of `addEvent` it could happen that an event is actually not added (because it is discarded due to timestamp etc.). But we are still updating the initial session timestamp (`[Replay] Updating session start time to earliest event in buffer to...`), as we don't actually abort there. This PR changes this to actually abort `handleRecordingEmit` in this case. I added an `addEventSync` method for this that just returns true/false instead of a promise, which should not change anything there as we haven't been waiting for the result of the promise anyhow. --- packages/replay/src/replay.ts | 5 +- packages/replay/src/util/addEvent.ts | 83 +++++++++++++------ .../replay/src/util/handleRecordingEmit.ts | 21 +++-- .../test/integration/errorSampleRate.test.ts | 70 ---------------- .../replay/test/integration/flush.test.ts | 34 +++----- .../replay/test/unit/util/addEvent.test.ts | 65 ++++++++++++++- .../unit/util/handleRecordingEmit.test.ts | 4 +- 7 files changed, 150 insertions(+), 132 deletions(-) diff --git a/packages/replay/src/replay.ts b/packages/replay/src/replay.ts index aa500382cbc3..4458fe7e349a 100644 --- a/packages/replay/src/replay.ts +++ b/packages/replay/src/replay.ts @@ -38,7 +38,7 @@ import type { Timeouts, } from './types'; import { ReplayEventTypeCustom } from './types'; -import { addEvent } from './util/addEvent'; +import { addEvent, addEventSync } from './util/addEvent'; import { addGlobalListeners } from './util/addGlobalListeners'; import { addMemoryEntry } from './util/addMemoryEntry'; import { createBreadcrumb } from './util/createBreadcrumb'; @@ -666,7 +666,8 @@ export class ReplayContainer implements ReplayContainerInterface { }); this.addUpdate(() => { - void addEvent(this, { + // Return `false` if the event _was_ added, as that means we schedule a flush + return !addEventSync(this, { type: ReplayEventTypeCustom, timestamp: breadcrumb.timestamp || 0, data: { diff --git a/packages/replay/src/util/addEvent.ts b/packages/replay/src/util/addEvent.ts index 9c73f6d87ac2..b5b6287034a9 100644 --- a/packages/replay/src/util/addEvent.ts +++ b/packages/replay/src/util/addEvent.ts @@ -13,39 +13,46 @@ function isCustomEvent(event: RecordingEvent): event is ReplayFrameEvent { /** * Add an event to the event buffer. + * In contrast to `addEvent`, this does not return a promise & does not wait for the adding of the event to succeed/fail. + * Instead this returns `true` if we tried to add the event, else false. + * It returns `false` e.g. if we are paused, disabled, or out of the max replay duration. + * * `isCheckout` is true if this is either the very first event, or an event triggered by `checkoutEveryNms`. */ -export async function addEvent( +export function addEventSync(replay: ReplayContainer, event: RecordingEvent, isCheckout?: boolean): boolean { + if (!shouldAddEvent(replay, event)) { + return false; + } + + void _addEvent(replay, event, isCheckout); + + return true; +} + +/** + * Add an event to the event buffer. + * Resolves to `null` if no event was added, else to `void`. + * + * `isCheckout` is true if this is either the very first event, or an event triggered by `checkoutEveryNms`. + */ +export function addEvent( replay: ReplayContainer, event: RecordingEvent, isCheckout?: boolean, ): Promise { - if (!replay.eventBuffer) { - // This implies that `_isEnabled` is false - return null; - } - - if (replay.isPaused()) { - // Do not add to event buffer when recording is paused - return null; + if (!shouldAddEvent(replay, event)) { + return Promise.resolve(null); } - const timestampInMs = timestampToMs(event.timestamp); - - // Throw out events that happen more than 5 minutes ago. This can happen if - // page has been left open and idle for a long period of time and user - // comes back to trigger a new session. The performance entries rely on - // `performance.timeOrigin`, which is when the page first opened. - if (timestampInMs + replay.timeouts.sessionIdlePause < Date.now()) { - return null; - } + return _addEvent(replay, event, isCheckout); +} - // Throw out events that are +60min from the initial timestamp - if (timestampInMs > replay.getContext().initialTimestamp + replay.getOptions().maxReplayDuration) { - logInfo( - `[Replay] Skipping event with timestamp ${timestampInMs} because it is after maxReplayDuration`, - replay.getOptions()._experiments.traceInternals, - ); +async function _addEvent( + replay: ReplayContainer, + event: RecordingEvent, + isCheckout?: boolean, +): Promise { + if (!replay.eventBuffer) { return null; } @@ -81,6 +88,34 @@ export async function addEvent( } } +/** Exported only for tests. */ +export function shouldAddEvent(replay: ReplayContainer, event: RecordingEvent): boolean { + if (!replay.eventBuffer || replay.isPaused() || !replay.isEnabled()) { + return false; + } + + const timestampInMs = timestampToMs(event.timestamp); + + // Throw out events that happen more than 5 minutes ago. This can happen if + // page has been left open and idle for a long period of time and user + // comes back to trigger a new session. The performance entries rely on + // `performance.timeOrigin`, which is when the page first opened. + if (timestampInMs + replay.timeouts.sessionIdlePause < Date.now()) { + return false; + } + + // Throw out events that are +60min from the initial timestamp + if (timestampInMs > replay.getContext().initialTimestamp + replay.getOptions().maxReplayDuration) { + logInfo( + `[Replay] Skipping event with timestamp ${timestampInMs} because it is after maxReplayDuration`, + replay.getOptions()._experiments.traceInternals, + ); + return false; + } + + return true; +} + function maybeApplyCallback( event: RecordingEvent, callback: ReplayPluginOptions['beforeAddRecordingEvent'], diff --git a/packages/replay/src/util/handleRecordingEmit.ts b/packages/replay/src/util/handleRecordingEmit.ts index f8f00c820ad9..ada70adf980d 100644 --- a/packages/replay/src/util/handleRecordingEmit.ts +++ b/packages/replay/src/util/handleRecordingEmit.ts @@ -2,8 +2,8 @@ import { EventType } from '@sentry-internal/rrweb'; import { logger } from '@sentry/utils'; import { saveSession } from '../session/saveSession'; -import type { AddEventResult, RecordingEvent, ReplayContainer, ReplayOptionFrameEvent } from '../types'; -import { addEvent } from './addEvent'; +import type { RecordingEvent, ReplayContainer, ReplayOptionFrameEvent } from '../types'; +import { addEventSync } from './addEvent'; import { logInfo } from './log'; type RecordingEmitCallback = (event: RecordingEvent, isCheckout?: boolean) => void; @@ -40,9 +40,12 @@ export function getHandleRecordingEmit(replay: ReplayContainer): RecordingEmitCa replay.setInitialState(); } - // We need to clear existing events on a checkout, otherwise they are - // incremental event updates and should be appended - void addEvent(replay, event, isCheckout); + // If the event is not added (e.g. due to being paused, disabled, or out of the max replay duration), + // Skip all further steps + if (!addEventSync(replay, event, isCheckout)) { + // Return true to skip scheduling a debounced flush + return true; + } // Different behavior for full snapshots (type=2), ignore other event types // See https://github.com/rrweb-io/rrweb/blob/d8f9290ca496712aa1e7d472549480c4e7876594/packages/rrweb/src/types.ts#L16 @@ -56,7 +59,7 @@ export function getHandleRecordingEmit(replay: ReplayContainer): RecordingEmitCa // // `isCheckout` is always true, but want to be explicit that it should // only be added for checkouts - void addSettingsEvent(replay, isCheckout); + addSettingsEvent(replay, isCheckout); // If there is a previousSessionId after a full snapshot occurs, then // the replay session was started due to session expiration. The new session @@ -130,11 +133,11 @@ export function createOptionsEvent(replay: ReplayContainer): ReplayOptionFrameEv * Add a "meta" event that contains a simplified view on current configuration * options. This should only be included on the first segment of a recording. */ -function addSettingsEvent(replay: ReplayContainer, isCheckout?: boolean): Promise { +function addSettingsEvent(replay: ReplayContainer, isCheckout?: boolean): void { // Only need to add this event when sending the first segment if (!isCheckout || !replay.session || replay.session.segmentId !== 0) { - return Promise.resolve(null); + return; } - return addEvent(replay, createOptionsEvent(replay), false); + addEventSync(replay, createOptionsEvent(replay), false); } diff --git a/packages/replay/test/integration/errorSampleRate.test.ts b/packages/replay/test/integration/errorSampleRate.test.ts index ad3543ec810a..c5f9c3921123 100644 --- a/packages/replay/test/integration/errorSampleRate.test.ts +++ b/packages/replay/test/integration/errorSampleRate.test.ts @@ -810,76 +810,6 @@ describe('Integration | errorSampleRate', () => { expect(replay).toHaveLastSentReplay(); }); - it('does not refresh replay based on earliest event in buffer', async () => { - jest.setSystemTime(BASE_TIMESTAMP); - - const TEST_EVENT = getTestEventIncremental({ timestamp: BASE_TIMESTAMP - 60000 }); - mockRecord._emitter(TEST_EVENT); - - expect(mockRecord.takeFullSnapshot).not.toHaveBeenCalled(); - expect(replay).not.toHaveLastSentReplay(); - - jest.runAllTimers(); - await new Promise(process.nextTick); - - expect(replay).not.toHaveLastSentReplay(); - captureException(new Error('testing')); - - await waitForBufferFlush(); - - expect(replay).toHaveLastSentReplay(); - - // Flush from calling `stopRecording` - await waitForFlush(); - - // Now wait after session expires - should stop recording - mockRecord.takeFullSnapshot.mockClear(); - (getCurrentHub().getClient()!.getTransport()!.send as unknown as jest.SpyInstance).mockClear(); - - expect(replay).not.toHaveLastSentReplay(); - - const TICKS = 80; - - // We advance time so that we are on the border of expiring, taking into - // account that TEST_EVENT timestamp is 60000 ms before BASE_TIMESTAMP. The - // 3 DEFAULT_FLUSH_MIN_DELAY is to account for the `waitForFlush` that has - // happened, and for the next two that will happen. The first following - // `waitForFlush` does not expire session, but the following one will. - jest.advanceTimersByTime(SESSION_IDLE_EXPIRE_DURATION - 60000 - 3 * DEFAULT_FLUSH_MIN_DELAY - TICKS); - await new Promise(process.nextTick); - - mockRecord._emitter(TEST_EVENT); - expect(replay).not.toHaveLastSentReplay(); - await waitForFlush(); - - expect(replay).not.toHaveLastSentReplay(); - expect(mockRecord.takeFullSnapshot).toHaveBeenCalledTimes(0); - expect(replay.isEnabled()).toBe(true); - - mockRecord._emitter(TEST_EVENT); - expect(replay).not.toHaveLastSentReplay(); - await waitForFlush(); - - expect(replay).not.toHaveLastSentReplay(); - expect(mockRecord.takeFullSnapshot).toHaveBeenCalledTimes(0); - expect(replay.isEnabled()).toBe(true); - - const sessionId = replay.getSessionId(); - - // It's hard to test, but if we advance the below time less 1 ms, it should - // be enabled, but we can't trigger a session check via flush without - // incurring another DEFAULT_FLUSH_MIN_DELAY timeout. - jest.advanceTimersByTime(60000 - DEFAULT_FLUSH_MIN_DELAY); - mockRecord._emitter(TEST_EVENT); - expect(replay).not.toHaveLastSentReplay(); - await waitForFlush(); - - expect(replay).not.toHaveLastSentReplay(); - expect(mockRecord.takeFullSnapshot).toHaveBeenCalledTimes(0); - expect(replay.isEnabled()).toBe(true); - expect(replay.getSessionId()).not.toBe(sessionId); - }); - it('handles very long active buffer session', async () => { const stepDuration = 10_000; const steps = 5_000; diff --git a/packages/replay/test/integration/flush.test.ts b/packages/replay/test/integration/flush.test.ts index 9b02ba980208..d7c8b0033561 100644 --- a/packages/replay/test/integration/flush.test.ts +++ b/packages/replay/test/integration/flush.test.ts @@ -403,6 +403,8 @@ describe('Integration | flush', () => { it('logs warning if adding event that is after maxReplayDuration', async () => { replay.getOptions()._experiments.traceInternals = true; + const spyLogger = jest.spyOn(SentryUtils.logger, 'info'); + sessionStorage.clear(); clearSession(replay); replay['_initializeSessionForSampling'](); @@ -422,32 +424,18 @@ describe('Integration | flush', () => { // no checkout! await advanceTimers(DEFAULT_FLUSH_MIN_DELAY); - expect(mockFlush).toHaveBeenCalledTimes(1); - expect(mockSendReplay).toHaveBeenCalledTimes(1); - - const replayData = mockSendReplay.mock.calls[0][0]; + // No flush is scheduled is aborted because event is after maxReplayDuration + expect(mockFlush).toHaveBeenCalledTimes(0); + expect(mockSendReplay).toHaveBeenCalledTimes(0); - expect(JSON.parse(replayData.recordingData)).toEqual([ - { - type: 5, - timestamp: BASE_TIMESTAMP, - data: { - tag: 'breadcrumb', - payload: { - timestamp: BASE_TIMESTAMP / 1000, - type: 'default', - category: 'console', - data: { logger: 'replay' }, - level: 'info', - message: `[Replay] Skipping event with timestamp ${ - BASE_TIMESTAMP + MAX_REPLAY_DURATION + 100 - } because it is after maxReplayDuration`, - }, - }, - }, - ]); + expect(spyLogger).toHaveBeenLastCalledWith( + `[Replay] Skipping event with timestamp ${ + BASE_TIMESTAMP + MAX_REPLAY_DURATION + 100 + } because it is after maxReplayDuration`, + ); replay.getOptions()._experiments.traceInternals = false; + spyLogger.mockRestore(); }); /** diff --git a/packages/replay/test/unit/util/addEvent.test.ts b/packages/replay/test/unit/util/addEvent.test.ts index ec6c752eb011..8c1d9dea8175 100644 --- a/packages/replay/test/unit/util/addEvent.test.ts +++ b/packages/replay/test/unit/util/addEvent.test.ts @@ -1,9 +1,9 @@ import 'jsdom-worker'; import { BASE_TIMESTAMP } from '../..'; -import { REPLAY_MAX_EVENT_BUFFER_SIZE } from '../../../src/constants'; +import { MAX_REPLAY_DURATION, REPLAY_MAX_EVENT_BUFFER_SIZE, SESSION_IDLE_PAUSE_DURATION } from '../../../src/constants'; import type { EventBufferProxy } from '../../../src/eventBuffer/EventBufferProxy'; -import { addEvent } from '../../../src/util/addEvent'; +import { addEvent, shouldAddEvent } from '../../../src/util/addEvent'; import { getTestEventIncremental } from '../../utils/getTestEvent'; import { setupReplayContainer } from '../../utils/setupReplayContainer'; import { useFakeTimers } from '../../utils/use-fake-timers'; @@ -57,4 +57,65 @@ describe('Unit | util | addEvent', () => { expect(replay.isEnabled()).toEqual(false); }); + + describe('shouldAddEvent', () => { + beforeEach(() => { + jest.setSystemTime(BASE_TIMESTAMP); + }); + + it('returns true by default', () => { + const replay = setupReplayContainer({}); + const event = getTestEventIncremental({ timestamp: BASE_TIMESTAMP }); + + expect(shouldAddEvent(replay, event)).toEqual(true); + }); + + it('returns false when paused', () => { + const replay = setupReplayContainer({}); + const event = getTestEventIncremental({ timestamp: BASE_TIMESTAMP }); + + replay.pause(); + + expect(shouldAddEvent(replay, event)).toEqual(false); + }); + + it('returns false when disabled', async () => { + const replay = setupReplayContainer({}); + const event = getTestEventIncremental({ timestamp: BASE_TIMESTAMP }); + + await replay.stop(); + + expect(shouldAddEvent(replay, event)).toEqual(false); + }); + + it('returns false if there is no eventBuffer', () => { + const replay = setupReplayContainer({}); + const event = getTestEventIncremental({ timestamp: BASE_TIMESTAMP }); + + replay.eventBuffer = null; + + expect(shouldAddEvent(replay, event)).toEqual(false); + }); + + it('returns false when event is too old', () => { + const replay = setupReplayContainer({}); + const event = getTestEventIncremental({ timestamp: BASE_TIMESTAMP - SESSION_IDLE_PAUSE_DURATION - 1 }); + + expect(shouldAddEvent(replay, event)).toEqual(false); + }); + + it('returns false if event is too long after initial timestamp', () => { + const replay = setupReplayContainer({}); + const event = getTestEventIncremental({ timestamp: BASE_TIMESTAMP + MAX_REPLAY_DURATION + 1 }); + + expect(shouldAddEvent(replay, event)).toEqual(false); + }); + + it('returns true if event is withing max duration after after initial timestamp', () => { + const replay = setupReplayContainer({}); + const event = getTestEventIncremental({ timestamp: BASE_TIMESTAMP + MAX_REPLAY_DURATION - 1 }); + + expect(shouldAddEvent(replay, event)).toEqual(true); + }); + }); }); diff --git a/packages/replay/test/unit/util/handleRecordingEmit.test.ts b/packages/replay/test/unit/util/handleRecordingEmit.test.ts index 73cab05a1535..8e22c886a5eb 100644 --- a/packages/replay/test/unit/util/handleRecordingEmit.test.ts +++ b/packages/replay/test/unit/util/handleRecordingEmit.test.ts @@ -16,8 +16,8 @@ describe('Unit | util | handleRecordingEmit', () => { beforeEach(function () { jest.setSystemTime(BASE_TIMESTAMP); - addEventMock = jest.spyOn(SentryAddEvent, 'addEvent').mockImplementation(async () => { - // Do nothing + addEventMock = jest.spyOn(SentryAddEvent, 'addEventSync').mockImplementation(() => { + return true; }); }); From f63036ba6d896f624328c857d2170f8a6843f5e1 Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Mon, 11 Sep 2023 15:05:31 +0200 Subject: [PATCH 20/35] build(lint): Remove unneeded `@ts-expect` comments (#8995) Noticed warnings for this during build. --- packages/replay-worker/src/_worker.ts | 1 - packages/replay-worker/src/handleMessage.ts | 2 -- 2 files changed, 3 deletions(-) diff --git a/packages/replay-worker/src/_worker.ts b/packages/replay-worker/src/_worker.ts index 597ce75bc4a9..b8ee301cfdff 100644 --- a/packages/replay-worker/src/_worker.ts +++ b/packages/replay-worker/src/_worker.ts @@ -3,7 +3,6 @@ import { handleMessage } from './handleMessage'; addEventListener('message', handleMessage); // Immediately send a message when worker loads, so we know the worker is ready -// @ts-expect-error this syntax is actually fine postMessage({ id: undefined, method: 'init', diff --git a/packages/replay-worker/src/handleMessage.ts b/packages/replay-worker/src/handleMessage.ts index bd0e7028eabd..2edeb71c1c22 100644 --- a/packages/replay-worker/src/handleMessage.ts +++ b/packages/replay-worker/src/handleMessage.ts @@ -41,7 +41,6 @@ export function handleMessage(e: MessageEvent): void { try { // @ts-expect-error this syntax is actually fine const response = handlers[method](data); - // @ts-expect-error this syntax is actually fine postMessage({ id, method, @@ -49,7 +48,6 @@ export function handleMessage(e: MessageEvent): void { response, }); } catch (err) { - // @ts-expect-error this syntax is actually fine postMessage({ id, method, From 9e397171d1b360cdd1c926e233014dcc22620fc3 Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Mon, 11 Sep 2023 16:23:43 +0200 Subject: [PATCH 21/35] fix(node-experimental): Ignore outgoing Sentry requests (#8994) We do filter these out in the span processor, but we can avoid all this work by not even generating OTEL spans at all for outgoing Sentry requests. --- packages/node-experimental/src/integrations/http.ts | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/packages/node-experimental/src/integrations/http.ts b/packages/node-experimental/src/integrations/http.ts index d6e6a825d8a1..a64fdca5eab8 100644 --- a/packages/node-experimental/src/integrations/http.ts +++ b/packages/node-experimental/src/integrations/http.ts @@ -93,6 +93,11 @@ export class Http implements Integration { this._unload = registerInstrumentations({ instrumentations: [ new HttpInstrumentation({ + ignoreOutgoingRequestHook: request => { + const host = request.host || request.hostname; + return isSentryHost(host); + }, + requireParentforOutgoingSpans: true, requireParentforIncomingSpans: false, requestHook: (span, req) => { @@ -210,3 +215,11 @@ function getHttpUrl(attributes: Attributes): string | undefined { const url = attributes[SemanticAttributes.HTTP_URL]; return typeof url === 'string' ? url : undefined; } + +/** + * Checks whether given host points to Sentry server + */ +function isSentryHost(host: string | undefined): boolean { + const dsn = getCurrentHub().getClient()?.getDsn(); + return dsn && host ? host.includes(dsn.host) : false; +} From ce84fb36c6df7f8080c2f6d96f47d8a0a63da761 Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Mon, 11 Sep 2023 17:30:27 +0200 Subject: [PATCH 22/35] feat(integration): Ensure `LinkedErrors` integration runs before all event processors (#8956) While looking through our existing integrations, I noticed that the `LinkedErrors` integration in node had some weird/custom code to manually run the context lines integration after it processed, as we cannot guarantee any order etc. I figured it would be much cleaner to solve this with a proper hook (I went with `preprocessEvent`), as it actually makes sense for this to generally run before all other event processors run, IMHO, and we can decouple these integrations from each other. --- .../browser/src/integrations/linkederrors.ts | 40 ++++++-------- packages/core/src/baseclient.ts | 26 ++++++++-- packages/core/src/integration.ts | 16 ++++-- .../node/src/integrations/linkederrors.ts | 51 +++++++----------- packages/types/src/client.ts | 52 +++++++++++++++---- packages/types/src/integration.ts | 7 +++ 6 files changed, 117 insertions(+), 75 deletions(-) diff --git a/packages/browser/src/integrations/linkederrors.ts b/packages/browser/src/integrations/linkederrors.ts index a30a7dc82c34..b07b12c98263 100644 --- a/packages/browser/src/integrations/linkederrors.ts +++ b/packages/browser/src/integrations/linkederrors.ts @@ -1,4 +1,4 @@ -import type { Event, EventHint, EventProcessor, Hub, Integration } from '@sentry/types'; +import type { Client, Event, EventHint, Integration } from '@sentry/types'; import { applyAggregateErrorsToEvent } from '@sentry/utils'; import { exceptionFromError } from '../eventbuilder'; @@ -42,31 +42,25 @@ export class LinkedErrors implements Integration { this._limit = options.limit || DEFAULT_LIMIT; } + /** @inheritdoc */ + public setupOnce(): void { + // noop + } + /** * @inheritDoc */ - public setupOnce(addGlobalEventProcessor: (callback: EventProcessor) => void, getCurrentHub: () => Hub): void { - addGlobalEventProcessor((event: Event, hint?: EventHint) => { - const hub = getCurrentHub(); - const client = hub.getClient(); - const self = hub.getIntegration(LinkedErrors); - - if (!client || !self) { - return event; - } - - const options = client.getOptions(); - applyAggregateErrorsToEvent( - exceptionFromError, - options.stackParser, - options.maxValueLength, - self._key, - self._limit, - event, - hint, - ); + public preprocessEvent(event: Event, hint: EventHint | undefined, client: Client): void { + const options = client.getOptions(); - return event; - }); + applyAggregateErrorsToEvent( + exceptionFromError, + options.stackParser, + options.maxValueLength, + this._key, + this._limit, + event, + hint, + ); } } diff --git a/packages/core/src/baseclient.ts b/packages/core/src/baseclient.ts index 7f22466cc281..1d1ad6aaa377 100644 --- a/packages/core/src/baseclient.ts +++ b/packages/core/src/baseclient.ts @@ -285,7 +285,7 @@ export abstract class BaseClient implements Client { */ public setupIntegrations(): void { if (this._isEnabled() && !this._integrationsInitialized) { - this._integrations = setupIntegrations(this._options.integrations); + this._integrations = setupIntegrations(this, this._options.integrations); this._integrationsInitialized = true; } } @@ -315,7 +315,7 @@ export abstract class BaseClient implements Client { * @inheritDoc */ public addIntegration(integration: Integration): void { - setupIntegration(integration, this._integrations); + setupIntegration(this, integration, this._integrations); } /** @@ -376,9 +376,13 @@ export abstract class BaseClient implements Client { } // Keep on() & emit() signatures in sync with types' client.ts interface + /* eslint-disable @typescript-eslint/unified-signatures */ /** @inheritdoc */ - public on(hook: 'startTransaction' | 'finishTransaction', callback: (transaction: Transaction) => void): void; + public on(hook: 'startTransaction', callback: (transaction: Transaction) => void): void; + + /** @inheritdoc */ + public on(hook: 'finishTransaction', callback: (transaction: Transaction) => void): void; /** @inheritdoc */ public on(hook: 'beforeEnvelope', callback: (envelope: Envelope) => void): void; @@ -386,6 +390,9 @@ export abstract class BaseClient implements Client { /** @inheritdoc */ public on(hook: 'beforeSendEvent', callback: (event: Event, hint?: EventHint) => void): void; + /** @inheritdoc */ + public on(hook: 'preprocessEvent', callback: (event: Event, hint?: EventHint) => void): void; + /** @inheritdoc */ public on( hook: 'afterSendEvent', @@ -412,7 +419,10 @@ export abstract class BaseClient implements Client { } /** @inheritdoc */ - public emit(hook: 'startTransaction' | 'finishTransaction', transaction: Transaction): void; + public emit(hook: 'startTransaction', transaction: Transaction): void; + + /** @inheritdoc */ + public emit(hook: 'finishTransaction', transaction: Transaction): void; /** @inheritdoc */ public emit(hook: 'beforeEnvelope', envelope: Envelope): void; @@ -420,6 +430,9 @@ export abstract class BaseClient implements Client { /** @inheritdoc */ public emit(hook: 'beforeSendEvent', event: Event, hint?: EventHint): void; + /** @inheritdoc */ + public emit(hook: 'preprocessEvent', event: Event, hint?: EventHint): void; + /** @inheritdoc */ public emit(hook: 'afterSendEvent', event: Event, sendResponse: TransportMakeRequestResponse | void): void; @@ -439,6 +452,8 @@ export abstract class BaseClient implements Client { } } + /* eslint-enable @typescript-eslint/unified-signatures */ + /** Updates existing session based on the provided event */ protected _updateSessionFromEvent(session: Session, event: Event): void { let crashed = false; @@ -527,6 +542,9 @@ export abstract class BaseClient implements Client { if (!hint.integrations && integrations.length > 0) { hint.integrations = integrations; } + + this.emit('preprocessEvent', event, hint); + return prepareEvent(options, event, hint, scope).then(evt => { if (evt === null) { return evt; diff --git a/packages/core/src/integration.ts b/packages/core/src/integration.ts index d60bb857bbd5..b2c8e4547ab8 100644 --- a/packages/core/src/integration.ts +++ b/packages/core/src/integration.ts @@ -1,4 +1,4 @@ -import type { Integration, Options } from '@sentry/types'; +import type { Client, Integration, Options } from '@sentry/types'; import { arrayify, logger } from '@sentry/utils'; import { getCurrentHub } from './hub'; @@ -84,13 +84,13 @@ export function getIntegrationsToSetup(options: Options): Integration[] { * @param integrations array of integration instances * @param withDefault should enable default integrations */ -export function setupIntegrations(integrations: Integration[]): IntegrationIndex { +export function setupIntegrations(client: Client, integrations: Integration[]): IntegrationIndex { const integrationIndex: IntegrationIndex = {}; integrations.forEach(integration => { // guard against empty provided integrations if (integration) { - setupIntegration(integration, integrationIndex); + setupIntegration(client, integration, integrationIndex); } }); @@ -98,14 +98,20 @@ export function setupIntegrations(integrations: Integration[]): IntegrationIndex } /** Setup a single integration. */ -export function setupIntegration(integration: Integration, integrationIndex: IntegrationIndex): void { +export function setupIntegration(client: Client, integration: Integration, integrationIndex: IntegrationIndex): void { integrationIndex[integration.name] = integration; if (installedIntegrations.indexOf(integration.name) === -1) { integration.setupOnce(addGlobalEventProcessor, getCurrentHub); installedIntegrations.push(integration.name); - __DEBUG_BUILD__ && logger.log(`Integration installed: ${integration.name}`); } + + if (client.on && typeof integration.preprocessEvent === 'function') { + const callback = integration.preprocessEvent.bind(integration); + client.on('preprocessEvent', (event, hint) => callback(event, hint, client)); + } + + __DEBUG_BUILD__ && logger.log(`Integration installed: ${integration.name}`); } // Polyfill for Array.findIndex(), which is not supported in ES5 diff --git a/packages/node/src/integrations/linkederrors.ts b/packages/node/src/integrations/linkederrors.ts index 610a376f640a..78062f708d6b 100644 --- a/packages/node/src/integrations/linkederrors.ts +++ b/packages/node/src/integrations/linkederrors.ts @@ -1,8 +1,7 @@ -import type { Event, EventHint, EventProcessor, Hub, Integration } from '@sentry/types'; +import type { Client, Event, EventHint, Integration } from '@sentry/types'; import { applyAggregateErrorsToEvent } from '@sentry/utils'; import { exceptionFromError } from '../eventbuilder'; -import { ContextLines } from './contextlines'; const DEFAULT_KEY = 'cause'; const DEFAULT_LIMIT = 5; @@ -37,39 +36,25 @@ export class LinkedErrors implements Integration { this._limit = options.limit || DEFAULT_LIMIT; } + /** @inheritdoc */ + public setupOnce(): void { + // noop + } + /** * @inheritDoc */ - public setupOnce(addGlobalEventProcessor: (callback: EventProcessor) => void, getCurrentHub: () => Hub): void { - addGlobalEventProcessor(async (event: Event, hint?: EventHint) => { - const hub = getCurrentHub(); - const client = hub.getClient(); - const self = hub.getIntegration(LinkedErrors); - - if (!client || !self) { - return event; - } - - const options = client.getOptions(); - - applyAggregateErrorsToEvent( - exceptionFromError, - options.stackParser, - options.maxValueLength, - self._key, - self._limit, - event, - hint, - ); - - // If the ContextLines integration is enabled, we add source code context to linked errors - // because we can't guarantee the order that integrations are run. - const contextLines = getCurrentHub().getIntegration(ContextLines); - if (contextLines) { - await contextLines.addSourceContext(event); - } - - return event; - }); + public preprocessEvent(event: Event, hint: EventHint | undefined, client: Client): void { + const options = client.getOptions(); + + applyAggregateErrorsToEvent( + exceptionFromError, + options.stackParser, + options.maxValueLength, + this._key, + this._limit, + event, + hint, + ); } } diff --git a/packages/types/src/client.ts b/packages/types/src/client.ts index 65da5947ce69..00361d0ada99 100644 --- a/packages/types/src/client.ts +++ b/packages/types/src/client.ts @@ -165,11 +165,19 @@ export interface Client { // HOOKS // TODO(v8): Make the hooks non-optional. + /* eslint-disable @typescript-eslint/unified-signatures */ /** - * Register a callback for transaction start and finish. + * Register a callback for transaction start. + * Receives the transaction as argument. */ - on?(hook: 'startTransaction' | 'finishTransaction', callback: (transaction: Transaction) => void): void; + on?(hook: 'startTransaction', callback: (transaction: Transaction) => void): void; + + /** + * Register a callback for transaction finish. + * Receives the transaction as argument. + */ + on?(hook: 'finishTransaction', callback: (transaction: Transaction) => void): void; /** * Register a callback for transaction start and finish. @@ -177,9 +185,18 @@ export interface Client { on?(hook: 'beforeEnvelope', callback: (envelope: Envelope) => void): void; /** - * Register a callback for before an event is sent. + * Register a callback for before sending an event. + * This is called right before an event is sent and should not be used to mutate the event. + * Receives an Event & EventHint as arguments. */ - on?(hook: 'beforeSendEvent', callback: (event: Event, hint?: EventHint | void) => void): void; + on?(hook: 'beforeSendEvent', callback: (event: Event, hint?: EventHint | undefined) => void): void; + + /** + * Register a callback for preprocessing an event, + * before it is passed to (global) event processors. + * Receives an Event & EventHint as arguments. + */ + on?(hook: 'preprocessEvent', callback: (event: Event, hint?: EventHint | undefined) => void): void; /** * Register a callback for when an event has been sent. @@ -206,10 +223,16 @@ export interface Client { on?(hook: 'otelSpanEnd', callback: (otelSpan: unknown, mutableOptions: { drop: boolean }) => void): void; /** - * Fire a hook event for transaction start and finish. Expects to be given a transaction as the - * second argument. + * Fire a hook event for transaction start. + * Expects to be given a transaction as the second argument. */ - emit?(hook: 'startTransaction' | 'finishTransaction', transaction: Transaction): void; + emit?(hook: 'startTransaction', transaction: Transaction): void; + + /** + * Fire a hook event for transaction finish. + * Expects to be given a transaction as the second argument. + */ + emit?(hook: 'finishTransaction', transaction: Transaction): void; /* * Fire a hook event for envelope creation and sending. Expects to be given an envelope as the @@ -217,12 +240,19 @@ export interface Client { */ emit?(hook: 'beforeEnvelope', envelope: Envelope): void; - /* - * Fire a hook event before sending an event. Expects to be given an Event & EventHint as the - * second/third argument. + /** + * Fire a hook event before sending an event. + * This is called right before an event is sent and should not be used to mutate the event. + * Expects to be given an Event & EventHint as the second/third argument. */ emit?(hook: 'beforeSendEvent', event: Event, hint?: EventHint): void; + /** + * Fire a hook event to process events before they are passed to (global) event processors. + * Expects to be given an Event & EventHint as the second/third argument. + */ + emit?(hook: 'preprocessEvent', event: Event, hint?: EventHint): void; + /* * Fire a hook event after sending an event. Expects to be given an Event as the * second argument. @@ -245,4 +275,6 @@ export interface Client { * The option argument may be mutated to drop the span. */ emit?(hook: 'otelSpanEnd', otelSpan: unknown, mutableOptions: { drop: boolean }): void; + + /* eslint-enable @typescript-eslint/unified-signatures */ } diff --git a/packages/types/src/integration.ts b/packages/types/src/integration.ts index c7672effc185..0c1feae65323 100644 --- a/packages/types/src/integration.ts +++ b/packages/types/src/integration.ts @@ -1,3 +1,5 @@ +import type { Client } from './client'; +import type { Event, EventHint } from './event'; import type { EventProcessor } from './eventprocessor'; import type { Hub } from './hub'; @@ -23,4 +25,9 @@ export interface Integration { * This takes no options on purpose, options should be passed in the constructor */ setupOnce(addGlobalEventProcessor: (callback: EventProcessor) => void, getCurrentHub: () => Hub): void; + + /** + * An optional hook that allows to preprocess an event _before_ it is passed to all other event processors. + */ + preprocessEvent?(event: Event, hint: EventHint | undefined, client: Client): void; } From 595e4e2502d318b257752cdbe78c8e73465ec039 Mon Sep 17 00:00:00 2001 From: Malay Patel <101856674+malay44@users.noreply.github.com> Date: Tue, 12 Sep 2023 00:44:37 +0530 Subject: [PATCH 23/35] feat(redux): Add 'attachReduxState' option (#8953) Co-authored-by: Abhijeet Prasad --- packages/react/src/redux.ts | 26 ++++- packages/react/test/redux.test.ts | 170 ++++++++++++++++++++++++++++++ packages/types/src/context.ts | 8 ++ 3 files changed, 203 insertions(+), 1 deletion(-) diff --git a/packages/react/src/redux.ts b/packages/react/src/redux.ts index 5bac99dbe511..cb90424ba4ad 100644 --- a/packages/react/src/redux.ts +++ b/packages/react/src/redux.ts @@ -1,5 +1,5 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -import { configureScope, getCurrentHub } from '@sentry/browser'; +import { addGlobalEventProcessor, configureScope, getCurrentHub } from '@sentry/browser'; import type { Scope } from '@sentry/types'; import { addNonEnumerableProperty } from '@sentry/utils'; @@ -49,6 +49,12 @@ type StoreEnhancerStoreCreator, StateExt = never> ) => Store, A, StateExt, Ext> & Ext; export interface SentryEnhancerOptions { + /** + * Redux state in attachments or not. + * @default true + */ + attachReduxState?: boolean; + /** * Transforms the state before attaching it to an event. * Use this to remove any private data before sending it to Sentry. @@ -71,6 +77,7 @@ const ACTION_BREADCRUMB_CATEGORY = 'redux.action'; const ACTION_BREADCRUMB_TYPE = 'info'; const defaultOptions: SentryEnhancerOptions = { + attachReduxState: true, actionTransformer: action => action, stateTransformer: state => state || null, }; @@ -89,6 +96,23 @@ function createReduxEnhancer(enhancerOptions?: Partial): return (next: StoreEnhancerStoreCreator): StoreEnhancerStoreCreator => (reducer: Reducer, initialState?: PreloadedState) => { + options.attachReduxState && + addGlobalEventProcessor((event, hint) => { + try { + // @ts-expect-error try catch to reduce bundle size + if (event.type === undefined && event.contexts.state.state.type === 'redux') { + hint.attachments = [ + ...(hint.attachments || []), + // @ts-expect-error try catch to reduce bundle size + { filename: 'redux_state.json', data: JSON.stringify(event.contexts.state.state.value) }, + ]; + } + } catch (_) { + // empty + } + return event; + }); + const sentryReducer: Reducer = (state, action): S => { const newState = reducer(state, action); diff --git a/packages/react/test/redux.test.ts b/packages/react/test/redux.test.ts index bf9fc31853c4..f8260a1dc278 100644 --- a/packages/react/test/redux.test.ts +++ b/packages/react/test/redux.test.ts @@ -14,11 +14,15 @@ jest.mock('@sentry/browser', () => ({ addBreadcrumb: mockAddBreadcrumb, setContext: mockSetContext, }), + addGlobalEventProcessor: jest.fn(), })); +const mockAddGlobalEventProcessor = Sentry.addGlobalEventProcessor as jest.Mock; + afterEach(() => { mockAddBreadcrumb.mockReset(); mockSetContext.mockReset(); + mockAddGlobalEventProcessor.mockReset(); }); describe('createReduxEnhancer', () => { @@ -243,4 +247,170 @@ describe('createReduxEnhancer', () => { value: 'latest', }); }); + + describe('Redux State Attachments', () => { + it('attaches Redux state to Sentry scope', () => { + const enhancer = createReduxEnhancer(); + + const initialState = { + value: 'initial', + }; + + Redux.createStore((state = initialState) => state, enhancer); + + expect(mockAddGlobalEventProcessor).toHaveBeenCalledTimes(1); + + const callbackFunction = mockAddGlobalEventProcessor.mock.calls[0][0]; + + const mockEvent = { + contexts: { + state: { + state: { + type: 'redux', + value: 'UPDATED_VALUE', + }, + }, + }, + }; + + const mockHint = { + attachments: [], + }; + + const result = callbackFunction(mockEvent, mockHint); + + expect(result).toEqual({ + ...mockEvent, + contexts: { + state: { + state: { + type: 'redux', + value: 'UPDATED_VALUE', + }, + }, + }, + }); + + expect(mockHint.attachments).toHaveLength(1); + expect(mockHint.attachments[0]).toEqual({ + filename: 'redux_state.json', + data: JSON.stringify('UPDATED_VALUE'), + }); + }); + + it('does not attach when attachReduxState is false', () => { + const enhancer = createReduxEnhancer({ attachReduxState: false }); + + const initialState = { + value: 'initial', + }; + + Redux.createStore((state = initialState) => state, enhancer); + + expect(mockAddGlobalEventProcessor).toHaveBeenCalledTimes(0); + }); + + it('does not attach when state.type is not redux', () => { + const enhancer = createReduxEnhancer(); + + const initialState = { + value: 'initial', + }; + + Redux.createStore((state = initialState) => state, enhancer); + + expect(mockAddGlobalEventProcessor).toHaveBeenCalledTimes(1); + + const callbackFunction = mockAddGlobalEventProcessor.mock.calls[0][0]; + + const mockEvent = { + contexts: { + state: { + state: { + type: 'not_redux', + value: 'UPDATED_VALUE', + }, + }, + }, + }; + + const mockHint = { + attachments: [], + }; + + const result = callbackFunction(mockEvent, mockHint); + + expect(result).toEqual(mockEvent); + + expect(mockHint.attachments).toHaveLength(0); + }); + + it('does not attach when state is undefined', () => { + const enhancer = createReduxEnhancer(); + + const initialState = { + value: 'initial', + }; + + Redux.createStore((state = initialState) => state, enhancer); + + expect(mockAddGlobalEventProcessor).toHaveBeenCalledTimes(1); + + const callbackFunction = mockAddGlobalEventProcessor.mock.calls[0][0]; + + const mockEvent = { + contexts: { + state: { + state: undefined, + }, + }, + }; + + const mockHint = { + attachments: [], + }; + + const result = callbackFunction(mockEvent, mockHint); + + expect(result).toEqual(mockEvent); + + expect(mockHint.attachments).toHaveLength(0); + }); + + it('does not attach when event type is not undefined', () => { + const enhancer = createReduxEnhancer(); + + const initialState = { + value: 'initial', + }; + + Redux.createStore((state = initialState) => state, enhancer); + + expect(mockAddGlobalEventProcessor).toHaveBeenCalledTimes(1); + + const callbackFunction = mockAddGlobalEventProcessor.mock.calls[0][0]; + + const mockEvent = { + type: 'not_redux', + contexts: { + state: { + state: { + type: 'redux', + value: 'UPDATED_VALUE', + }, + }, + }, + }; + + const mockHint = { + attachments: [], + }; + + const result = callbackFunction(mockEvent, mockHint); + + expect(result).toEqual(mockEvent); + + expect(mockHint.attachments).toHaveLength(0); + }); + }); }); diff --git a/packages/types/src/context.ts b/packages/types/src/context.ts index 110267284fc0..d52b674e2cbe 100644 --- a/packages/types/src/context.ts +++ b/packages/types/src/context.ts @@ -10,6 +10,14 @@ export interface Contexts extends Record { response?: ResponseContext; trace?: TraceContext; cloud_resource?: CloudResourceContext; + state?: StateContext; +} + +export interface StateContext extends Record { + state: { + type: string; + value: Record; + }; } export interface AppContext extends Record { From 3db988f463c8a41c728ac3b4e0a12bf5a5f7379d Mon Sep 17 00:00:00 2001 From: Abhijeet Prasad Date: Mon, 11 Sep 2023 16:29:41 -0400 Subject: [PATCH 24/35] feat(core): Introduce startSpanManual (#8913) Based on https://github.com/getsentry/sentry-javascript/pull/8911 and convos in slack, it was brought up that we might need to expose a method that works similar to `startSpan`, but that does not automatically finish the span at the end of the callback. This is necessary when you have event emitters (`res.once`) or similar. ```ts Sentry.startSpanManual(ctx, (span, finish) => { // do something with span // when you're done, call finish() finish(); }); ``` --- packages/browser/src/exports.ts | 1 + packages/core/src/tracing/index.ts | 2 +- packages/core/src/tracing/trace.ts | 99 +++++++++++++++++++------- packages/node/src/index.ts | 1 + packages/serverless/src/index.ts | 1 + packages/sveltekit/src/server/index.ts | 1 + 6 files changed, 77 insertions(+), 28 deletions(-) diff --git a/packages/browser/src/exports.ts b/packages/browser/src/exports.ts index 659b80bc8962..f46b55f45214 100644 --- a/packages/browser/src/exports.ts +++ b/packages/browser/src/exports.ts @@ -40,6 +40,7 @@ export { getActiveSpan, startSpan, startInactiveSpan, + startSpanManual, SDK_VERSION, setContext, setExtra, diff --git a/packages/core/src/tracing/index.ts b/packages/core/src/tracing/index.ts index c4ed853c5316..7c42aad2a15d 100644 --- a/packages/core/src/tracing/index.ts +++ b/packages/core/src/tracing/index.ts @@ -7,6 +7,6 @@ export { extractTraceparentData, getActiveTransaction } from './utils'; export { SpanStatus } from './spanstatus'; export type { SpanStatusType } from './span'; // eslint-disable-next-line deprecation/deprecation -export { trace, getActiveSpan, startSpan, startInactiveSpan, startActiveSpan } from './trace'; +export { trace, getActiveSpan, startSpan, startInactiveSpan, startActiveSpan, startSpanManual } from './trace'; export { getDynamicSamplingContextFromClient } from './dynamicSamplingContext'; export { setMeasurement } from './measurement'; diff --git a/packages/core/src/tracing/trace.ts b/packages/core/src/tracing/trace.ts index 31671104bd02..8f9b226b4afb 100644 --- a/packages/core/src/tracing/trace.ts +++ b/packages/core/src/tracing/trace.ts @@ -1,6 +1,7 @@ import type { TransactionContext } from '@sentry/types'; import { isThenable } from '@sentry/utils'; +import type { Hub } from '../hub'; import { getCurrentHub } from '../hub'; import { hasTracingEnabled } from '../utils/hasTracingEnabled'; import type { Span } from './span'; @@ -23,25 +24,14 @@ export function trace( // eslint-disable-next-line @typescript-eslint/no-empty-function onError: (error: unknown) => void = () => {}, ): T { - const ctx = { ...context }; - // If a name is set and a description is not, set the description to the name. - if (ctx.name !== undefined && ctx.description === undefined) { - ctx.description = ctx.name; - } + const ctx = normalizeContext(context); const hub = getCurrentHub(); const scope = hub.getScope(); - const parentSpan = scope.getSpan(); - function createChildSpanOrTransaction(): Span | undefined { - if (!hasTracingEnabled()) { - return undefined; - } - return parentSpan ? parentSpan.startChild(ctx) : hub.startTransaction(ctx); - } + const activeSpan = createChildSpanOrTransaction(hub, parentSpan, ctx); - const activeSpan = createChildSpanOrTransaction(); scope.setSpan(activeSpan); function finishAndSetSpan(): void { @@ -89,25 +79,13 @@ export function trace( * and the `span` returned from the callback will be undefined. */ export function startSpan(context: TransactionContext, callback: (span: Span | undefined) => T): T { - const ctx = { ...context }; - // If a name is set and a description is not, set the description to the name. - if (ctx.name !== undefined && ctx.description === undefined) { - ctx.description = ctx.name; - } + const ctx = normalizeContext(context); const hub = getCurrentHub(); const scope = hub.getScope(); - const parentSpan = scope.getSpan(); - function createChildSpanOrTransaction(): Span | undefined { - if (!hasTracingEnabled()) { - return undefined; - } - return parentSpan ? parentSpan.startChild(ctx) : hub.startTransaction(ctx); - } - - const activeSpan = createChildSpanOrTransaction(); + const activeSpan = createChildSpanOrTransaction(hub, parentSpan, ctx); scope.setSpan(activeSpan); function finishAndSetSpan(): void { @@ -146,6 +124,52 @@ export function startSpan(context: TransactionContext, callback: (span: Span */ export const startActiveSpan = startSpan; +/** + * Similar to `Sentry.startSpan`. Wraps a function with a transaction/span, but does not finish the span + * after the function is done automatically. + * + * The created span is the active span and will be used as parent by other spans created inside the function + * and can be accessed via `Sentry.getActiveSpan()`, as long as the function is executed while the scope is active. + * + * Note that if you have not enabled tracing extensions via `addTracingExtensions` + * or you didn't set `tracesSampleRate`, this function will not generate spans + * and the `span` returned from the callback will be undefined. + */ +export function startSpanManual( + context: TransactionContext, + callback: (span: Span | undefined, finish: () => void) => T, +): T { + const ctx = normalizeContext(context); + + const hub = getCurrentHub(); + const scope = hub.getScope(); + const parentSpan = scope.getSpan(); + + const activeSpan = createChildSpanOrTransaction(hub, parentSpan, ctx); + scope.setSpan(activeSpan); + + function finishAndSetSpan(): void { + activeSpan && activeSpan.finish(); + hub.getScope().setSpan(parentSpan); + } + + let maybePromiseResult: T; + try { + maybePromiseResult = callback(activeSpan, finishAndSetSpan); + } catch (e) { + activeSpan && activeSpan.setStatus('internal_error'); + throw e; + } + + if (isThenable(maybePromiseResult)) { + Promise.resolve(maybePromiseResult).then(undefined, () => { + activeSpan && activeSpan.setStatus('internal_error'); + }); + } + + return maybePromiseResult; +} + /** * Creates a span. This span is not set as active, so will not get automatic instrumentation spans * as children or be able to be accessed via `Sentry.getSpan()`. @@ -178,3 +202,24 @@ export function startInactiveSpan(context: TransactionContext): Span | undefined export function getActiveSpan(): Span | undefined { return getCurrentHub().getScope().getSpan(); } + +function createChildSpanOrTransaction( + hub: Hub, + parentSpan: Span | undefined, + ctx: TransactionContext, +): Span | undefined { + if (!hasTracingEnabled()) { + return undefined; + } + return parentSpan ? parentSpan.startChild(ctx) : hub.startTransaction(ctx); +} + +function normalizeContext(context: TransactionContext): TransactionContext { + const ctx = { ...context }; + // If a name is set and a description is not, set the description to the name. + if (ctx.name !== undefined && ctx.description === undefined) { + ctx.description = ctx.name; + } + + return ctx; +} diff --git a/packages/node/src/index.ts b/packages/node/src/index.ts index ab8c82e5a3fd..c7d93ef16463 100644 --- a/packages/node/src/index.ts +++ b/packages/node/src/index.ts @@ -60,6 +60,7 @@ export { // eslint-disable-next-line deprecation/deprecation startActiveSpan, startInactiveSpan, + startSpanManual, } from '@sentry/core'; export type { SpanStatusType } from '@sentry/core'; export { autoDiscoverNodePerformanceMonitoringIntegrations } from './tracing'; diff --git a/packages/serverless/src/index.ts b/packages/serverless/src/index.ts index 3d1a8c7c3aad..a17d0463202d 100644 --- a/packages/serverless/src/index.ts +++ b/packages/serverless/src/index.ts @@ -55,4 +55,5 @@ export { // eslint-disable-next-line deprecation/deprecation startActiveSpan, startInactiveSpan, + startSpanManual, } from '@sentry/node'; diff --git a/packages/sveltekit/src/server/index.ts b/packages/sveltekit/src/server/index.ts index b4a128843217..90c651a41175 100644 --- a/packages/sveltekit/src/server/index.ts +++ b/packages/sveltekit/src/server/index.ts @@ -50,6 +50,7 @@ export { // eslint-disable-next-line deprecation/deprecation startActiveSpan, startInactiveSpan, + startSpanManual, } from '@sentry/node'; // We can still leave this for the carrier init and type exports From 5a42bd99ee17473a39afe97b3cafb6741f985df0 Mon Sep 17 00:00:00 2001 From: Luca Forstner Date: Tue, 12 Sep 2023 11:40:19 +0100 Subject: [PATCH 25/35] fix(nextjs): Add new potential location for Next.js request AsyncLocalStorage (#9006) --- .../app/route-handlers/[param]/edge/route.ts | 2 +- .../app/route-handlers/[param]/error/route.ts | 2 +- .../src/config/loaders/wrappingLoader.ts | 34 +++++++++++++------ 3 files changed, 26 insertions(+), 12 deletions(-) diff --git a/packages/e2e-tests/test-applications/nextjs-app-dir/app/route-handlers/[param]/edge/route.ts b/packages/e2e-tests/test-applications/nextjs-app-dir/app/route-handlers/[param]/edge/route.ts index 8c96a39e5554..a43862231568 100644 --- a/packages/e2e-tests/test-applications/nextjs-app-dir/app/route-handlers/[param]/edge/route.ts +++ b/packages/e2e-tests/test-applications/nextjs-app-dir/app/route-handlers/[param]/edge/route.ts @@ -6,6 +6,6 @@ export async function PATCH() { return NextResponse.json({ name: 'John Doe' }, { status: 401 }); } -export async function DELETE() { +export async function DELETE(): Promise { throw new Error('route-handler-edge-error'); } diff --git a/packages/e2e-tests/test-applications/nextjs-app-dir/app/route-handlers/[param]/error/route.ts b/packages/e2e-tests/test-applications/nextjs-app-dir/app/route-handlers/[param]/error/route.ts index fd50ef5c8a44..e2de561c4783 100644 --- a/packages/e2e-tests/test-applications/nextjs-app-dir/app/route-handlers/[param]/error/route.ts +++ b/packages/e2e-tests/test-applications/nextjs-app-dir/app/route-handlers/[param]/error/route.ts @@ -1,3 +1,3 @@ -export async function PUT() { +export async function PUT(): Promise { throw new Error('route-handler-error'); } diff --git a/packages/nextjs/src/config/loaders/wrappingLoader.ts b/packages/nextjs/src/config/loaders/wrappingLoader.ts index e6452f815184..ebeb6c90294b 100644 --- a/packages/nextjs/src/config/loaders/wrappingLoader.ts +++ b/packages/nextjs/src/config/loaders/wrappingLoader.ts @@ -15,8 +15,8 @@ const SENTRY_WRAPPER_MODULE_NAME = 'sentry-wrapper-module'; // Needs to end in .cjs in order for the `commonjs` plugin to pick it up const WRAPPING_TARGET_MODULE_NAME = '__SENTRY_WRAPPING_TARGET_FILE__.cjs'; -// Non-public API. Can be found here: https://github.com/vercel/next.js/blob/46151dd68b417e7850146d00354f89930d10b43b/packages/next/src/client/components/request-async-storage.ts -const NEXTJS_REQUEST_ASYNC_STORAGE_MODULE_PATH = 'next/dist/client/components/request-async-storage'; +// This module is non-public API and may break +const nextjsRequestAsyncStorageModulePath = getRequestAsyncLocalStorageModule(); const apiWrapperTemplatePath = path.resolve(__dirname, '..', 'templates', 'apiWrapperTemplate.js'); const apiWrapperTemplateCode = fs.readFileSync(apiWrapperTemplatePath, { encoding: 'utf8' }); @@ -27,7 +27,6 @@ const pageWrapperTemplateCode = fs.readFileSync(pageWrapperTemplatePath, { encod const middlewareWrapperTemplatePath = path.resolve(__dirname, '..', 'templates', 'middlewareWrapperTemplate.js'); const middlewareWrapperTemplateCode = fs.readFileSync(middlewareWrapperTemplatePath, { encoding: 'utf8' }); -const requestAsyncStorageModuleExists = moduleExists(NEXTJS_REQUEST_ASYNC_STORAGE_MODULE_PATH); let showedMissingAsyncStorageModuleWarning = false; const sentryInitWrapperTemplatePath = path.resolve(__dirname, '..', 'templates', 'sentryInitWrapperTemplate.js'); @@ -54,13 +53,28 @@ type LoaderOptions = { vercelCronsConfig?: VercelCronsConfig; }; -function moduleExists(id: string): boolean { +function getRequestAsyncLocalStorageModule(): string | undefined { try { - require.resolve(id); - return true; - } catch (e) { - return false; + // Original location of that module + // https://github.com/vercel/next.js/blob/46151dd68b417e7850146d00354f89930d10b43b/packages/next/src/client/components/request-async-storage.ts + const location = 'next/dist/client/components/request-async-storage'; + require.resolve(location); + return location; + } catch { + // noop } + + try { + // Introduced in Next.js 13.4.20 + // https://github.com/vercel/next.js/blob/e1bc270830f2fc2df3542d4ef4c61b916c802df3/packages/next/src/client/components/request-async-storage.external.ts + const location = 'next/dist/client/components/request-async-storage.external'; + require.resolve(location); + return location; + } catch { + // noop + } + + return undefined; } /** @@ -183,10 +197,10 @@ export default function wrappingLoader( templateCode = routeHandlerWrapperTemplateCode; } - if (requestAsyncStorageModuleExists) { + if (nextjsRequestAsyncStorageModulePath !== undefined) { templateCode = templateCode.replace( /__SENTRY_NEXTJS_REQUEST_ASYNC_STORAGE_SHIM__/g, - NEXTJS_REQUEST_ASYNC_STORAGE_MODULE_PATH, + nextjsRequestAsyncStorageModulePath, ); } else { if (!showedMissingAsyncStorageModuleWarning) { From d16f8195b6aeac08949dac6e85ca1e0f84e9b778 Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Tue, 12 Sep 2023 15:26:36 +0200 Subject: [PATCH 26/35] fix(node-experimental): Require parent span for `pg` spans (#8993) --- packages/node-experimental/src/integrations/postgres.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/node-experimental/src/integrations/postgres.ts b/packages/node-experimental/src/integrations/postgres.ts index 0df7ae31d8ae..4ecab8d685f2 100644 --- a/packages/node-experimental/src/integrations/postgres.ts +++ b/packages/node-experimental/src/integrations/postgres.ts @@ -30,6 +30,7 @@ export class Postgres extends NodePerformanceIntegration implements Integr public setupInstrumentation(): void | Instrumentation[] { return [ new PgInstrumentation({ + requireParentSpan: true, requestHook(span) { addOriginToOtelSpan(span, 'auto.db.otel.postgres'); }, From 47d0c8923879facf0702dd3c5cd575f7ae959f41 Mon Sep 17 00:00:00 2001 From: Onur Temizkan Date: Tue, 12 Sep 2023 14:27:06 +0100 Subject: [PATCH 27/35] fix(react): Switch to props in `useRoutes` (#8998) --- .github/workflows/build.yml | 1 + .../react-router-6-use-routes/.gitignore | 29 ++ .../react-router-6-use-routes/.npmrc | 2 + .../react-router-6-use-routes/package.json | 57 ++++ .../playwright.config.ts | 70 +++++ .../public/index.html | 24 ++ .../src/globals.d.ts | 5 + .../react-router-6-use-routes/src/index.tsx | 77 ++++++ .../src/pages/Index.tsx | 24 ++ .../src/pages/User.tsx | 7 + .../src/react-app-env.d.ts | 1 + .../tests/behaviour-test.spec.ts | 254 ++++++++++++++++++ .../tests/fixtures/ReplayRecordingData.ts | 243 +++++++++++++++++ .../react-router-6-use-routes/tsconfig.json | 20 ++ packages/react/src/reactrouterv6.tsx | 56 ++-- 15 files changed, 845 insertions(+), 25 deletions(-) create mode 100644 packages/e2e-tests/test-applications/react-router-6-use-routes/.gitignore create mode 100644 packages/e2e-tests/test-applications/react-router-6-use-routes/.npmrc create mode 100644 packages/e2e-tests/test-applications/react-router-6-use-routes/package.json create mode 100644 packages/e2e-tests/test-applications/react-router-6-use-routes/playwright.config.ts create mode 100644 packages/e2e-tests/test-applications/react-router-6-use-routes/public/index.html create mode 100644 packages/e2e-tests/test-applications/react-router-6-use-routes/src/globals.d.ts create mode 100644 packages/e2e-tests/test-applications/react-router-6-use-routes/src/index.tsx create mode 100644 packages/e2e-tests/test-applications/react-router-6-use-routes/src/pages/Index.tsx create mode 100644 packages/e2e-tests/test-applications/react-router-6-use-routes/src/pages/User.tsx create mode 100644 packages/e2e-tests/test-applications/react-router-6-use-routes/src/react-app-env.d.ts create mode 100644 packages/e2e-tests/test-applications/react-router-6-use-routes/tests/behaviour-test.spec.ts create mode 100644 packages/e2e-tests/test-applications/react-router-6-use-routes/tests/fixtures/ReplayRecordingData.ts create mode 100644 packages/e2e-tests/test-applications/react-router-6-use-routes/tsconfig.json diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 68ecae92b8bd..4a8eb33ad5dd 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -789,6 +789,7 @@ jobs: 'create-remix-app', 'nextjs-app-dir', 'react-create-hash-router', + 'react-router-6-use-routes', 'standard-frontend-react', 'standard-frontend-react-tracing-import', 'sveltekit', diff --git a/packages/e2e-tests/test-applications/react-router-6-use-routes/.gitignore b/packages/e2e-tests/test-applications/react-router-6-use-routes/.gitignore new file mode 100644 index 000000000000..84634c973eeb --- /dev/null +++ b/packages/e2e-tests/test-applications/react-router-6-use-routes/.gitignore @@ -0,0 +1,29 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js + +# testing +/coverage + +# production +/build + +# misc +.DS_Store +.env.local +.env.development.local +.env.test.local +.env.production.local + +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +/test-results/ +/playwright-report/ +/playwright/.cache/ + +!*.d.ts diff --git a/packages/e2e-tests/test-applications/react-router-6-use-routes/.npmrc b/packages/e2e-tests/test-applications/react-router-6-use-routes/.npmrc new file mode 100644 index 000000000000..c6b3ef9b3eaa --- /dev/null +++ b/packages/e2e-tests/test-applications/react-router-6-use-routes/.npmrc @@ -0,0 +1,2 @@ +@sentry:registry=http://localhost:4873 +@sentry-internal:registry=http://localhost:4873 diff --git a/packages/e2e-tests/test-applications/react-router-6-use-routes/package.json b/packages/e2e-tests/test-applications/react-router-6-use-routes/package.json new file mode 100644 index 000000000000..e287bb9c8873 --- /dev/null +++ b/packages/e2e-tests/test-applications/react-router-6-use-routes/package.json @@ -0,0 +1,57 @@ +{ + "name": "react-router-6-e2e-test-app", + "version": "0.1.0", + "private": true, + "dependencies": { + "@sentry/react": "latest || *", + "@testing-library/jest-dom": "5.14.1", + "@testing-library/react": "13.0.0", + "@testing-library/user-event": "13.2.1", + "@types/jest": "27.0.1", + "@types/node": "16.7.13", + "@types/react": "18.0.0", + "@types/react-dom": "18.0.0", + "react": "18.2.0", + "react-dom": "18.2.0", + "react-router-dom": "^6.4.1", + "react-scripts": "5.0.1", + "typescript": "4.9.5", + "web-vitals": "2.1.0" + }, + "scripts": { + "build": "react-scripts build", + "start": "serve -s build", + "test": "playwright test", + "clean": "npx rimraf node_modules,pnpm-lock.yaml", + "test:build": "pnpm install && npx playwright install && pnpm build", + "test:build-ts3.8": "pnpm install && pnpm add typescript@3.8 && npx playwright install && pnpm build", + "test:build-canary": "pnpm install && pnpm add react@canary react-dom@canary && npx playwright install && pnpm build", + "test:assert": "pnpm test" + }, + "eslintConfig": { + "extends": [ + "react-app", + "react-app/jest" + ] + }, + "browserslist": { + "production": [ + ">0.2%", + "not dead", + "not op_mini all" + ], + "development": [ + "last 1 chrome version", + "last 1 firefox version", + "last 1 safari version" + ] + }, + "devDependencies": { + "@playwright/test": "1.26.1", + "axios": "1.1.2", + "serve": "14.0.1" + }, + "volta": { + "extends": "../../package.json" + } +} diff --git a/packages/e2e-tests/test-applications/react-router-6-use-routes/playwright.config.ts b/packages/e2e-tests/test-applications/react-router-6-use-routes/playwright.config.ts new file mode 100644 index 000000000000..c68482378d7a --- /dev/null +++ b/packages/e2e-tests/test-applications/react-router-6-use-routes/playwright.config.ts @@ -0,0 +1,70 @@ +import type { PlaywrightTestConfig } from '@playwright/test'; +import { devices } from '@playwright/test'; + +/** + * See https://playwright.dev/docs/test-configuration. + */ +const config: PlaywrightTestConfig = { + testDir: './tests', + /* Maximum time one test can run for. */ + timeout: 60 * 1000, + expect: { + /** + * Maximum time expect() should wait for the condition to be met. + * For example in `await expect(locator).toHaveText();` + */ + timeout: 5000, + }, + /* Run tests in files in parallel */ + fullyParallel: true, + /* Fail the build on CI if you accidentally left test.only in the source code. */ + forbidOnly: !!process.env.CI, + /* Retry on CI only */ + retries: 0, + /* Opt out of parallel tests on CI. */ + workers: 1, + /* Reporter to use. See https://playwright.dev/docs/test-reporters */ + reporter: 'list', + /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ + use: { + /* Maximum time each action such as `click()` can take. Defaults to 0 (no limit). */ + actionTimeout: 0, + + /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ + trace: 'on-first-retry', + }, + + /* Configure projects for major browsers */ + projects: [ + { + name: 'chromium', + use: { + ...devices['Desktop Chrome'], + }, + }, + // For now we only test Chrome! + // { + // name: 'firefox', + // use: { + // ...devices['Desktop Firefox'], + // }, + // }, + // { + // name: 'webkit', + // use: { + // ...devices['Desktop Safari'], + // }, + // }, + ], + + /* Run your local dev server before starting the tests */ + webServer: { + command: 'pnpm start', + port: 3030, + env: { + PORT: '3030', + }, + }, +}; + +export default config; diff --git a/packages/e2e-tests/test-applications/react-router-6-use-routes/public/index.html b/packages/e2e-tests/test-applications/react-router-6-use-routes/public/index.html new file mode 100644 index 000000000000..39da76522bea --- /dev/null +++ b/packages/e2e-tests/test-applications/react-router-6-use-routes/public/index.html @@ -0,0 +1,24 @@ + + + + + + + + React App + + + +

+ + + diff --git a/packages/e2e-tests/test-applications/react-router-6-use-routes/src/globals.d.ts b/packages/e2e-tests/test-applications/react-router-6-use-routes/src/globals.d.ts new file mode 100644 index 000000000000..ffa61ca49acc --- /dev/null +++ b/packages/e2e-tests/test-applications/react-router-6-use-routes/src/globals.d.ts @@ -0,0 +1,5 @@ +interface Window { + recordedTransactions?: string[]; + capturedExceptionId?: string; + sentryReplayId?: string; +} diff --git a/packages/e2e-tests/test-applications/react-router-6-use-routes/src/index.tsx b/packages/e2e-tests/test-applications/react-router-6-use-routes/src/index.tsx new file mode 100644 index 000000000000..c38730e02ae7 --- /dev/null +++ b/packages/e2e-tests/test-applications/react-router-6-use-routes/src/index.tsx @@ -0,0 +1,77 @@ +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import * as Sentry from '@sentry/react'; +import { + BrowserRouter, + useLocation, + useNavigationType, + createRoutesFromChildren, + matchRoutes, + useRoutes, +} from 'react-router-dom'; +import Index from './pages/Index'; +import User from './pages/User'; + +const replay = new Sentry.Replay(); + +Sentry.init({ + environment: 'qa', // dynamic sampling bias to keep transactions + dsn: process.env.REACT_APP_E2E_TEST_DSN, + integrations: [ + new Sentry.BrowserTracing({ + routingInstrumentation: Sentry.reactRouterV6Instrumentation( + React.useEffect, + useLocation, + useNavigationType, + createRoutesFromChildren, + matchRoutes, + ), + }), + replay, + ], + // We recommend adjusting this value in production, or using tracesSampler + // for finer control + tracesSampleRate: 1.0, + release: 'e2e-test', + + // Always capture replays, so we can test this properly + replaysSessionSampleRate: 1.0, + replaysOnErrorSampleRate: 0.0, +}); + +Object.defineProperty(window, 'sentryReplayId', { + get() { + return replay['_replay'].session.id; + }, +}); + +Sentry.addGlobalEventProcessor(event => { + if ( + event.type === 'transaction' && + (event.contexts?.trace?.op === 'pageload' || event.contexts?.trace?.op === 'navigation') + ) { + const eventId = event.event_id; + if (eventId) { + window.recordedTransactions = window.recordedTransactions || []; + window.recordedTransactions.push(eventId); + } + } + + return event; +}); + +const useSentryRoutes = Sentry.wrapUseRoutes(useRoutes); + +function App() { + return useSentryRoutes([ + { path: '/', element: }, + { path: '/user/:id', element: }, + ]); +} + +const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement); +root.render( + + + , +); diff --git a/packages/e2e-tests/test-applications/react-router-6-use-routes/src/pages/Index.tsx b/packages/e2e-tests/test-applications/react-router-6-use-routes/src/pages/Index.tsx new file mode 100644 index 000000000000..2f683c63ed84 --- /dev/null +++ b/packages/e2e-tests/test-applications/react-router-6-use-routes/src/pages/Index.tsx @@ -0,0 +1,24 @@ +import * as React from 'react'; +import * as Sentry from '@sentry/react'; +import { Link } from 'react-router-dom'; + +const Index = () => { + return ( + <> + { + const eventId = Sentry.captureException(new Error('I am an error!')); + window.capturedExceptionId = eventId; + }} + /> + + navigate + + + ); +}; + +export default Index; diff --git a/packages/e2e-tests/test-applications/react-router-6-use-routes/src/pages/User.tsx b/packages/e2e-tests/test-applications/react-router-6-use-routes/src/pages/User.tsx new file mode 100644 index 000000000000..671455a92fff --- /dev/null +++ b/packages/e2e-tests/test-applications/react-router-6-use-routes/src/pages/User.tsx @@ -0,0 +1,7 @@ +import * as React from 'react'; + +const User = () => { + return

I am a blank page :)

; +}; + +export default User; diff --git a/packages/e2e-tests/test-applications/react-router-6-use-routes/src/react-app-env.d.ts b/packages/e2e-tests/test-applications/react-router-6-use-routes/src/react-app-env.d.ts new file mode 100644 index 000000000000..6431bc5fc6b2 --- /dev/null +++ b/packages/e2e-tests/test-applications/react-router-6-use-routes/src/react-app-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/packages/e2e-tests/test-applications/react-router-6-use-routes/tests/behaviour-test.spec.ts b/packages/e2e-tests/test-applications/react-router-6-use-routes/tests/behaviour-test.spec.ts new file mode 100644 index 000000000000..fb2d291dd70d --- /dev/null +++ b/packages/e2e-tests/test-applications/react-router-6-use-routes/tests/behaviour-test.spec.ts @@ -0,0 +1,254 @@ +import { test, expect } from '@playwright/test'; +import axios, { AxiosError } from 'axios'; +import { ReplayRecordingData } from './fixtures/ReplayRecordingData'; + +const EVENT_POLLING_TIMEOUT = 30_000; + +const authToken = process.env.E2E_TEST_AUTH_TOKEN; +const sentryTestOrgSlug = process.env.E2E_TEST_SENTRY_ORG_SLUG; +const sentryTestProject = process.env.E2E_TEST_SENTRY_TEST_PROJECT; + +test('Sends an exception to Sentry', async ({ page }) => { + await page.goto('/'); + + const exceptionButton = page.locator('id=exception-button'); + await exceptionButton.click(); + + const exceptionIdHandle = await page.waitForFunction(() => window.capturedExceptionId); + const exceptionEventId = await exceptionIdHandle.jsonValue(); + + console.log(`Polling for error eventId: ${exceptionEventId}`); + + await expect + .poll( + async () => { + try { + const response = await axios.get( + `https://sentry.io/api/0/projects/${sentryTestOrgSlug}/${sentryTestProject}/events/${exceptionEventId}/`, + { headers: { Authorization: `Bearer ${authToken}` } }, + ); + return response.status; + } catch (e) { + if (e instanceof AxiosError && e.response) { + if (e.response.status !== 404) { + throw e; + } else { + return e.response.status; + } + } else { + throw e; + } + } + }, + { + timeout: EVENT_POLLING_TIMEOUT, + }, + ) + .toBe(200); +}); + +test('Sends a pageload transaction to Sentry', async ({ page }) => { + await page.goto('/'); + + const recordedTransactionsHandle = await page.waitForFunction(() => { + if (window.recordedTransactions && window.recordedTransactions?.length >= 1) { + return window.recordedTransactions; + } else { + return undefined; + } + }); + const recordedTransactionEventIds = await recordedTransactionsHandle.jsonValue(); + + if (recordedTransactionEventIds === undefined) { + throw new Error("Application didn't record any transaction event IDs."); + } + + let hadPageLoadTransaction = false; + + console.log(`Polling for transaction eventIds: ${JSON.stringify(recordedTransactionEventIds)}`); + + await Promise.all( + recordedTransactionEventIds.map(async transactionEventId => { + await expect + .poll( + async () => { + try { + const response = await axios.get( + `https://sentry.io/api/0/projects/${sentryTestOrgSlug}/${sentryTestProject}/events/${transactionEventId}/`, + { headers: { Authorization: `Bearer ${authToken}` } }, + ); + + if (response.data.contexts.trace.op === 'pageload') { + hadPageLoadTransaction = true; + } + + return response.status; + } catch (e) { + if (e instanceof AxiosError && e.response) { + if (e.response.status !== 404) { + throw e; + } else { + return e.response.status; + } + } else { + throw e; + } + } + }, + { + timeout: EVENT_POLLING_TIMEOUT, + }, + ) + .toBe(200); + }), + ); + + expect(hadPageLoadTransaction).toBe(true); +}); + +test('Sends a navigation transaction to Sentry', async ({ page }) => { + await page.goto('/'); + + // Give pageload transaction time to finish + page.waitForTimeout(4000); + + const linkElement = page.locator('id=navigation'); + await linkElement.click(); + + const recordedTransactionsHandle = await page.waitForFunction(() => { + if (window.recordedTransactions && window.recordedTransactions?.length >= 2) { + return window.recordedTransactions; + } else { + return undefined; + } + }); + const recordedTransactionEventIds = await recordedTransactionsHandle.jsonValue(); + + if (recordedTransactionEventIds === undefined) { + throw new Error("Application didn't record any transaction event IDs."); + } + + let hadPageNavigationTransaction = false; + + console.log(`Polling for transaction eventIds: ${JSON.stringify(recordedTransactionEventIds)}`); + + await Promise.all( + recordedTransactionEventIds.map(async transactionEventId => { + await expect + .poll( + async () => { + try { + const response = await axios.get( + `https://sentry.io/api/0/projects/${sentryTestOrgSlug}/${sentryTestProject}/events/${transactionEventId}/`, + { headers: { Authorization: `Bearer ${authToken}` } }, + ); + + if (response.data.contexts.trace.op === 'navigation') { + hadPageNavigationTransaction = true; + } + + return response.status; + } catch (e) { + if (e instanceof AxiosError && e.response) { + if (e.response.status !== 404) { + throw e; + } else { + return e.response.status; + } + } else { + throw e; + } + } + }, + { + timeout: EVENT_POLLING_TIMEOUT, + }, + ) + .toBe(200); + }), + ); + + expect(hadPageNavigationTransaction).toBe(true); +}); + +test('Sends a Replay recording to Sentry', async ({ browser }) => { + const context = await browser.newContext(); + const page = await context.newPage(); + + await page.goto('/'); + + const replayId = await page.waitForFunction(() => { + return window.sentryReplayId; + }); + + // Wait for replay to be sent + + if (replayId === undefined) { + throw new Error("Application didn't set a replayId"); + } + + console.log(`Polling for replay with ID: ${replayId}`); + + await expect + .poll( + async () => { + try { + const response = await axios.get( + `https://sentry.io/api/0/projects/${sentryTestOrgSlug}/${sentryTestProject}/replays/${replayId}/`, + { headers: { Authorization: `Bearer ${authToken}` } }, + ); + + return response.status; + } catch (e) { + if (e instanceof AxiosError && e.response) { + if (e.response.status !== 404) { + throw e; + } else { + return e.response.status; + } + } else { + throw e; + } + } + }, + { + timeout: EVENT_POLLING_TIMEOUT, + }, + ) + .toBe(200); + + // now fetch the first recording segment + await expect + .poll( + async () => { + try { + const response = await axios.get( + `https://sentry.io/api/0/projects/${sentryTestOrgSlug}/${sentryTestProject}/replays/${replayId}/recording-segments/?cursor=100%3A0%3A1`, + { headers: { Authorization: `Bearer ${authToken}` } }, + ); + + return { + status: response.status, + data: response.data, + }; + } catch (e) { + if (e instanceof AxiosError && e.response) { + if (e.response.status !== 404) { + throw e; + } else { + return e.response.status; + } + } else { + throw e; + } + } + }, + { + timeout: EVENT_POLLING_TIMEOUT, + }, + ) + .toEqual({ + status: 200, + data: ReplayRecordingData, + }); +}); diff --git a/packages/e2e-tests/test-applications/react-router-6-use-routes/tests/fixtures/ReplayRecordingData.ts b/packages/e2e-tests/test-applications/react-router-6-use-routes/tests/fixtures/ReplayRecordingData.ts new file mode 100644 index 000000000000..0da2e1b2e327 --- /dev/null +++ b/packages/e2e-tests/test-applications/react-router-6-use-routes/tests/fixtures/ReplayRecordingData.ts @@ -0,0 +1,243 @@ +import { expect } from '@playwright/test'; + +export const ReplayRecordingData = [ + [ + { + type: 4, + data: { href: expect.stringMatching(/http:\/\/localhost:\d+\//), width: 1280, height: 720 }, + timestamp: expect.any(Number), + }, + { + data: { + payload: { + blockAllMedia: true, + errorSampleRate: 0, + maskAllInputs: true, + maskAllText: true, + networkCaptureBodies: true, + networkDetailHasUrls: false, + networkRequestHasHeaders: true, + networkResponseHasHeaders: true, + sessionSampleRate: 1, + useCompression: false, + useCompressionOption: true, + }, + tag: 'options', + }, + timestamp: expect.any(Number), + type: 5, + }, + { + type: 2, + data: { + node: { + type: 0, + childNodes: [ + { type: 1, name: 'html', publicId: '', systemId: '', id: 2 }, + { + type: 2, + tagName: 'html', + attributes: { lang: 'en' }, + childNodes: [ + { + type: 2, + tagName: 'head', + attributes: {}, + childNodes: [ + { type: 2, tagName: 'meta', attributes: { charset: 'utf-8' }, childNodes: [], id: 5 }, + { + type: 2, + tagName: 'meta', + attributes: { name: 'viewport', content: 'width=device-width,initial-scale=1' }, + childNodes: [], + id: 6, + }, + { + type: 2, + tagName: 'meta', + attributes: { name: 'theme-color', content: '#000000' }, + childNodes: [], + id: 7, + }, + { + type: 2, + tagName: 'title', + attributes: {}, + childNodes: [{ type: 3, textContent: '***** ***', id: 9 }], + id: 8, + }, + ], + id: 4, + }, + { + type: 2, + tagName: 'body', + attributes: {}, + childNodes: [ + { + type: 2, + tagName: 'noscript', + attributes: {}, + childNodes: [{ type: 3, textContent: '*** **** ** ****** ********** ** *** **** ****', id: 12 }], + id: 11, + }, + { type: 2, tagName: 'div', attributes: { id: 'root' }, childNodes: [], id: 13 }, + ], + id: 10, + }, + ], + id: 3, + }, + ], + id: 1, + }, + initialOffset: { left: 0, top: 0 }, + }, + timestamp: expect.any(Number), + }, + { + type: 3, + data: { + source: 0, + texts: [], + attributes: [], + removes: [], + adds: [ + { + parentId: 13, + nextId: null, + node: { + type: 2, + tagName: 'a', + attributes: { id: 'navigation', href: expect.stringMatching(/http:\/\/localhost:\d+\/user\/5/) }, + childNodes: [], + id: 14, + }, + }, + { parentId: 14, nextId: null, node: { type: 3, textContent: '********', id: 15 } }, + { + parentId: 13, + nextId: 14, + node: { + type: 2, + tagName: 'input', + attributes: { type: 'button', id: 'exception-button', value: '******* *********' }, + childNodes: [], + id: 16, + }, + }, + ], + }, + timestamp: expect.any(Number), + }, + { + type: 3, + data: { source: 5, text: 'Capture Exception', isChecked: false, id: 16 }, + timestamp: expect.any(Number), + }, + { + type: 5, + timestamp: expect.any(Number), + data: { + tag: 'performanceSpan', + payload: { + op: 'navigation.navigate', + description: expect.stringMatching(/http:\/\/localhost:\d+\//), + startTimestamp: expect.any(Number), + endTimestamp: expect.any(Number), + data: { + decodedBodySize: expect.any(Number), + encodedBodySize: expect.any(Number), + duration: expect.any(Number), + domInteractive: expect.any(Number), + domContentLoadedEventEnd: expect.any(Number), + domContentLoadedEventStart: expect.any(Number), + loadEventStart: expect.any(Number), + loadEventEnd: expect.any(Number), + domComplete: expect.any(Number), + redirectCount: expect.any(Number), + size: expect.any(Number), + }, + }, + }, + }, + { + type: 5, + timestamp: expect.any(Number), + data: { + tag: 'performanceSpan', + payload: { + op: 'resource.script', + description: expect.stringMatching(/http:\/\/localhost:\d+\/static\/js\/main.(\w+).js/), + startTimestamp: expect.any(Number), + endTimestamp: expect.any(Number), + data: { + decodedBodySize: expect.any(Number), + encodedBodySize: expect.any(Number), + size: expect.any(Number), + }, + }, + }, + }, + { + type: 5, + timestamp: expect.any(Number), + data: { + tag: 'performanceSpan', + payload: { + op: 'largest-contentful-paint', + description: 'largest-contentful-paint', + startTimestamp: expect.any(Number), + endTimestamp: expect.any(Number), + data: { value: expect.any(Number), size: expect.any(Number), nodeId: 16 }, + }, + }, + }, + { + type: 5, + timestamp: expect.any(Number), + data: { + tag: 'performanceSpan', + payload: { + op: 'paint', + description: 'first-paint', + startTimestamp: expect.any(Number), + endTimestamp: expect.any(Number), + }, + }, + }, + { + type: 5, + timestamp: expect.any(Number), + data: { + tag: 'performanceSpan', + payload: { + op: 'paint', + description: 'first-contentful-paint', + startTimestamp: expect.any(Number), + endTimestamp: expect.any(Number), + }, + }, + }, + { + type: 5, + timestamp: expect.any(Number), + data: { + tag: 'performanceSpan', + payload: { + op: 'memory', + description: 'memory', + startTimestamp: expect.any(Number), + endTimestamp: expect.any(Number), + data: { + memory: { + jsHeapSizeLimit: expect.any(Number), + totalJSHeapSize: expect.any(Number), + usedJSHeapSize: expect.any(Number), + }, + }, + }, + }, + }, + ], +]; diff --git a/packages/e2e-tests/test-applications/react-router-6-use-routes/tsconfig.json b/packages/e2e-tests/test-applications/react-router-6-use-routes/tsconfig.json new file mode 100644 index 000000000000..c8df41dcf4b5 --- /dev/null +++ b/packages/e2e-tests/test-applications/react-router-6-use-routes/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "target": "es5", + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "noFallthroughCasesInSwitch": true, + "module": "esnext", + "moduleResolution": "node", + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react" + }, + "include": ["src", "tests"] +} diff --git a/packages/react/src/reactrouterv6.tsx b/packages/react/src/reactrouterv6.tsx index 0ba88c8f958d..bd127ca1010a 100644 --- a/packages/react/src/reactrouterv6.tsx +++ b/packages/react/src/reactrouterv6.tsx @@ -226,36 +226,42 @@ export function wrapUseRoutes(origUseRoutes: UseRoutes): UseRoutes { let isMountRenderPass: boolean = true; - // eslint-disable-next-line react/display-name - return (routes: RouteObject[], locationArg?: Partial | string): React.ReactElement | null => { - const SentryRoutes: React.FC = () => { - const Routes = origUseRoutes(routes, locationArg); + const SentryRoutes: React.FC<{ + children?: React.ReactNode; + routes: RouteObject[]; + locationArg?: Partial | string; + }> = (props: { children?: React.ReactNode; routes: RouteObject[]; locationArg?: Partial | string }) => { + const { routes, locationArg } = props; - const location = _useLocation(); - const navigationType = _useNavigationType(); + const Routes = origUseRoutes(routes, locationArg); - // A value with stable identity to either pick `locationArg` if available or `location` if not - const stableLocationParam = - typeof locationArg === 'string' || (locationArg && locationArg.pathname) - ? (locationArg as { pathname: string }) - : location; - - _useEffect(() => { - const normalizedLocation = - typeof stableLocationParam === 'string' ? { pathname: stableLocationParam } : stableLocationParam; + const location = _useLocation(); + const navigationType = _useNavigationType(); - if (isMountRenderPass) { - updatePageloadTransaction(normalizedLocation, routes); - isMountRenderPass = false; - } else { - handleNavigation(normalizedLocation, routes, navigationType); - } - }, [navigationType, stableLocationParam]); + // A value with stable identity to either pick `locationArg` if available or `location` if not + const stableLocationParam = + typeof locationArg === 'string' || (locationArg && locationArg.pathname) + ? (locationArg as { pathname: string }) + : location; + + _useEffect(() => { + const normalizedLocation = + typeof stableLocationParam === 'string' ? { pathname: stableLocationParam } : stableLocationParam; + + if (isMountRenderPass) { + updatePageloadTransaction(normalizedLocation, routes); + isMountRenderPass = false; + } else { + handleNavigation(normalizedLocation, routes, navigationType); + } + }, [navigationType, stableLocationParam]); - return Routes; - }; + return Routes; + }; - return ; + // eslint-disable-next-line react/display-name + return (routes: RouteObject[], locationArg?: Partial | string): React.ReactElement | null => { + return ; }; } From 868a3cdc1faedfa8c5570e0f5d1e6c210683f7e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kry=C5=A1tof=20Wold=C5=99ich?= <31292499+krystofwoldrich@users.noreply.github.com> Date: Tue, 12 Sep 2023 15:36:52 +0200 Subject: [PATCH 28/35] feat(core): Export `BeforeFinishCallback` type (#8999) --- packages/core/src/tracing/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/core/src/tracing/index.ts b/packages/core/src/tracing/index.ts index 7c42aad2a15d..c5be88f8c350 100644 --- a/packages/core/src/tracing/index.ts +++ b/packages/core/src/tracing/index.ts @@ -1,5 +1,6 @@ export { startIdleTransaction, addTracingExtensions } from './hubextensions'; export { IdleTransaction, TRACING_DEFAULTS } from './idletransaction'; +export type { BeforeFinishCallback } from './idletransaction'; export { Span, spanStatusfromHttpCode } from './span'; export { Transaction } from './transaction'; export { extractTraceparentData, getActiveTransaction } from './utils'; From a7f5911ce2092280dc035cefa48d0bbd2ae10d0a Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Tue, 12 Sep 2023 15:43:38 +0200 Subject: [PATCH 29/35] ref: Avoid unnecessary `hub.getScope()` checks (#9008) We've changed this some time ago so that `hub.getScope()` _always_ returns a scope, so we can actually update our code where we still check for the existence of scope. --- packages/angular/src/tracing.ts | 4 +- .../src/common/utils/edgeWrapperUtils.ts | 2 +- .../src/common/wrapApiHandlerWithSentry.ts | 150 +++++++++--------- .../common/wrapServerComponentWithSentry.ts | 4 +- packages/node/src/handlers.ts | 6 +- packages/react/src/profiler.tsx | 4 +- packages/remix/src/utils/instrumentServer.ts | 41 ++--- .../remix/src/utils/serverAdapters/express.ts | 4 +- .../replay/src/util/addGlobalListeners.ts | 4 +- packages/serverless/src/awsservices.ts | 7 +- packages/serverless/src/google-cloud-grpc.ts | 7 +- packages/serverless/src/google-cloud-http.ts | 7 +- .../src/node/integrations/apollo.ts | 2 +- .../src/node/integrations/graphql.ts | 2 +- .../src/node/integrations/mongo.ts | 2 +- .../src/node/integrations/mysql.ts | 2 +- .../src/node/integrations/postgres.ts | 2 +- 17 files changed, 106 insertions(+), 144 deletions(-) diff --git a/packages/angular/src/tracing.ts b/packages/angular/src/tracing.ts index 852442af66eb..10b3f3f254f1 100644 --- a/packages/angular/src/tracing.ts +++ b/packages/angular/src/tracing.ts @@ -55,9 +55,7 @@ export function getActiveTransaction(): Transaction | undefined { if (currentHub) { const scope = currentHub.getScope(); - if (scope) { - return scope.getTransaction(); - } + return scope.getTransaction(); } return undefined; diff --git a/packages/nextjs/src/common/utils/edgeWrapperUtils.ts b/packages/nextjs/src/common/utils/edgeWrapperUtils.ts index 94ff5ad5bec4..515a382ee4b8 100644 --- a/packages/nextjs/src/common/utils/edgeWrapperUtils.ts +++ b/packages/nextjs/src/common/utils/edgeWrapperUtils.ts @@ -14,7 +14,7 @@ export function withEdgeWrapping( return async function (this: unknown, ...args) { const req = args[0]; const currentScope = getCurrentHub().getScope(); - const prevSpan = currentScope?.getSpan(); + const prevSpan = currentScope.getSpan(); let span: Span | undefined; diff --git a/packages/nextjs/src/common/wrapApiHandlerWithSentry.ts b/packages/nextjs/src/common/wrapApiHandlerWithSentry.ts index 74815e5a209f..2a6120f164aa 100644 --- a/packages/nextjs/src/common/wrapApiHandlerWithSentry.ts +++ b/packages/nextjs/src/common/wrapApiHandlerWithSentry.ts @@ -89,75 +89,73 @@ export function withSentry(apiHandler: NextApiHandler, parameterizedRoute?: stri const currentScope = hub.getScope(); const options = hub.getClient()?.getOptions(); - if (currentScope) { - currentScope.setSDKProcessingMetadata({ request: req }); - - if (hasTracingEnabled(options) && options?.instrumenter === 'sentry') { - const sentryTrace = - req.headers && isString(req.headers['sentry-trace']) ? req.headers['sentry-trace'] : undefined; - const baggage = req.headers?.baggage; - const { traceparentData, dynamicSamplingContext, propagationContext } = tracingContextFromHeaders( - sentryTrace, - baggage, - ); - hub.getScope().setPropagationContext(propagationContext); - - if (__DEBUG_BUILD__ && traceparentData) { - logger.log(`[Tracing] Continuing trace ${traceparentData.traceId}.`); - } + currentScope.setSDKProcessingMetadata({ request: req }); + + if (hasTracingEnabled(options) && options?.instrumenter === 'sentry') { + const sentryTrace = + req.headers && isString(req.headers['sentry-trace']) ? req.headers['sentry-trace'] : undefined; + const baggage = req.headers?.baggage; + const { traceparentData, dynamicSamplingContext, propagationContext } = tracingContextFromHeaders( + sentryTrace, + baggage, + ); + currentScope.setPropagationContext(propagationContext); + + if (__DEBUG_BUILD__ && traceparentData) { + logger.log(`[Tracing] Continuing trace ${traceparentData.traceId}.`); + } - // prefer the parameterized route, if we have it (which we will if we've auto-wrapped the route handler) - let reqPath = parameterizedRoute; - - // If not, fake it by just replacing parameter values with their names, hoping that none of them match either - // each other or any hard-coded parts of the path - if (!reqPath) { - const url = `${req.url}`; - // pull off query string, if any - reqPath = stripUrlQueryAndFragment(url); - // Replace with placeholder - if (req.query) { - for (const [key, value] of Object.entries(req.query)) { - reqPath = reqPath.replace(`${value}`, `[${key}]`); - } + // prefer the parameterized route, if we have it (which we will if we've auto-wrapped the route handler) + let reqPath = parameterizedRoute; + + // If not, fake it by just replacing parameter values with their names, hoping that none of them match either + // each other or any hard-coded parts of the path + if (!reqPath) { + const url = `${req.url}`; + // pull off query string, if any + reqPath = stripUrlQueryAndFragment(url); + // Replace with placeholder + if (req.query) { + for (const [key, value] of Object.entries(req.query)) { + reqPath = reqPath.replace(`${value}`, `[${key}]`); } } + } - const reqMethod = `${(req.method || 'GET').toUpperCase()} `; - - transaction = startTransaction( - { - name: `${reqMethod}${reqPath}`, - op: 'http.server', - origin: 'auto.http.nextjs', - ...traceparentData, - metadata: { - dynamicSamplingContext: traceparentData && !dynamicSamplingContext ? {} : dynamicSamplingContext, - source: 'route', - request: req, - }, + const reqMethod = `${(req.method || 'GET').toUpperCase()} `; + + transaction = startTransaction( + { + name: `${reqMethod}${reqPath}`, + op: 'http.server', + origin: 'auto.http.nextjs', + ...traceparentData, + metadata: { + dynamicSamplingContext: traceparentData && !dynamicSamplingContext ? {} : dynamicSamplingContext, + source: 'route', + request: req, }, - // extra context passed to the `tracesSampler` - { request: req }, - ); - currentScope.setSpan(transaction); - if (platformSupportsStreaming() && !wrappingTarget.__sentry_test_doesnt_support_streaming__) { - autoEndTransactionOnResponseEnd(transaction, res); - } else { - // If we're not on a platform that supports streaming, we're blocking res.end() until the queue is flushed. - // res.json() and res.send() will implicitly call res.end(), so it is enough to wrap res.end(). - - // eslint-disable-next-line @typescript-eslint/unbound-method - const origResEnd = res.end; - res.end = async function (this: unknown, ...args: unknown[]) { - if (transaction) { - await finishTransaction(transaction, res); - await flushQueue(); - } - - origResEnd.apply(this, args); - }; - } + }, + // extra context passed to the `tracesSampler` + { request: req }, + ); + currentScope.setSpan(transaction); + if (platformSupportsStreaming() && !wrappingTarget.__sentry_test_doesnt_support_streaming__) { + autoEndTransactionOnResponseEnd(transaction, res); + } else { + // If we're not on a platform that supports streaming, we're blocking res.end() until the queue is flushed. + // res.json() and res.send() will implicitly call res.end(), so it is enough to wrap res.end(). + + // eslint-disable-next-line @typescript-eslint/unbound-method + const origResEnd = res.end; + res.end = async function (this: unknown, ...args: unknown[]) { + if (transaction) { + await finishTransaction(transaction, res); + await flushQueue(); + } + + origResEnd.apply(this, args); + }; } } @@ -187,21 +185,19 @@ export function withSentry(apiHandler: NextApiHandler, parameterizedRoute?: stri // way to prevent it from actually being reported twice.) const objectifiedErr = objectify(e); - if (currentScope) { - currentScope.addEventProcessor(event => { - addExceptionMechanism(event, { - type: 'instrument', - handled: false, - data: { - wrapped_handler: wrappingTarget.name, - function: 'withSentry', - }, - }); - return event; + currentScope.addEventProcessor(event => { + addExceptionMechanism(event, { + type: 'instrument', + handled: false, + data: { + wrapped_handler: wrappingTarget.name, + function: 'withSentry', + }, }); + return event; + }); - captureException(objectifiedErr); - } + captureException(objectifiedErr); // Because we're going to finish and send the transaction before passing the error onto nextjs, it won't yet // have had a chance to set the status to 500, so unless we do it ourselves now, we'll incorrectly report that diff --git a/packages/nextjs/src/common/wrapServerComponentWithSentry.ts b/packages/nextjs/src/common/wrapServerComponentWithSentry.ts index 31220dcf2c73..b39ca674af6e 100644 --- a/packages/nextjs/src/common/wrapServerComponentWithSentry.ts +++ b/packages/nextjs/src/common/wrapServerComponentWithSentry.ts @@ -51,9 +51,7 @@ export function wrapServerComponentWithSentry any> }, }); - if (currentScope) { - currentScope.setSpan(transaction); - } + currentScope.setSpan(transaction); const handleErrorCase = (e: unknown): void => { if (isNotFoundNavigationError(e)) { diff --git a/packages/node/src/handlers.ts b/packages/node/src/handlers.ts index 885fae1430d8..868cf7d5a6b2 100644 --- a/packages/node/src/handlers.ts +++ b/packages/node/src/handlers.ts @@ -198,10 +198,8 @@ export function requestHandler( const client = currentHub.getClient(); if (isAutoSessionTrackingEnabled(client)) { const scope = currentHub.getScope(); - if (scope) { - // Set `status` of `RequestSession` to Ok, at the beginning of the request - scope.setRequestSession({ status: 'ok' }); - } + // Set `status` of `RequestSession` to Ok, at the beginning of the request + scope.setRequestSession({ status: 'ok' }); } }); diff --git a/packages/react/src/profiler.tsx b/packages/react/src/profiler.tsx index 34aa52092635..9647d34f0fb4 100644 --- a/packages/react/src/profiler.tsx +++ b/packages/react/src/profiler.tsx @@ -217,9 +217,7 @@ export { withProfiler, Profiler, useProfiler }; export function getActiveTransaction(hub: Hub = getCurrentHub()): T | undefined { if (hub) { const scope = hub.getScope(); - if (scope) { - return scope.getTransaction() as T | undefined; - } + return scope.getTransaction() as T | undefined; } return undefined; diff --git a/packages/remix/src/utils/instrumentServer.ts b/packages/remix/src/utils/instrumentServer.ts index 780b5e9df121..f98bdd6bcd24 100644 --- a/packages/remix/src/utils/instrumentServer.ts +++ b/packages/remix/src/utils/instrumentServer.ts @@ -131,11 +131,6 @@ function makeWrappedDocumentRequestFunction( let res: Response; const activeTransaction = getActiveTransaction(); - const currentScope = getCurrentHub().getScope(); - - if (!currentScope) { - return origDocumentRequestFunction.call(this, request, responseStatusCode, responseHeaders, context, loadContext); - } try { const span = activeTransaction?.startChild({ @@ -176,10 +171,6 @@ function makeWrappedDataFunction(origFn: DataFunction, id: string, name: 'action const activeTransaction = getActiveTransaction(); const currentScope = getCurrentHub().getScope(); - if (!currentScope) { - return origFn.call(this, args); - } - try { const span = activeTransaction?.startChild({ op: `function.remix.${name}`, @@ -228,17 +219,15 @@ function getTraceAndBaggage(): { sentryTrace?: string; sentryBaggage?: string } const currentScope = getCurrentHub().getScope(); if (isNodeEnv() && hasTracingEnabled()) { - if (currentScope) { - const span = currentScope.getSpan(); + const span = currentScope.getSpan(); - if (span && transaction) { - const dynamicSamplingContext = transaction.getDynamicSamplingContext(); + if (span && transaction) { + const dynamicSamplingContext = transaction.getDynamicSamplingContext(); - return { - sentryTrace: span.toTraceparent(), - sentryBaggage: dynamicSamplingContextToSentryBaggageHeader(dynamicSamplingContext), - }; - } + return { + sentryTrace: span.toTraceparent(), + sentryBaggage: dynamicSamplingContextToSentryBaggageHeader(dynamicSamplingContext), + }; } } @@ -376,16 +365,14 @@ function wrapRequestHandler(origRequestHandler: RequestHandler, build: ServerBui const url = new URL(request.url); const [name, source] = getTransactionName(routes, url, pkg); - if (scope) { - scope.setSDKProcessingMetadata({ - request: { - ...normalizedRequest, - route: { - path: name, - }, + scope.setSDKProcessingMetadata({ + request: { + ...normalizedRequest, + route: { + path: name, }, - }); - } + }, + }); if (!options || !hasTracingEnabled(options)) { return origRequestHandler.call(this, request, loadContext); diff --git a/packages/remix/src/utils/serverAdapters/express.ts b/packages/remix/src/utils/serverAdapters/express.ts index 742c938f2d06..78affeaaa9ac 100644 --- a/packages/remix/src/utils/serverAdapters/express.ts +++ b/packages/remix/src/utils/serverAdapters/express.ts @@ -61,9 +61,7 @@ function wrapExpressRequestHandler( const options = hub.getClient()?.getOptions(); const scope = hub.getScope(); - if (scope) { - scope.setSDKProcessingMetadata({ request }); - } + scope.setSDKProcessingMetadata({ request }); if (!options || !hasTracingEnabled(options) || !request.url || !request.method) { return origRequestHandler.call(this, req, res, next); diff --git a/packages/replay/src/util/addGlobalListeners.ts b/packages/replay/src/util/addGlobalListeners.ts index c1adbc72e787..6e73b270bb93 100644 --- a/packages/replay/src/util/addGlobalListeners.ts +++ b/packages/replay/src/util/addGlobalListeners.ts @@ -19,9 +19,7 @@ export function addGlobalListeners(replay: ReplayContainer): void { const scope = getCurrentHub().getScope(); const client = getCurrentHub().getClient(); - if (scope) { - scope.addScopeListener(handleScopeListener(replay)); - } + scope.addScopeListener(handleScopeListener(replay)); addInstrumentationHandler('dom', handleDomListener(replay)); addInstrumentationHandler('history', handleHistorySpanListener(replay)); handleNetworkBreadcrumbs(replay); diff --git a/packages/serverless/src/awsservices.ts b/packages/serverless/src/awsservices.ts index f5ba74fd52b2..699ae9c40ab5 100644 --- a/packages/serverless/src/awsservices.ts +++ b/packages/serverless/src/awsservices.ts @@ -1,5 +1,5 @@ import { getCurrentHub } from '@sentry/node'; -import type { Integration, Span, Transaction } from '@sentry/types'; +import type { Integration, Span } from '@sentry/types'; import { fill } from '@sentry/utils'; // 'aws-sdk/global' import is expected to be type-only so it's erased in the final .js file. // When TypeScript compiler is upgraded, use `import type` syntax to explicitly assert that we don't want to load a module here. @@ -56,12 +56,9 @@ function wrapMakeRequest( orig: MakeRequestFunction, ): MakeRequestFunction { return function (this: TService, operation: string, params?: GenericParams, callback?: MakeRequestCallback) { - let transaction: Transaction | undefined; let span: Span | undefined; const scope = getCurrentHub().getScope(); - if (scope) { - transaction = scope.getTransaction(); - } + const transaction = scope.getTransaction(); const req = orig.call(this, operation, params); req.on('afterBuild', () => { if (transaction) { diff --git a/packages/serverless/src/google-cloud-grpc.ts b/packages/serverless/src/google-cloud-grpc.ts index 515579a0d242..7e3810300826 100644 --- a/packages/serverless/src/google-cloud-grpc.ts +++ b/packages/serverless/src/google-cloud-grpc.ts @@ -1,5 +1,5 @@ import { getCurrentHub } from '@sentry/node'; -import type { Integration, Span, Transaction } from '@sentry/types'; +import type { Integration, Span } from '@sentry/types'; import { fill } from '@sentry/utils'; import type { EventEmitter } from 'events'; @@ -107,12 +107,9 @@ function fillGrpcFunction(stub: Stub, serviceIdentifier: string, methodName: str if (typeof ret?.on !== 'function') { return ret; } - let transaction: Transaction | undefined; let span: Span | undefined; const scope = getCurrentHub().getScope(); - if (scope) { - transaction = scope.getTransaction(); - } + const transaction = scope.getTransaction(); if (transaction) { span = transaction.startChild({ description: `${callType} ${methodName}`, diff --git a/packages/serverless/src/google-cloud-http.ts b/packages/serverless/src/google-cloud-http.ts index b781a8c4c9c5..d3ef8646eab7 100644 --- a/packages/serverless/src/google-cloud-http.ts +++ b/packages/serverless/src/google-cloud-http.ts @@ -2,7 +2,7 @@ // When TypeScript compiler is upgraded, use `import type` syntax to explicitly assert that we don't want to load a module here. import type * as common from '@google-cloud/common'; import { getCurrentHub } from '@sentry/node'; -import type { Integration, Span, Transaction } from '@sentry/types'; +import type { Integration, Span } from '@sentry/types'; import { fill } from '@sentry/utils'; type RequestOptions = common.DecorateRequestOptions; @@ -51,12 +51,9 @@ export class GoogleCloudHttp implements Integration { /** Returns a wrapped function that makes a request with tracing enabled */ function wrapRequestFunction(orig: RequestFunction): RequestFunction { return function (this: common.Service, reqOpts: RequestOptions, callback: ResponseCallback): void { - let transaction: Transaction | undefined; let span: Span | undefined; const scope = getCurrentHub().getScope(); - if (scope) { - transaction = scope.getTransaction(); - } + const transaction = scope.getTransaction(); if (transaction) { const httpMethod = reqOpts.method || 'GET'; span = transaction.startChild({ diff --git a/packages/tracing-internal/src/node/integrations/apollo.ts b/packages/tracing-internal/src/node/integrations/apollo.ts index 945cde0d4a7c..e4c64a299925 100644 --- a/packages/tracing-internal/src/node/integrations/apollo.ts +++ b/packages/tracing-internal/src/node/integrations/apollo.ts @@ -188,7 +188,7 @@ function wrapResolver( fill(model[resolverGroupName], resolverName, function (orig: () => unknown | Promise) { return function (this: unknown, ...args: unknown[]) { const scope = getCurrentHub().getScope(); - const parentSpan = scope?.getSpan(); + const parentSpan = scope.getSpan(); const span = parentSpan?.startChild({ description: `${resolverGroupName}.${resolverName}`, op: 'graphql.resolve', diff --git a/packages/tracing-internal/src/node/integrations/graphql.ts b/packages/tracing-internal/src/node/integrations/graphql.ts index fc9bf5aece57..3fdf6e8ede4c 100644 --- a/packages/tracing-internal/src/node/integrations/graphql.ts +++ b/packages/tracing-internal/src/node/integrations/graphql.ts @@ -51,7 +51,7 @@ export class GraphQL implements LazyLoadedIntegration { fill(pkg, 'execute', function (orig: () => void | Promise) { return function (this: unknown, ...args: unknown[]) { const scope = getCurrentHub().getScope(); - const parentSpan = scope?.getSpan(); + const parentSpan = scope.getSpan(); const span = parentSpan?.startChild({ description: 'execute', diff --git a/packages/tracing-internal/src/node/integrations/mongo.ts b/packages/tracing-internal/src/node/integrations/mongo.ts index df460520b87b..a3bc810be7a3 100644 --- a/packages/tracing-internal/src/node/integrations/mongo.ts +++ b/packages/tracing-internal/src/node/integrations/mongo.ts @@ -174,7 +174,7 @@ export class Mongo implements LazyLoadedIntegration { return function (this: unknown, ...args: unknown[]) { const lastArg = args[args.length - 1]; const scope = getCurrentHub().getScope(); - const parentSpan = scope?.getSpan(); + const parentSpan = scope.getSpan(); // Check if the operation was passed a callback. (mapReduce requires a different check, as // its (non-callback) arguments can also be functions.) diff --git a/packages/tracing-internal/src/node/integrations/mysql.ts b/packages/tracing-internal/src/node/integrations/mysql.ts index 8a3fa0166fd5..748c43ec51b2 100644 --- a/packages/tracing-internal/src/node/integrations/mysql.ts +++ b/packages/tracing-internal/src/node/integrations/mysql.ts @@ -103,7 +103,7 @@ export class Mysql implements LazyLoadedIntegration { fill(pkg, 'createQuery', function (orig: () => void) { return function (this: unknown, options: unknown, values: unknown, callback: unknown) { const scope = getCurrentHub().getScope(); - const parentSpan = scope?.getSpan(); + const parentSpan = scope.getSpan(); const span = parentSpan?.startChild({ description: typeof options === 'string' ? options : (options as { sql: string }).sql, diff --git a/packages/tracing-internal/src/node/integrations/postgres.ts b/packages/tracing-internal/src/node/integrations/postgres.ts index 6a92e76b059c..4f3e2a94fc11 100644 --- a/packages/tracing-internal/src/node/integrations/postgres.ts +++ b/packages/tracing-internal/src/node/integrations/postgres.ts @@ -83,7 +83,7 @@ export class Postgres implements LazyLoadedIntegration { fill(Client.prototype, 'query', function (orig: () => void | Promise) { return function (this: PgClientThis, config: unknown, values: unknown, callback: unknown) { const scope = getCurrentHub().getScope(); - const parentSpan = scope?.getSpan(); + const parentSpan = scope.getSpan(); const data: Record = { 'db.system': 'postgresql', From 475c295625e4511b697264dc22252ff78cbab264 Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Tue, 12 Sep 2023 16:11:02 +0200 Subject: [PATCH 30/35] fix(node-otel): Refactor OTEL span reference cleanup (#9000) This PR updates the span reference cleanup to take into account if a span is/may still be referenced somewhere else. Previously, whenever a span finished we removed the reference from the map, to clean up and avoid memory leaks. However, it seems that sometimes spans are ended before a child span is started (at least the hooks may fire in this order). This leads to the potential case where a parent that _should_ exist cannot be found, thus creating a new transaction instead of a span. With this change, we keep more information in our span map, in order to clear sub-spans (=not transactions) only when the root span (=transaction) is finished. --- packages/opentelemetry-node/src/index.ts | 2 +- packages/opentelemetry-node/src/propagator.ts | 4 +- .../opentelemetry-node/src/spanprocessor.ts | 20 +-- .../opentelemetry-node/src/utils/spanData.ts | 2 +- .../opentelemetry-node/src/utils/spanMap.ts | 92 +++++++++++++ .../test/propagator.test.ts | 8 +- .../test/spanprocessor.test.ts | 122 ++++++++++++++++-- 7 files changed, 215 insertions(+), 35 deletions(-) create mode 100644 packages/opentelemetry-node/src/utils/spanMap.ts diff --git a/packages/opentelemetry-node/src/index.ts b/packages/opentelemetry-node/src/index.ts index 24c477a968fd..630acd960059 100644 --- a/packages/opentelemetry-node/src/index.ts +++ b/packages/opentelemetry-node/src/index.ts @@ -1,4 +1,4 @@ -import { getSentrySpan } from './spanprocessor'; +import { getSentrySpan } from './utils/spanMap'; export { SentrySpanProcessor } from './spanprocessor'; export { SentryPropagator } from './propagator'; diff --git a/packages/opentelemetry-node/src/propagator.ts b/packages/opentelemetry-node/src/propagator.ts index c032d776047f..63ca69c98fb7 100644 --- a/packages/opentelemetry-node/src/propagator.ts +++ b/packages/opentelemetry-node/src/propagator.ts @@ -13,7 +13,7 @@ import { SENTRY_TRACE_HEADER, SENTRY_TRACE_PARENT_CONTEXT_KEY, } from './constants'; -import { SENTRY_SPAN_PROCESSOR_MAP } from './spanprocessor'; +import { getSentrySpan } from './utils/spanMap'; /** * Injects and extracts `sentry-trace` and `baggage` headers from carriers. @@ -30,7 +30,7 @@ export class SentryPropagator extends W3CBaggagePropagator { let baggage = propagation.getBaggage(context) || propagation.createBaggage({}); - const span = SENTRY_SPAN_PROCESSOR_MAP.get(spanContext.spanId); + const span = getSentrySpan(spanContext.spanId); if (span) { setter.set(carrier, SENTRY_TRACE_HEADER, span.toTraceparent()); diff --git a/packages/opentelemetry-node/src/spanprocessor.ts b/packages/opentelemetry-node/src/spanprocessor.ts index 7c5873069dbe..7adc0ee31e5f 100644 --- a/packages/opentelemetry-node/src/spanprocessor.ts +++ b/packages/opentelemetry-node/src/spanprocessor.ts @@ -10,18 +10,7 @@ import { SENTRY_DYNAMIC_SAMPLING_CONTEXT_KEY, SENTRY_TRACE_PARENT_CONTEXT_KEY } import { isSentryRequestSpan } from './utils/isSentryRequest'; import { mapOtelStatus } from './utils/mapOtelStatus'; import { parseSpanDescription } from './utils/parseOtelSpanDescription'; - -export const SENTRY_SPAN_PROCESSOR_MAP: Map = new Map(); - -// make sure to remove references in maps, to ensure this can be GCed -function clearSpan(otelSpanId: string): void { - SENTRY_SPAN_PROCESSOR_MAP.delete(otelSpanId); -} - -/** Get a Sentry span for an otel span ID. */ -export function getSentrySpan(otelSpanId: string): SentrySpan | undefined { - return SENTRY_SPAN_PROCESSOR_MAP.get(otelSpanId); -} +import { clearSpan, getSentrySpan, setSentrySpan } from './utils/spanMap'; /** * Converts OpenTelemetry Spans to Sentry Spans and sends them to Sentry via @@ -62,7 +51,7 @@ export class SentrySpanProcessor implements OtelSpanProcessor { // Otel supports having multiple non-nested spans at the same time // so we cannot use hub.getSpan(), as we cannot rely on this being on the current span - const sentryParentSpan = otelParentSpanId && SENTRY_SPAN_PROCESSOR_MAP.get(otelParentSpanId); + const sentryParentSpan = otelParentSpanId && getSentrySpan(otelParentSpanId); if (sentryParentSpan) { const sentryChildSpan = sentryParentSpan.startChild({ @@ -72,7 +61,7 @@ export class SentrySpanProcessor implements OtelSpanProcessor { spanId: otelSpanId, }); - SENTRY_SPAN_PROCESSOR_MAP.set(otelSpanId, sentryChildSpan); + setSentrySpan(otelSpanId, sentryChildSpan); } else { const traceCtx = getTraceData(otelSpan, parentContext); const transaction = getCurrentHub().startTransaction({ @@ -83,7 +72,7 @@ export class SentrySpanProcessor implements OtelSpanProcessor { spanId: otelSpanId, }); - SENTRY_SPAN_PROCESSOR_MAP.set(otelSpanId, transaction); + setSentrySpan(otelSpanId, transaction); } } @@ -97,6 +86,7 @@ export class SentrySpanProcessor implements OtelSpanProcessor { if (!sentrySpan) { __DEBUG_BUILD__ && logger.error(`SentrySpanProcessor could not find span with OTEL-spanId ${otelSpanId} to finish.`); + clearSpan(otelSpanId); return; } diff --git a/packages/opentelemetry-node/src/utils/spanData.ts b/packages/opentelemetry-node/src/utils/spanData.ts index d0e582d5763a..1cdbacf74955 100644 --- a/packages/opentelemetry-node/src/utils/spanData.ts +++ b/packages/opentelemetry-node/src/utils/spanData.ts @@ -2,7 +2,7 @@ import { Transaction } from '@sentry/core'; import type { Context, SpanOrigin } from '@sentry/types'; -import { getSentrySpan } from '../spanprocessor'; +import { getSentrySpan } from './spanMap'; type SentryTags = Record; type SentryData = Record; diff --git a/packages/opentelemetry-node/src/utils/spanMap.ts b/packages/opentelemetry-node/src/utils/spanMap.ts new file mode 100644 index 000000000000..8fe43222e93a --- /dev/null +++ b/packages/opentelemetry-node/src/utils/spanMap.ts @@ -0,0 +1,92 @@ +import type { Span as SentrySpan } from '@sentry/types'; + +interface SpanMapEntry { + sentrySpan: SentrySpan; + ref: SpanRefType; + // These are not direct children, but all spans under the tree of a root span. + subSpans: string[]; +} + +const SPAN_REF_ROOT = Symbol('root'); +const SPAN_REF_CHILD = Symbol('child'); +const SPAN_REF_CHILD_ENDED = Symbol('child_ended'); +type SpanRefType = typeof SPAN_REF_ROOT | typeof SPAN_REF_CHILD | typeof SPAN_REF_CHILD_ENDED; + +/** Exported only for tests. */ +export const SPAN_MAP = new Map(); + +/** + * Get a Sentry span for a given span ID. + */ +export function getSentrySpan(spanId: string): SentrySpan | undefined { + const entry = SPAN_MAP.get(spanId); + return entry ? entry.sentrySpan : undefined; +} + +/** + * Set a Sentry span for a given span ID. + * This is necessary so we can lookup parent spans later. + * We also keep a list of children for root spans only, in order to be able to clean them up together. + */ +export function setSentrySpan(spanId: string, sentrySpan: SentrySpan): void { + let ref: SpanRefType = SPAN_REF_ROOT; + + const rootSpanId = sentrySpan.transaction?.spanId; + + if (rootSpanId && rootSpanId !== spanId) { + const root = SPAN_MAP.get(rootSpanId); + if (root) { + root.subSpans.push(spanId); + ref = SPAN_REF_CHILD; + } + } + + SPAN_MAP.set(spanId, { + sentrySpan, + ref, + subSpans: [], + }); +} + +/** + * Clear references of the given span ID. + */ +export function clearSpan(spanId: string): void { + const entry = SPAN_MAP.get(spanId); + if (!entry) { + return; + } + + const { ref, subSpans } = entry; + + // If this is a child, mark it as ended. + if (ref === SPAN_REF_CHILD) { + entry.ref = SPAN_REF_CHILD_ENDED; + return; + } + + // If this is a root span, clear all (ended) children + if (ref === SPAN_REF_ROOT) { + for (const childId of subSpans) { + const child = SPAN_MAP.get(childId); + if (!child) { + continue; + } + + if (child.ref === SPAN_REF_CHILD_ENDED) { + // if the child has already ended, just clear it + SPAN_MAP.delete(childId); + } else if (child.ref === SPAN_REF_CHILD) { + // If the child has not ended yet, mark it as a root span so it is cleared when it ends. + child.ref = SPAN_REF_ROOT; + } + } + + SPAN_MAP.delete(spanId); + return; + } + + // Generally, `clearSpan` should never be called for ref === SPAN_REF_CHILD_ENDED + // But if it does, just clear the span + SPAN_MAP.delete(spanId); +} diff --git a/packages/opentelemetry-node/test/propagator.test.ts b/packages/opentelemetry-node/test/propagator.test.ts index 8136b81d9b9a..cee113e38e8b 100644 --- a/packages/opentelemetry-node/test/propagator.test.ts +++ b/packages/opentelemetry-node/test/propagator.test.ts @@ -17,7 +17,7 @@ import { SENTRY_TRACE_PARENT_CONTEXT_KEY, } from '../src/constants'; import { SentryPropagator } from '../src/propagator'; -import { SENTRY_SPAN_PROCESSOR_MAP } from '../src/spanprocessor'; +import { setSentrySpan, SPAN_MAP } from '../src/utils/spanMap'; beforeAll(() => { addTracingExtensions(); @@ -51,7 +51,7 @@ describe('SentryPropagator', () => { makeMain(hub); afterEach(() => { - SENTRY_SPAN_PROCESSOR_MAP.clear(); + SPAN_MAP.clear(); }); enum PerfType { @@ -61,12 +61,12 @@ describe('SentryPropagator', () => { function createTransactionAndMaybeSpan(type: PerfType, transactionContext: TransactionContext) { const transaction = new Transaction(transactionContext, hub); - SENTRY_SPAN_PROCESSOR_MAP.set(transaction.spanId, transaction); + setSentrySpan(transaction.spanId, transaction); if (type === PerfType.Span) { // eslint-disable-next-line @typescript-eslint/no-unused-vars const { spanId, ...ctx } = transactionContext; const span = transaction.startChild({ ...ctx, description: transaction.name }); - SENTRY_SPAN_PROCESSOR_MAP.set(span.spanId, span); + setSentrySpan(span.spanId, span); } } diff --git a/packages/opentelemetry-node/test/spanprocessor.test.ts b/packages/opentelemetry-node/test/spanprocessor.test.ts index 695086d9cce2..3b87068c76f7 100644 --- a/packages/opentelemetry-node/test/spanprocessor.test.ts +++ b/packages/opentelemetry-node/test/spanprocessor.test.ts @@ -17,7 +17,8 @@ import { import { NodeClient } from '@sentry/node'; import { resolvedSyncPromise } from '@sentry/utils'; -import { SENTRY_SPAN_PROCESSOR_MAP, SentrySpanProcessor } from '../src/spanprocessor'; +import { SentrySpanProcessor } from '../src/spanprocessor'; +import { clearSpan, getSentrySpan, SPAN_MAP } from '../src/utils/spanMap'; const SENTRY_DSN = 'https://0@0.ingest.sentry.io/0'; @@ -41,6 +42,9 @@ describe('SentrySpanProcessor', () => { let spanProcessor: SentrySpanProcessor; beforeEach(() => { + // To avoid test leakage, clear before each test + SPAN_MAP.clear(); + client = new NodeClient(DEFAULT_NODE_CLIENT_OPTIONS); hub = new Hub(client); makeMain(hub); @@ -56,12 +60,16 @@ describe('SentrySpanProcessor', () => { }); afterEach(async () => { + // Ensure test map is empty! + // Otherwise, we seem to have a leak somewhere... + expect(SPAN_MAP.size).toBe(0); + await provider.forceFlush(); await provider.shutdown(); }); function getSpanForOtelSpan(otelSpan: OtelSpan | OpenTelemetry.Span) { - return SENTRY_SPAN_PROCESSOR_MAP.get(otelSpan.spanContext().spanId); + return getSentrySpan(otelSpan.spanContext().spanId); } function getContext(transaction: Transaction) { @@ -125,6 +133,46 @@ describe('SentrySpanProcessor', () => { }); }); + it('handles a missing parent reference', () => { + const startTimestampMs = 1667381672309; + const endTimestampMs = 1667381672875; + const startTime = otelNumberToHrtime(startTimestampMs); + const endTime = otelNumberToHrtime(endTimestampMs); + + const tracer = provider.getTracer('default'); + + tracer.startActiveSpan('GET /users', parentOtelSpan => { + // We simulate the parent somehow not existing in our internal map + // this can happen if a race condition leads to spans being processed out of order + clearSpan(parentOtelSpan.spanContext().spanId); + + tracer.startActiveSpan('SELECT * FROM users;', { startTime }, child => { + const childOtelSpan = child as OtelSpan; + + // Parent span does not exist... + const sentrySpanTransaction = getSpanForOtelSpan(parentOtelSpan); + expect(sentrySpanTransaction).toBeUndefined(); + + // Span itself exists and is created as transaction + const sentrySpan = getSpanForOtelSpan(childOtelSpan); + expect(sentrySpan).toBeInstanceOf(SentrySpan); + expect(sentrySpan).toBeInstanceOf(Transaction); + expect(sentrySpan?.name).toBe('SELECT * FROM users;'); + expect(sentrySpan?.startTimestamp).toEqual(startTimestampMs / 1000); + expect(sentrySpan?.spanId).toEqual(childOtelSpan.spanContext().spanId); + expect(sentrySpan?.parentSpanId).toEqual(parentOtelSpan.spanContext().spanId); + + expect(hub.getScope().getSpan()).toBeUndefined(); + + child.end(endTime); + + expect(sentrySpan?.endTimestamp).toEqual(endTimestampMs / 1000); + }); + + parentOtelSpan.end(); + }); + }); + it('allows to create multiple child spans on same level', () => { const tracer = provider.getTracer('default'); @@ -159,6 +207,57 @@ describe('SentrySpanProcessor', () => { }); }); + it('handles child spans finished out of order', async () => { + const tracer = provider.getTracer('default'); + + tracer.startActiveSpan('GET /users', parent => { + tracer.startActiveSpan('SELECT * FROM users;', child => { + const grandchild = tracer.startSpan('child 1'); + + const parentSpan = getSpanForOtelSpan(parent); + const childSpan = getSpanForOtelSpan(child); + const grandchildSpan = getSpanForOtelSpan(grandchild); + + parent.end(); + child.end(); + grandchild.end(); + + expect(parentSpan).toBeDefined(); + expect(childSpan).toBeDefined(); + expect(grandchildSpan).toBeDefined(); + + expect(parentSpan?.endTimestamp).toBeDefined(); + expect(childSpan?.endTimestamp).toBeDefined(); + expect(grandchildSpan?.endTimestamp).toBeDefined(); + }); + }); + }); + + it('handles finished parent span before child span starts', async () => { + const tracer = provider.getTracer('default'); + + tracer.startActiveSpan('GET /users', parent => { + const parentSpan = getSpanForOtelSpan(parent); + + parent.end(); + + tracer.startActiveSpan('SELECT * FROM users;', child => { + const childSpan = getSpanForOtelSpan(child); + + child.end(); + + expect(parentSpan).toBeDefined(); + expect(childSpan).toBeDefined(); + expect(parentSpan).toBeInstanceOf(Transaction); + expect(childSpan).toBeInstanceOf(Transaction); + expect(parentSpan?.endTimestamp).toBeDefined(); + expect(childSpan?.endTimestamp).toBeDefined(); + expect(parentSpan?.parentSpanId).toBeUndefined(); + expect(childSpan?.parentSpanId).toEqual(parentSpan?.spanId); + }); + }); + }); + it('sets context for transaction', async () => { const otelSpan = provider.getTracer('default').startSpan('GET /users'); @@ -704,7 +803,7 @@ describe('SentrySpanProcessor', () => { it('does not finish spans for Sentry request', async () => { const tracer = provider.getTracer('default'); - tracer.startActiveSpan('GET /users', () => { + tracer.startActiveSpan('GET /users', parent => { tracer.startActiveSpan( 'SELECT * FROM users;', { @@ -720,6 +819,7 @@ describe('SentrySpanProcessor', () => { expect(sentrySpan).toBeDefined(); childOtelSpan.end(); + parent.end(); expect(sentrySpan?.endTimestamp).toBeUndefined(); @@ -733,7 +833,7 @@ describe('SentrySpanProcessor', () => { it('handles child spans of Sentry requests normally', async () => { const tracer = provider.getTracer('default'); - tracer.startActiveSpan('GET /users', () => { + tracer.startActiveSpan('GET /users', parent => { tracer.startActiveSpan( 'SELECT * FROM users;', { @@ -743,19 +843,17 @@ describe('SentrySpanProcessor', () => { }, }, child => { - const childOtelSpan = child as OtelSpan; - - const grandchildSpan = tracer.startSpan('child 1'); + const grandchild = tracer.startSpan('child 1'); - const sentrySpan = getSpanForOtelSpan(childOtelSpan); + const sentrySpan = getSpanForOtelSpan(child); expect(sentrySpan).toBeDefined(); - const sentryGrandchildSpan = getSpanForOtelSpan(grandchildSpan); + const sentryGrandchildSpan = getSpanForOtelSpan(grandchild); expect(sentryGrandchildSpan).toBeDefined(); - grandchildSpan.end(); - - childOtelSpan.end(); + grandchild.end(); + child.end(); + parent.end(); expect(sentryGrandchildSpan?.endTimestamp).toBeDefined(); expect(sentrySpan?.endTimestamp).toBeUndefined(); From 0e23d4d47d52a57c27b7f5777495d5c26d7efe23 Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Tue, 12 Sep 2023 17:31:25 +0200 Subject: [PATCH 31/35] fix(node-experimental): Ignore OPTIONS & HEAD requests (#9001) Similar to express, we want to ignore (incoming) OPTIONS & HEAD requests. --- packages/node-experimental/src/integrations/http.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/packages/node-experimental/src/integrations/http.ts b/packages/node-experimental/src/integrations/http.ts index a64fdca5eab8..c5837b02fae6 100644 --- a/packages/node-experimental/src/integrations/http.ts +++ b/packages/node-experimental/src/integrations/http.ts @@ -98,6 +98,16 @@ export class Http implements Integration { return isSentryHost(host); }, + ignoreIncomingRequestHook: request => { + const method = request.method?.toUpperCase(); + // We do not capture OPTIONS/HEAD requests as transactions + if (method === 'OPTIONS' || method === 'HEAD') { + return true; + } + + return false; + }, + requireParentforOutgoingSpans: true, requireParentforIncomingSpans: false, requestHook: (span, req) => { From cfc233389149eb64026022258c803e627931a05e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kry=C5=A1tof=20Wold=C5=99ich?= <31292499+krystofwoldrich@users.noreply.github.com> Date: Tue, 12 Sep 2023 21:07:41 +0200 Subject: [PATCH 32/35] chore(ts): Add TS3.8 compile test (#8955) --- .github/workflows/build.yml | 1 + .../test-applications/generic-ts3.8/.npmrc | 2 ++ .../test-applications/generic-ts3.8/index.ts | 13 +++++++++ .../generic-ts3.8/package.json | 28 +++++++++++++++++++ .../generic-ts3.8/tsconfig.json | 11 ++++++++ 5 files changed, 55 insertions(+) create mode 100644 packages/e2e-tests/test-applications/generic-ts3.8/.npmrc create mode 100644 packages/e2e-tests/test-applications/generic-ts3.8/index.ts create mode 100644 packages/e2e-tests/test-applications/generic-ts3.8/package.json create mode 100644 packages/e2e-tests/test-applications/generic-ts3.8/tsconfig.json diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 4a8eb33ad5dd..6dc1f35ae8ba 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -793,6 +793,7 @@ jobs: 'standard-frontend-react', 'standard-frontend-react-tracing-import', 'sveltekit', + 'generic-ts3.8', ] build-command: - false diff --git a/packages/e2e-tests/test-applications/generic-ts3.8/.npmrc b/packages/e2e-tests/test-applications/generic-ts3.8/.npmrc new file mode 100644 index 000000000000..c6b3ef9b3eaa --- /dev/null +++ b/packages/e2e-tests/test-applications/generic-ts3.8/.npmrc @@ -0,0 +1,2 @@ +@sentry:registry=http://localhost:4873 +@sentry-internal:registry=http://localhost:4873 diff --git a/packages/e2e-tests/test-applications/generic-ts3.8/index.ts b/packages/e2e-tests/test-applications/generic-ts3.8/index.ts new file mode 100644 index 000000000000..823bd62fe09c --- /dev/null +++ b/packages/e2e-tests/test-applications/generic-ts3.8/index.ts @@ -0,0 +1,13 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ +// we need to import the SDK to ensure tsc check the types +import * as _SentryBrowser from '@sentry/browser'; +import * as _SentryCore from '@sentry/core'; +import * as _SentryHub from '@sentry/hub'; +import * as _SentryIntegrations from '@sentry/integrations'; +import * as _SentryNode from '@sentry/node'; +import * as _SentryOpentelemetry from '@sentry/opentelemetry-node'; +import * as _SentryReplay from '@sentry/replay'; +import * as _SentryTracing from '@sentry/tracing'; +import * as _SentryTypes from '@sentry/types'; +import * as _SentryUtils from '@sentry/utils'; +import * as _SentryWasm from '@sentry/wasm'; diff --git a/packages/e2e-tests/test-applications/generic-ts3.8/package.json b/packages/e2e-tests/test-applications/generic-ts3.8/package.json new file mode 100644 index 000000000000..dd4a2b22544d --- /dev/null +++ b/packages/e2e-tests/test-applications/generic-ts3.8/package.json @@ -0,0 +1,28 @@ +{ + "name": "@sentry-internal/ts3.8-test", + "private": true, + "license": "MIT", + "scripts": { + "build:types": "pnpm run type-check", + "ts-version": "tsc --version", + "type-check": "tsc --project tsconfig.json", + "test:build": "pnpm install && pnpm run build:types", + "test:assert": "pnpm -v" + }, + "devDependencies": { + "typescript": "3.8.3" + }, + "dependencies": { + "@sentry/browser": "latest || *", + "@sentry/core": "latest || *", + "@sentry/hub": "latest || *", + "@sentry/integrations": "latest || *", + "@sentry/node": "latest || *", + "@sentry/opentelemetry-node": "latest || *", + "@sentry/replay": "latest || *", + "@sentry/tracing": "latest || *", + "@sentry/types": "latest || *", + "@sentry/utils": "latest || *", + "@sentry/wasm": "latest || *" + } +} diff --git a/packages/e2e-tests/test-applications/generic-ts3.8/tsconfig.json b/packages/e2e-tests/test-applications/generic-ts3.8/tsconfig.json new file mode 100644 index 000000000000..932aa58e0c9a --- /dev/null +++ b/packages/e2e-tests/test-applications/generic-ts3.8/tsconfig.json @@ -0,0 +1,11 @@ +{ + "include": ["index.ts"], + "compilerOptions": { + "lib": ["es6", "DOM"], + "skipLibCheck": false, + "noEmit": true, + "types": [], + "target": "es6", + "moduleResolution": "node" + } +} From 789e8494f45ac5e2ed08eaa8b899eca164aff8da Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Tue, 12 Sep 2023 21:08:11 +0200 Subject: [PATCH 33/35] feat(node-experimental): Keep breadcrumbs on transaction (#8967) --- packages/node-experimental/src/index.ts | 3 +- .../src/integrations/http.ts | 2 +- packages/node-experimental/src/sdk/client.ts | 26 +++- packages/node-experimental/src/sdk/hub.ts | 140 ++++++++++++++++++ .../node-experimental/src/sdk/initOtel.ts | 2 +- .../src/sdk/otelContextManager.ts | 3 +- packages/node-experimental/src/sdk/scope.ts | 138 +++++++++++++++++ packages/node-experimental/src/sdk/trace.ts | 2 +- packages/node-experimental/src/types.ts | 6 + 9 files changed, 314 insertions(+), 8 deletions(-) create mode 100644 packages/node-experimental/src/sdk/hub.ts create mode 100644 packages/node-experimental/src/sdk/scope.ts diff --git a/packages/node-experimental/src/index.ts b/packages/node-experimental/src/index.ts index c55a01641da7..3c7fa347cf94 100644 --- a/packages/node-experimental/src/index.ts +++ b/packages/node-experimental/src/index.ts @@ -12,6 +12,7 @@ export { INTEGRATIONS as Integrations }; export { getAutoPerformanceIntegrations } from './integrations/getAutoPerformanceIntegrations'; export * as Handlers from './sdk/handlers'; export * from './sdk/trace'; +export { getCurrentHub, getHubFromCarrier } from './sdk/hub'; export { makeNodeTransport, @@ -33,8 +34,6 @@ export { extractTraceparentData, flush, getActiveTransaction, - getHubFromCarrier, - getCurrentHub, Hub, lastEventId, makeMain, diff --git a/packages/node-experimental/src/integrations/http.ts b/packages/node-experimental/src/integrations/http.ts index c5837b02fae6..2b1fc465049b 100644 --- a/packages/node-experimental/src/integrations/http.ts +++ b/packages/node-experimental/src/integrations/http.ts @@ -10,7 +10,7 @@ import { _INTERNAL_getSentrySpan } from '@sentry/opentelemetry-node'; import type { EventProcessor, Hub, Integration } from '@sentry/types'; import type { ClientRequest, IncomingMessage, ServerResponse } from 'http'; -import type { NodeExperimentalClient } from '../sdk/client'; +import type { NodeExperimentalClient } from '../types'; import { getRequestSpanData } from '../utils/getRequestSpanData'; interface TracingOptions { diff --git a/packages/node-experimental/src/sdk/client.ts b/packages/node-experimental/src/sdk/client.ts index 8d7bddad07f6..29f68980f008 100644 --- a/packages/node-experimental/src/sdk/client.ts +++ b/packages/node-experimental/src/sdk/client.ts @@ -1,13 +1,19 @@ import type { Tracer } from '@opentelemetry/api'; import { trace } from '@opentelemetry/api'; +import type { EventHint, Scope } from '@sentry/node'; import { NodeClient, SDK_VERSION } from '@sentry/node'; +import type { Event } from '@sentry/types'; -import type { NodeExperimentalClientOptions } from '../types'; +import type { + NodeExperimentalClient as NodeExperimentalClientInterface, + NodeExperimentalClientOptions, +} from '../types'; +import { OtelScope } from './scope'; /** * A client built on top of the NodeClient, which provides some otel-specific things on top. */ -export class NodeExperimentalClient extends NodeClient { +export class NodeExperimentalClient extends NodeClient implements NodeExperimentalClientInterface { private _tracer: Tracer | undefined; public constructor(options: ConstructorParameters[0]) { @@ -47,4 +53,20 @@ export class NodeExperimentalClient extends NodeClient { // Just a type-cast, basically return super.getOptions(); } + + /** + * Extends the base `_prepareEvent` so that we can properly handle `captureContext`. + * This uses `Scope.clone()`, which we need to replace with `OtelScope.clone()` for this client. + */ + protected _prepareEvent(event: Event, hint: EventHint, scope?: Scope): PromiseLike { + let actualScope = scope; + + // Remove `captureContext` hint and instead clone already here + if (hint && hint.captureContext) { + actualScope = OtelScope.clone(scope); + delete hint.captureContext; + } + + return super._prepareEvent(event, hint, actualScope); + } } diff --git a/packages/node-experimental/src/sdk/hub.ts b/packages/node-experimental/src/sdk/hub.ts new file mode 100644 index 000000000000..8220265e600c --- /dev/null +++ b/packages/node-experimental/src/sdk/hub.ts @@ -0,0 +1,140 @@ +import type { Carrier, Scope } from '@sentry/core'; +import { Hub } from '@sentry/core'; +import type { Client } from '@sentry/types'; +import { getGlobalSingleton, GLOBAL_OBJ } from '@sentry/utils'; + +import { OtelScope } from './scope'; + +/** A custom hub that ensures we always creat an OTEL scope. */ + +class OtelHub extends Hub { + public constructor(client?: Client, scope: Scope = new OtelScope()) { + super(client, scope); + } + + /** + * @inheritDoc + */ + public pushScope(): Scope { + // We want to clone the content of prev scope + const scope = OtelScope.clone(this.getScope()); + this.getStack().push({ + client: this.getClient(), + scope, + }); + return scope; + } +} + +/** + * ******************************************************************************* + * Everything below here is a copy of the stuff from core's hub.ts, + * only that we make sure to create our custom OtelScope instead of the default Scope. + * This is necessary to get the correct breadcrumbs behavior. + * + * Basically, this overwrites all places that do `new Scope()` with `new OtelScope()`. + * Which in turn means overwriting all places that do `new Hub()` and make sure to pass in a OtelScope instead. + * ******************************************************************************* + */ + +/** + * API compatibility version of this hub. + * + * WARNING: This number should only be increased when the global interface + * changes and new methods are introduced. + * + * @hidden + */ +const API_VERSION = 4; + +/** + * Returns the default hub instance. + * + * If a hub is already registered in the global carrier but this module + * contains a more recent version, it replaces the registered version. + * Otherwise, the currently registered hub will be returned. + */ +export function getCurrentHub(): Hub { + // Get main carrier (global for every environment) + const registry = getMainCarrier(); + + if (registry.__SENTRY__ && registry.__SENTRY__.acs) { + const hub = registry.__SENTRY__.acs.getCurrentHub(); + + if (hub) { + return hub; + } + } + + // Return hub that lives on a global object + return getGlobalHub(registry); +} + +/** + * This will create a new {@link Hub} and add to the passed object on + * __SENTRY__.hub. + * @param carrier object + * @hidden + */ +export function getHubFromCarrier(carrier: Carrier): Hub { + return getGlobalSingleton('hub', () => new OtelHub(), carrier); +} + +/** + * @private Private API with no semver guarantees! + * + * If the carrier does not contain a hub, a new hub is created with the global hub client and scope. + */ +export function ensureHubOnCarrier(carrier: Carrier, parent: Hub = getGlobalHub()): void { + // If there's no hub on current domain, or it's an old API, assign a new one + if (!hasHubOnCarrier(carrier) || getHubFromCarrier(carrier).isOlderThan(API_VERSION)) { + const globalHubTopStack = parent.getStackTop(); + setHubOnCarrier(carrier, new OtelHub(globalHubTopStack.client, OtelScope.clone(globalHubTopStack.scope))); + } +} + +function getGlobalHub(registry: Carrier = getMainCarrier()): Hub { + // If there's no hub, or its an old API, assign a new one + if (!hasHubOnCarrier(registry) || getHubFromCarrier(registry).isOlderThan(API_VERSION)) { + setHubOnCarrier(registry, new OtelHub()); + } + + // Return hub that lives on a global object + return getHubFromCarrier(registry); +} + +/** + * This will tell whether a carrier has a hub on it or not + * @param carrier object + */ +function hasHubOnCarrier(carrier: Carrier): boolean { + return !!(carrier && carrier.__SENTRY__ && carrier.__SENTRY__.hub); +} + +/** + * Returns the global shim registry. + * + * FIXME: This function is problematic, because despite always returning a valid Carrier, + * it has an optional `__SENTRY__` property, which then in turn requires us to always perform an unnecessary check + * at the call-site. We always access the carrier through this function, so we can guarantee that `__SENTRY__` is there. + **/ +function getMainCarrier(): Carrier { + GLOBAL_OBJ.__SENTRY__ = GLOBAL_OBJ.__SENTRY__ || { + extensions: {}, + hub: undefined, + }; + return GLOBAL_OBJ; +} + +/** + * This will set passed {@link Hub} on the passed object's __SENTRY__.hub attribute + * @param carrier object + * @param hub Hub + * @returns A boolean indicating success or failure + */ +function setHubOnCarrier(carrier: Carrier, hub: Hub): boolean { + if (!carrier) return false; + const __SENTRY__ = (carrier.__SENTRY__ = carrier.__SENTRY__ || {}); + __SENTRY__.hub = hub; + return true; +} diff --git a/packages/node-experimental/src/sdk/initOtel.ts b/packages/node-experimental/src/sdk/initOtel.ts index 318bba138837..44ae76329dd2 100644 --- a/packages/node-experimental/src/sdk/initOtel.ts +++ b/packages/node-experimental/src/sdk/initOtel.ts @@ -4,7 +4,7 @@ import { getCurrentHub } from '@sentry/core'; import { SentryPropagator, SentrySpanProcessor } from '@sentry/opentelemetry-node'; import { logger } from '@sentry/utils'; -import type { NodeExperimentalClient } from './client'; +import type { NodeExperimentalClient } from '../types'; import { SentryContextManager } from './otelContextManager'; /** diff --git a/packages/node-experimental/src/sdk/otelContextManager.ts b/packages/node-experimental/src/sdk/otelContextManager.ts index 9110b9e62328..c8eadfb85f65 100644 --- a/packages/node-experimental/src/sdk/otelContextManager.ts +++ b/packages/node-experimental/src/sdk/otelContextManager.ts @@ -2,7 +2,8 @@ import type { Context } from '@opentelemetry/api'; import * as api from '@opentelemetry/api'; import { AsyncLocalStorageContextManager } from '@opentelemetry/context-async-hooks'; import type { Carrier, Hub } from '@sentry/core'; -import { ensureHubOnCarrier, getCurrentHub, getHubFromCarrier } from '@sentry/core'; + +import { ensureHubOnCarrier, getCurrentHub, getHubFromCarrier } from './hub'; export const OTEL_CONTEXT_HUB_KEY = api.createContextKey('sentry_hub'); diff --git a/packages/node-experimental/src/sdk/scope.ts b/packages/node-experimental/src/sdk/scope.ts new file mode 100644 index 000000000000..ab255f1d20bc --- /dev/null +++ b/packages/node-experimental/src/sdk/scope.ts @@ -0,0 +1,138 @@ +import { Scope } from '@sentry/core'; +import type { Breadcrumb, Transaction } from '@sentry/types'; +import { dateTimestampInSeconds } from '@sentry/utils'; + +import { getActiveSpan } from './trace'; + +const DEFAULT_MAX_BREADCRUMBS = 100; + +/** + * This is a fork of the base Transaction with OTEL specific stuff added. + * Note that we do not solve this via an actual subclass, but by wrapping this in a proxy when we need it - + * as we can't easily control all the places a transaction may be created. + */ +interface TransactionWithBreadcrumbs extends Transaction { + _breadcrumbs: Breadcrumb[]; + + /** Get all breadcrumbs added to this transaction. */ + getBreadcrumbs(): Breadcrumb[]; + + /** Add a breadcrumb to this transaction. */ + addBreadcrumb(breadcrumb: Breadcrumb, maxBreadcrumbs?: number): void; +} + +/** A fork of the classic scope with some otel specific stuff. */ +export class OtelScope extends Scope { + /** + * @inheritDoc + */ + public static clone(scope?: Scope): Scope { + const newScope = new OtelScope(); + if (scope) { + newScope._breadcrumbs = [...scope['_breadcrumbs']]; + newScope._tags = { ...scope['_tags'] }; + newScope._extra = { ...scope['_extra'] }; + newScope._contexts = { ...scope['_contexts'] }; + newScope._user = scope['_user']; + newScope._level = scope['_level']; + newScope._span = scope['_span']; + newScope._session = scope['_session']; + newScope._transactionName = scope['_transactionName']; + newScope._fingerprint = scope['_fingerprint']; + newScope._eventProcessors = [...scope['_eventProcessors']]; + newScope._requestSession = scope['_requestSession']; + newScope._attachments = [...scope['_attachments']]; + newScope._sdkProcessingMetadata = { ...scope['_sdkProcessingMetadata'] }; + newScope._propagationContext = { ...scope['_propagationContext'] }; + } + return newScope; + } + + /** + * @inheritDoc + */ + public addBreadcrumb(breadcrumb: Breadcrumb, maxBreadcrumbs?: number): this { + const transaction = getActiveTransaction(); + + if (transaction) { + transaction.addBreadcrumb(breadcrumb, maxBreadcrumbs); + return this; + } + + return super.addBreadcrumb(breadcrumb, maxBreadcrumbs); + } + + /** + * @inheritDoc + */ + protected _getBreadcrumbs(): Breadcrumb[] { + const transaction = getActiveTransaction(); + const transactionBreadcrumbs = transaction ? transaction.getBreadcrumbs() : []; + + return this._breadcrumbs.concat(transactionBreadcrumbs); + } +} + +/** + * This gets the currently active transaction, + * and ensures to wrap it so that we can store breadcrumbs on it. + */ +function getActiveTransaction(): TransactionWithBreadcrumbs | undefined { + const activeSpan = getActiveSpan(); + const transaction = activeSpan && activeSpan.transaction; + + if (!transaction) { + return undefined; + } + + if (transactionHasBreadcrumbs(transaction)) { + return transaction; + } + + return new Proxy(transaction as TransactionWithBreadcrumbs, { + get(target, prop, receiver) { + if (prop === 'addBreadcrumb') { + return addBreadcrumb; + } + if (prop === 'getBreadcrumbs') { + return getBreadcrumbs; + } + if (prop === '_breadcrumbs') { + const breadcrumbs = Reflect.get(target, prop, receiver); + return breadcrumbs || []; + } + return Reflect.get(target, prop, receiver); + }, + }); +} + +function transactionHasBreadcrumbs(transaction: Transaction): transaction is TransactionWithBreadcrumbs { + return ( + typeof (transaction as TransactionWithBreadcrumbs).getBreadcrumbs === 'function' && + typeof (transaction as TransactionWithBreadcrumbs).addBreadcrumb === 'function' + ); +} + +/** Add a breadcrumb to a transaction. */ +function addBreadcrumb(this: TransactionWithBreadcrumbs, breadcrumb: Breadcrumb, maxBreadcrumbs?: number): void { + const maxCrumbs = typeof maxBreadcrumbs === 'number' ? maxBreadcrumbs : DEFAULT_MAX_BREADCRUMBS; + + // No data has been changed, so don't notify scope listeners + if (maxCrumbs <= 0) { + return; + } + + const mergedBreadcrumb = { + timestamp: dateTimestampInSeconds(), + ...breadcrumb, + }; + + const breadcrumbs = this._breadcrumbs; + breadcrumbs.push(mergedBreadcrumb); + this._breadcrumbs = breadcrumbs.length > maxCrumbs ? breadcrumbs.slice(-maxCrumbs) : breadcrumbs; +} + +/** Get all breadcrumbs from a transaction. */ +function getBreadcrumbs(this: TransactionWithBreadcrumbs): Breadcrumb[] { + return this._breadcrumbs; +} diff --git a/packages/node-experimental/src/sdk/trace.ts b/packages/node-experimental/src/sdk/trace.ts index e2fd14663e28..1faf780ec5c7 100644 --- a/packages/node-experimental/src/sdk/trace.ts +++ b/packages/node-experimental/src/sdk/trace.ts @@ -5,7 +5,7 @@ import { _INTERNAL_getSentrySpan } from '@sentry/opentelemetry-node'; import type { Span, TransactionContext } from '@sentry/types'; import { isThenable } from '@sentry/utils'; -import type { NodeExperimentalClient } from './client'; +import type { NodeExperimentalClient } from '../types'; /** * Wraps a function with a transaction/span and finishes the span after the function is done. diff --git a/packages/node-experimental/src/types.ts b/packages/node-experimental/src/types.ts index 77b1a3ebee4c..107cfdb37266 100644 --- a/packages/node-experimental/src/types.ts +++ b/packages/node-experimental/src/types.ts @@ -1,4 +1,10 @@ +import type { Tracer } from '@opentelemetry/api'; import type { NodeClient, NodeOptions } from '@sentry/node'; export type NodeExperimentalOptions = NodeOptions; export type NodeExperimentalClientOptions = ConstructorParameters[0]; + +export interface NodeExperimentalClient extends NodeClient { + tracer: Tracer; + getOptions(): NodeExperimentalClientOptions; +} From 90ee2a419634fcdd334c2aba9b6b27f00cc0fccc Mon Sep 17 00:00:00 2001 From: cyx <30902641+Duncanxyz@users.noreply.github.com> Date: Wed, 13 Sep 2023 14:29:51 +0800 Subject: [PATCH 34/35] fix(utils): Prevent iterating over VueViewModel (#8981) Prevent stringifying VueViewModel objects which causes a warning when the object is logged to console. Instead, normalize it's string value to `"[VueViewModel]"` More details in #8980 --- .../browser/src/integrations/breadcrumbs.ts | 12 -------- packages/utils/src/is.ts | 17 +++++++++++ packages/utils/src/normalize.ts | 6 +++- packages/utils/src/string.ts | 13 +++++++-- packages/utils/test/is.test.ts | 10 +++++++ packages/utils/test/normalize.test.ts | 29 +++++++++++++++++++ 6 files changed, 72 insertions(+), 15 deletions(-) diff --git a/packages/browser/src/integrations/breadcrumbs.ts b/packages/browser/src/integrations/breadcrumbs.ts index e41bedc8bf1c..f71361b7d96e 100644 --- a/packages/browser/src/integrations/breadcrumbs.ts +++ b/packages/browser/src/integrations/breadcrumbs.ts @@ -183,18 +183,6 @@ function _domBreadcrumb(dom: BreadcrumbsOptions['dom']): (handlerData: HandlerDa * Creates breadcrumbs from console API calls */ function _consoleBreadcrumb(handlerData: HandlerData & { args: unknown[]; level: string }): void { - // This is a hack to fix a Vue3-specific bug that causes an infinite loop of - // console warnings. This happens when a Vue template is rendered with - // an undeclared variable, which we try to stringify, ultimately causing - // Vue to issue another warning which repeats indefinitely. - // see: https://github.com/getsentry/sentry-javascript/pull/6010 - // see: https://github.com/getsentry/sentry-javascript/issues/5916 - for (let i = 0; i < handlerData.args.length; i++) { - if (handlerData.args[i] === 'ref=Ref<') { - handlerData.args[i + 1] = 'viewRef'; - break; - } - } const breadcrumb = { category: 'console', data: { diff --git a/packages/utils/src/is.ts b/packages/utils/src/is.ts index 350826cb567c..61a94053a265 100644 --- a/packages/utils/src/is.ts +++ b/packages/utils/src/is.ts @@ -179,3 +179,20 @@ export function isInstanceOf(wat: any, base: any): boolean { return false; } } + +interface VueViewModel { + // Vue3 + __isVue?: boolean; + // Vue2 + _isVue?: boolean; +} +/** + * Checks whether given value's type is a Vue ViewModel. + * + * @param wat A value to be checked. + * @returns A boolean representing the result. + */ +export function isVueViewModel(wat: unknown): boolean { + // Not using Object.prototype.toString because in Vue 3 it would read the instance's Symbol(Symbol.toStringTag) property. + return !!(typeof wat === 'object' && wat !== null && ((wat as VueViewModel).__isVue || (wat as VueViewModel)._isVue)); +} diff --git a/packages/utils/src/normalize.ts b/packages/utils/src/normalize.ts index 1f200640dbb1..7c1adaa32ccc 100644 --- a/packages/utils/src/normalize.ts +++ b/packages/utils/src/normalize.ts @@ -1,6 +1,6 @@ import type { Primitive } from '@sentry/types'; -import { isNaN, isSyntheticEvent } from './is'; +import { isNaN, isSyntheticEvent, isVueViewModel } from './is'; import type { MemoFunc } from './memo'; import { memoBuilder } from './memo'; import { convertToPlainObject } from './object'; @@ -214,6 +214,10 @@ function stringifyValue( return '[Document]'; } + if (isVueViewModel(value)) { + return '[VueViewModel]'; + } + // React's SyntheticEvent thingy if (isSyntheticEvent(value)) { return '[SyntheticEvent]'; diff --git a/packages/utils/src/string.ts b/packages/utils/src/string.ts index 73efe8ef625e..743b25c3deef 100644 --- a/packages/utils/src/string.ts +++ b/packages/utils/src/string.ts @@ -1,4 +1,4 @@ -import { isRegExp, isString } from './is'; +import { isRegExp, isString, isVueViewModel } from './is'; export { escapeStringForRegex } from './vendor/escapeStringForRegex'; @@ -76,7 +76,16 @@ export function safeJoin(input: any[], delimiter?: string): string { for (let i = 0; i < input.length; i++) { const value = input[i]; try { - output.push(String(value)); + // This is a hack to fix a Vue3-specific bug that causes an infinite loop of + // console warnings. This happens when a Vue template is rendered with + // an undeclared variable, which we try to stringify, ultimately causing + // Vue to issue another warning which repeats indefinitely. + // see: https://github.com/getsentry/sentry-javascript/pull/8981 + if (isVueViewModel(value)) { + output.push('[VueViewModel]'); + } else { + output.push(String(value)); + } } catch (e) { output.push('[value cannot be serialized]'); } diff --git a/packages/utils/test/is.test.ts b/packages/utils/test/is.test.ts index 6a4172e4a595..da9d77a44fde 100644 --- a/packages/utils/test/is.test.ts +++ b/packages/utils/test/is.test.ts @@ -7,6 +7,7 @@ import { isNaN, isPrimitive, isThenable, + isVueViewModel, } from '../src/is'; import { supportsDOMError, supportsDOMException, supportsErrorEvent } from '../src/supports'; import { resolvedSyncPromise } from '../src/syncpromise'; @@ -134,3 +135,12 @@ describe('isNaN()', () => { expect(isNaN(new Date())).toEqual(false); }); }); + +describe('isVueViewModel()', () => { + test('should work as advertised', () => { + expect(isVueViewModel({ _isVue: true })).toEqual(true); + expect(isVueViewModel({ __isVue: true })).toEqual(true); + + expect(isVueViewModel({ foo: true })).toEqual(false); + }); +}); diff --git a/packages/utils/test/normalize.test.ts b/packages/utils/test/normalize.test.ts index d13631d43a9a..fda1798c3792 100644 --- a/packages/utils/test/normalize.test.ts +++ b/packages/utils/test/normalize.test.ts @@ -476,6 +476,17 @@ describe('normalize()', () => { foo: '[SyntheticEvent]', }); }); + + test('known classes like `VueViewModel`', () => { + const obj = { + foo: { + _isVue: true, + }, + }; + expect(normalize(obj)).toEqual({ + foo: '[VueViewModel]', + }); + }); }); describe('can limit object to depth', () => { @@ -618,6 +629,24 @@ describe('normalize()', () => { }); }); + test('normalizes value on every iteration of decycle and takes care of things like `VueViewModel`', () => { + const obj = { + foo: { + _isVue: true, + }, + baz: NaN, + qux: function qux(): void { + /* no-empty */ + }, + }; + const result = normalize(obj); + expect(result).toEqual({ + foo: '[VueViewModel]', + baz: '[NaN]', + qux: '[Function: qux]', + }); + }); + describe('skips normalizing objects marked with a non-enumerable property __sentry_skip_normalization__', () => { test('by leaving non-serializable values intact', () => { const someFun = () => undefined; From 1768ba0acc193c7309969fc4779177e90360f985 Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Wed, 13 Sep 2023 09:31:30 +0200 Subject: [PATCH 35/35] meta(changelog): Update changelog for 7.69.0 --- CHANGELOG.md | 87 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 87 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 99a1680a6365..f08a6c49af17 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,93 @@ - "You miss 100 percent of the chances you don't take. — Wayne Gretzky" — Michael Scott +## 7.69.0 + +### Important Changes + +- **New Performance APIs** + - feat: Update span performance API names (#8971) + - feat(core): Introduce startSpanManual (#8913) + +This release introduces a new set of top level APIs for the Performance Monitoring SDKs. These aim to simplify creating spans and reduce the boilerplate needed for performance instrumentation. The three new methods introduced are `Sentry.startSpan`, `Sentry.startInactiveSpan`, and `Sentry.startSpanManual`. These methods are available in the browser and node SDKs. + +`Sentry.startSpan` wraps a callback in a span. The span is automatically finished when the callback returns. This is the recommended way to create spans. + +```js +// Start a span that tracks the duration of expensiveFunction +const result = Sentry.startSpan({ name: 'important function' }, () => { + return expensiveFunction(); +}); + +// You can also mutate the span wrapping the callback to set data or status +Sentry.startSpan({ name: 'important function' }, (span) => { + // span is undefined if performance monitoring is turned off or if + // the span was not sampled. This is done to reduce overhead. + span?.setData('version', '1.0.0'); + return expensiveFunction(); +}); +``` + +If you don't want the span to finish when the callback returns, use `Sentry.startSpanManual` to control when the span is finished. This is useful for event emitters or similar. + +```js +// Start a span that tracks the duration of middleware +function middleware(_req, res, next) { + return Sentry.startSpanManual({ name: 'middleware' }, (span, finish) => { + res.once('finish', () => { + span?.setHttpStatus(res.status); + finish(); + }); + return next(); + }); +} +``` + +`Sentry.startSpan` and `Sentry.startSpanManual` create a span and make it active for the duration of the callback. Any spans created while this active span is running will be added as a child span to it. If you want to create a span without making it active, use `Sentry.startInactiveSpan`. This is useful for creating parallel spans that are not related to each other. + +```js +const span1 = Sentry.startInactiveSpan({ name: 'span1' }); + +someWork(); + +const span2 = Sentry.startInactiveSpan({ name: 'span2' }); + +moreWork(); + +const span3 = Sentry.startInactiveSpan({ name: 'span3' }); + +evenMoreWork(); + +span1?.finish(); +span2?.finish(); +span3?.finish(); +``` + +### Other Changes + +- feat(core): Export `BeforeFinishCallback` type (#8999) +- build(eslint): Enforce that ts-expect-error is used (#8987) +- feat(integration): Ensure `LinkedErrors` integration runs before all event processors (#8956) +- feat(node-experimental): Keep breadcrumbs on transaction (#8967) +- feat(redux): Add 'attachReduxState' option (#8953) +- feat(remix): Accept `org`, `project` and `url` as args to upload script (#8985) +- fix(utils): Prevent iterating over VueViewModel (#8981) +- fix(utils): uuidv4 fix for cloudflare (#8968) +- fix(core): Always use event message and exception values for `ignoreErrors` (#8986) +- fix(nextjs): Add new potential location for Next.js request AsyncLocalStorage (#9006) +- fix(node-experimental): Ensure we only create HTTP spans when outgoing (#8966) +- fix(node-experimental): Ignore OPTIONS & HEAD requests (#9001) +- fix(node-experimental): Ignore outgoing Sentry requests (#8994) +- fix(node-experimental): Require parent span for `pg` spans (#8993) +- fix(node-experimental): Use Sentry logger as Otel logger (#8960) +- fix(node-otel): Refactor OTEL span reference cleanup (#9000) +- fix(react): Switch to props in `useRoutes` (#8998) +- fix(remix): Add `glob` to Remix SDK dependencies. (#8963) +- fix(replay): Ensure `handleRecordingEmit` aborts when event is not added (#8938) +- fix(replay): Fully stop & restart session when it expires (#8834) + +Work in this release contributed by @Duncanxyz and @malay44. Thank you for your contributions! + ## 7.68.0 - feat(browser): Add `BroadcastChannel` and `SharedWorker` to TryCatch EventTargets (#8943)