From 5eeb8a9ef25cf1809a149a6b0b00e823f9f96536 Mon Sep 17 00:00:00 2001 From: Tim Fish Date: Thu, 4 Jan 2024 23:49:52 +0100 Subject: [PATCH 01/43] fix(node): Anr events should have an `event_id` (#10068) --- packages/node/src/integrations/anr/worker.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/packages/node/src/integrations/anr/worker.ts b/packages/node/src/integrations/anr/worker.ts index e2292ce0aff0..f87ff8bb672f 100644 --- a/packages/node/src/integrations/anr/worker.ts +++ b/packages/node/src/integrations/anr/worker.ts @@ -6,7 +6,13 @@ import { updateSession, } from '@sentry/core'; import type { Event, Session, StackFrame, TraceContext } from '@sentry/types'; -import { callFrameToStackFrame, normalizeUrlToBase, stripSentryFramesAndReverse, watchdogTimer } from '@sentry/utils'; +import { + callFrameToStackFrame, + normalizeUrlToBase, + stripSentryFramesAndReverse, + uuid4, + watchdogTimer, +} from '@sentry/utils'; import { Session as InspectorSession } from 'inspector'; import { parentPort, workerData } from 'worker_threads'; import { makeNodeTransport } from '../../transports'; @@ -90,6 +96,7 @@ async function sendAnrEvent(frames?: StackFrame[], traceContext?: TraceContext): log('Sending event'); const event: Event = { + event_id: uuid4(), contexts: { ...options.contexts, trace: traceContext }, release: options.release, environment: options.environment, From 366ed0bbe4e236860b6954998e200022686fe7a1 Mon Sep 17 00:00:00 2001 From: Luca Forstner Date: Fri, 5 Jan 2024 10:23:28 +0100 Subject: [PATCH 02/43] fix(nextjs): Don't capture not-found and redirect errors in generation functions (#10057) --- .../with-notfound/page.tsx | 11 ++++++ .../with-redirect/page.tsx | 11 ++++++ .../test-applications/nextjs-14/app/page.tsx | 3 ++ .../tests/generation-functions.test.ts | 34 ++++++++++++++++++ .../wrapGenerationFunctionWithSentry.ts | 36 +++++++++++++------ 5 files changed, 84 insertions(+), 11 deletions(-) create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-14/app/generation-functions/with-notfound/page.tsx create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-14/app/generation-functions/with-redirect/page.tsx create mode 100644 dev-packages/e2e-tests/test-applications/nextjs-14/app/page.tsx diff --git a/dev-packages/e2e-tests/test-applications/nextjs-14/app/generation-functions/with-notfound/page.tsx b/dev-packages/e2e-tests/test-applications/nextjs-14/app/generation-functions/with-notfound/page.tsx new file mode 100644 index 000000000000..46d4ddd7f962 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-14/app/generation-functions/with-notfound/page.tsx @@ -0,0 +1,11 @@ +import { notFound } from 'next/navigation'; + +export const dynamic = 'force-dynamic'; + +export default function PageWithRedirect() { + return

Hello World!

; +} + +export async function generateMetadata() { + notFound(); +} diff --git a/dev-packages/e2e-tests/test-applications/nextjs-14/app/generation-functions/with-redirect/page.tsx b/dev-packages/e2e-tests/test-applications/nextjs-14/app/generation-functions/with-redirect/page.tsx new file mode 100644 index 000000000000..f1f37d7a32c6 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-14/app/generation-functions/with-redirect/page.tsx @@ -0,0 +1,11 @@ +import { redirect } from 'next/navigation'; + +export const dynamic = 'force-dynamic'; + +export default function PageWithRedirect() { + return

Hello World!

; +} + +export async function generateMetadata() { + redirect('/'); +} diff --git a/dev-packages/e2e-tests/test-applications/nextjs-14/app/page.tsx b/dev-packages/e2e-tests/test-applications/nextjs-14/app/page.tsx new file mode 100644 index 000000000000..6f4e63ef5748 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-14/app/page.tsx @@ -0,0 +1,3 @@ +export default function Page() { + return

Home

; +} diff --git a/dev-packages/e2e-tests/test-applications/nextjs-14/tests/generation-functions.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-14/tests/generation-functions.test.ts index 3828312607ea..b5fe7ee67393 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-14/tests/generation-functions.test.ts +++ b/dev-packages/e2e-tests/test-applications/nextjs-14/tests/generation-functions.test.ts @@ -77,3 +77,37 @@ test('Should send a transaction and an error event for a faulty generateViewport expect(await transactionPromise).toBeDefined(); expect(await errorEventPromise).toBeDefined(); }); + +test('Should send a transaction event with correct status for a generateMetadata() function invokation with redirect()', async ({ + page, +}) => { + const testTitle = 'redirect-foobar'; + + const transactionPromise = waitForTransaction('nextjs-14', async transactionEvent => { + return ( + transactionEvent?.transaction === 'Page.generateMetadata (/generation-functions/with-redirect)' && + transactionEvent.contexts?.trace?.data?.['searchParams']?.['metadataTitle'] === testTitle + ); + }); + + await page.goto(`/generation-functions/with-redirect?metadataTitle=${testTitle}`); + + expect((await transactionPromise).contexts?.trace?.status).toBe('ok'); +}); + +test('Should send a transaction event with correct status for a generateMetadata() function invokation with notfound()', async ({ + page, +}) => { + const testTitle = 'notfound-foobar'; + + const transactionPromise = waitForTransaction('nextjs-14', async transactionEvent => { + return ( + transactionEvent?.transaction === 'Page.generateMetadata (/generation-functions/with-notfound)' && + transactionEvent.contexts?.trace?.data?.['searchParams']?.['metadataTitle'] === testTitle + ); + }); + + await page.goto(`/generation-functions/with-notfound?metadataTitle=${testTitle}`); + + expect((await transactionPromise).contexts?.trace?.status).toBe('not_found'); +}); diff --git a/packages/nextjs/src/common/wrapGenerationFunctionWithSentry.ts b/packages/nextjs/src/common/wrapGenerationFunctionWithSentry.ts index d1765aa2c41e..fe90b6f6ca39 100644 --- a/packages/nextjs/src/common/wrapGenerationFunctionWithSentry.ts +++ b/packages/nextjs/src/common/wrapGenerationFunctionWithSentry.ts @@ -6,12 +6,13 @@ import { getCurrentScope, handleCallbackErrors, runWithAsyncContext, - startSpan, + startSpanManual, } from '@sentry/core'; import type { WebFetchHeaders } from '@sentry/types'; import { winterCGHeadersToDict } from '@sentry/utils'; import type { GenerationFunctionContext } from '../common/types'; +import { isNotFoundNavigationError, isRedirectNavigationError } from './nextNavigationErrorUtils'; import { commonObjectToPropagationContext } from './utils/commonObjectTracing'; /** @@ -61,7 +62,7 @@ export function wrapGenerationFunctionWithSentry a transactionContext.parentSpanId = commonSpanId; } - return startSpan( + return startSpanManual( { op: 'function.nextjs', name: `${componentType}.${generationFunctionIdentifier} (${componentRoute})`, @@ -76,18 +77,31 @@ export function wrapGenerationFunctionWithSentry a }, }, }, - () => { + span => { return handleCallbackErrors( () => originalFunction.apply(thisArg, args), - err => - captureException(err, { - mechanism: { - handled: false, - data: { - function: 'wrapGenerationFunctionWithSentry', + err => { + if (isNotFoundNavigationError(err)) { + // We don't want to report "not-found"s + span?.setStatus('not_found'); + } else if (isRedirectNavigationError(err)) { + // We don't want to report redirects + span?.setStatus('ok'); + } else { + span?.setStatus('internal_error'); + captureException(err, { + mechanism: { + handled: false, + data: { + function: 'wrapGenerationFunctionWithSentry', + }, }, - }, - }), + }); + } + }, + () => { + span?.end(); + }, ); }, ); From 5def98394f993c27178451e8f1f645640265d9df Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Fri, 5 Jan 2024 11:16:19 +0100 Subject: [PATCH 03/43] feat(core): Allow to pass start/end timestamp for spans flexibly (#10060) We allow the same formats as OpenTelemetry: * `number` (we handle both seconds and milliseconds) * `Date` * `[seconds, nanoseconds]` --- packages/core/src/tracing/idletransaction.ts | 10 ++--- packages/core/src/tracing/span.ts | 9 ++--- packages/core/src/tracing/trace.ts | 35 ++++++++++++------ packages/core/src/tracing/transaction.ts | 11 +++--- packages/core/src/tracing/utils.ts | 8 ---- packages/core/src/utils/spanUtils.ts | 32 +++++++++++++++- packages/core/test/lib/tracing/span.test.ts | 35 ++++++++++++++++++ packages/core/test/lib/tracing/trace.test.ts | 22 +++++++++++ .../core/test/lib/utils/spanUtils.test.ts | 37 ++++++++++++++++++- packages/types/src/index.ts | 2 +- packages/types/src/opentelemetry.ts | 31 ++++++++++++++++ packages/types/src/span.ts | 6 ++- 12 files changed, 198 insertions(+), 40 deletions(-) create mode 100644 packages/types/src/opentelemetry.ts diff --git a/packages/core/src/tracing/idletransaction.ts b/packages/core/src/tracing/idletransaction.ts index 458bd9281627..fdc1813e5b50 100644 --- a/packages/core/src/tracing/idletransaction.ts +++ b/packages/core/src/tracing/idletransaction.ts @@ -1,13 +1,13 @@ /* eslint-disable max-lines */ -import type { TransactionContext } from '@sentry/types'; +import type { SpanTimeInput, TransactionContext } from '@sentry/types'; import { logger, timestampInSeconds } from '@sentry/utils'; import { DEBUG_BUILD } from '../debug-build'; import type { Hub } from '../hub'; +import { spanTimeInputToSeconds } from '../utils/spanUtils'; import type { Span } from './span'; import { SpanRecorder } from './span'; import { Transaction } from './transaction'; -import { ensureTimestampInSeconds } from './utils'; export const TRACING_DEFAULTS = { idleTimeout: 1000, @@ -138,8 +138,8 @@ export class IdleTransaction extends Transaction { } /** {@inheritDoc} */ - public end(endTimestamp: number = timestampInSeconds()): string | undefined { - const endTimestampInS = ensureTimestampInSeconds(endTimestamp); + public end(endTimestamp?: SpanTimeInput): string | undefined { + const endTimestampInS = spanTimeInputToSeconds(endTimestamp); this._finished = true; this.activities = {}; @@ -153,7 +153,7 @@ export class IdleTransaction extends Transaction { logger.log('[Tracing] finishing IdleTransaction', new Date(endTimestampInS * 1000).toISOString(), this.op); for (const callback of this._beforeFinishCallbacks) { - callback(this, endTimestamp); + callback(this, endTimestampInS); } this.spanRecorder.spans = this.spanRecorder.spans.filter((span: Span) => { diff --git a/packages/core/src/tracing/span.ts b/packages/core/src/tracing/span.ts index e30f6e416675..c4e91a38f9c7 100644 --- a/packages/core/src/tracing/span.ts +++ b/packages/core/src/tracing/span.ts @@ -7,14 +7,14 @@ import type { SpanAttributes, SpanContext, SpanOrigin, + SpanTimeInput, TraceContext, Transaction, } from '@sentry/types'; import { dropUndefinedKeys, logger, timestampInSeconds, uuid4 } from '@sentry/utils'; import { DEBUG_BUILD } from '../debug-build'; -import { spanToTraceContext, spanToTraceHeader } from '../utils/spanUtils'; -import { ensureTimestampInSeconds } from './utils'; +import { spanTimeInputToSeconds, spanToTraceContext, spanToTraceHeader } from '../utils/spanUtils'; /** * Keeps track of finished spans for a given transaction @@ -300,7 +300,7 @@ export class Span implements SpanInterface { } /** @inheritdoc */ - public end(endTimestamp?: number): void { + public end(endTimestamp?: SpanTimeInput): void { if ( DEBUG_BUILD && // Don't call this for transactions @@ -313,8 +313,7 @@ export class Span implements SpanInterface { } } - this.endTimestamp = - typeof endTimestamp === 'number' ? ensureTimestampInSeconds(endTimestamp) : timestampInSeconds(); + this.endTimestamp = spanTimeInputToSeconds(endTimestamp); } /** diff --git a/packages/core/src/tracing/trace.ts b/packages/core/src/tracing/trace.ts index b5d49f4767a7..e31b27cfb6ff 100644 --- a/packages/core/src/tracing/trace.ts +++ b/packages/core/src/tracing/trace.ts @@ -1,4 +1,4 @@ -import type { Span, TransactionContext } from '@sentry/types'; +import type { Span, SpanTimeInput, TransactionContext } from '@sentry/types'; import { dropUndefinedKeys, logger, tracingContextFromHeaders } from '@sentry/utils'; import { DEBUG_BUILD } from '../debug-build'; @@ -7,6 +7,12 @@ import type { Hub } from '../hub'; import { getCurrentHub } from '../hub'; import { handleCallbackErrors } from '../utils/handleCallbackErrors'; import { hasTracingEnabled } from '../utils/hasTracingEnabled'; +import { spanTimeInputToSeconds } from '../utils/spanUtils'; + +interface StartSpanOptions extends TransactionContext { + /** A manually specified start time for the created `Span` object. */ + startTime?: SpanTimeInput; +} /** * Wraps a function with a transaction/span and finishes the span after the function is done. @@ -65,7 +71,7 @@ export function trace( * or you didn't set `tracesSampleRate`, this function will not generate spans * and the `span` returned from the callback will be undefined. */ -export function startSpan(context: TransactionContext, callback: (span: Span | undefined) => T): T { +export function startSpan(context: StartSpanOptions, callback: (span: Span | undefined) => T): T { const ctx = normalizeContext(context); return withScope(scope => { @@ -105,7 +111,7 @@ export const startActiveSpan = startSpan; * and the `span` returned from the callback will be undefined. */ export function startSpanManual( - context: TransactionContext, + context: StartSpanOptions, callback: (span: Span | undefined, finish: () => void) => T, ): T { const ctx = normalizeContext(context); @@ -143,17 +149,12 @@ export function startSpanManual( * 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 startInactiveSpan(context: TransactionContext): Span | undefined { +export function startInactiveSpan(context: StartSpanOptions): Span | undefined { if (!hasTracingEnabled()) { return undefined; } - 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 parentSpan = getActiveSpan(); return parentSpan ? parentSpan.startChild(ctx) : hub.startTransaction(ctx); @@ -238,12 +239,24 @@ function createChildSpanOrTransaction( return parentSpan ? parentSpan.startChild(ctx) : hub.startTransaction(ctx); } -function normalizeContext(context: TransactionContext): TransactionContext { +/** + * This converts StartSpanOptions to TransactionContext. + * For the most part (for now) we accept the same options, + * but some of them need to be transformed. + * + * Eventually the StartSpanOptions will be more aligned with OpenTelemetry. + */ +function normalizeContext(context: StartSpanOptions): 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; } + if (context.startTime) { + ctx.startTimestamp = spanTimeInputToSeconds(context.startTime); + delete ctx.startTime; + } + return ctx; } diff --git a/packages/core/src/tracing/transaction.ts b/packages/core/src/tracing/transaction.ts index 4652ab160143..7142ec3419e7 100644 --- a/packages/core/src/tracing/transaction.ts +++ b/packages/core/src/tracing/transaction.ts @@ -4,20 +4,20 @@ import type { DynamicSamplingContext, MeasurementUnit, Measurements, + SpanTimeInput, Transaction as TransactionInterface, TransactionContext, TransactionEvent, TransactionMetadata, } from '@sentry/types'; -import { dropUndefinedKeys, logger, timestampInSeconds } from '@sentry/utils'; +import { dropUndefinedKeys, logger } from '@sentry/utils'; import { DEBUG_BUILD } from '../debug-build'; import type { Hub } from '../hub'; import { getCurrentHub } from '../hub'; -import { spanToTraceContext } from '../utils/spanUtils'; +import { spanTimeInputToSeconds, spanToTraceContext } from '../utils/spanUtils'; import { getDynamicSamplingContextFromClient } from './dynamicSamplingContext'; import { Span as SpanClass, SpanRecorder } from './span'; -import { ensureTimestampInSeconds } from './utils'; /** JSDoc */ export class Transaction extends SpanClass implements TransactionInterface { @@ -147,9 +147,8 @@ export class Transaction extends SpanClass implements TransactionInterface { /** * @inheritDoc */ - public end(endTimestamp?: number): string | undefined { - const timestampInS = - typeof endTimestamp === 'number' ? ensureTimestampInSeconds(endTimestamp) : timestampInSeconds(); + public end(endTimestamp?: SpanTimeInput): string | undefined { + const timestampInS = spanTimeInputToSeconds(endTimestamp); const transaction = this._finishTransaction(timestampInS); if (!transaction) { return undefined; diff --git a/packages/core/src/tracing/utils.ts b/packages/core/src/tracing/utils.ts index 4c1d49780554..f1b4c0f1ae06 100644 --- a/packages/core/src/tracing/utils.ts +++ b/packages/core/src/tracing/utils.ts @@ -27,11 +27,3 @@ export { stripUrlQueryAndFragment } from '@sentry/utils'; * @deprecated Import this function from `@sentry/utils` instead */ export const extractTraceparentData = _extractTraceparentData; - -/** - * Converts a timestamp to second, if it was in milliseconds, or keeps it as second. - */ -export function ensureTimestampInSeconds(timestamp: number): number { - const isMs = timestamp > 9999999999; - return isMs ? timestamp / 1000 : timestamp; -} diff --git a/packages/core/src/utils/spanUtils.ts b/packages/core/src/utils/spanUtils.ts index 2dae21a78fca..9a99dd247bb6 100644 --- a/packages/core/src/utils/spanUtils.ts +++ b/packages/core/src/utils/spanUtils.ts @@ -1,5 +1,5 @@ -import type { Span, TraceContext } from '@sentry/types'; -import { dropUndefinedKeys, generateSentryTraceHeader } from '@sentry/utils'; +import type { Span, SpanTimeInput, TraceContext } from '@sentry/types'; +import { dropUndefinedKeys, generateSentryTraceHeader, timestampInSeconds } from '@sentry/utils'; /** * Convert a span to a trace context, which can be sent as the `trace` context in an event. @@ -26,3 +26,31 @@ export function spanToTraceContext(span: Span): TraceContext { export function spanToTraceHeader(span: Span): string { return generateSentryTraceHeader(span.traceId, span.spanId, span.sampled); } + +/** + * Convert a span time input intp a timestamp in seconds. + */ +export function spanTimeInputToSeconds(input: SpanTimeInput | undefined): number { + if (typeof input === 'number') { + return ensureTimestampInSeconds(input); + } + + if (Array.isArray(input)) { + // See {@link HrTime} for the array-based time format + return input[0] + input[1] / 1e9; + } + + if (input instanceof Date) { + return ensureTimestampInSeconds(input.getTime()); + } + + return timestampInSeconds(); +} + +/** + * Converts a timestamp to second, if it was in milliseconds, or keeps it as second. + */ +function ensureTimestampInSeconds(timestamp: number): number { + const isMs = timestamp > 9999999999; + return isMs ? timestamp / 1000 : timestamp; +} diff --git a/packages/core/test/lib/tracing/span.test.ts b/packages/core/test/lib/tracing/span.test.ts index c0b13df647f6..d439d6eb6ab1 100644 --- a/packages/core/test/lib/tracing/span.test.ts +++ b/packages/core/test/lib/tracing/span.test.ts @@ -1,3 +1,4 @@ +import { timestampInSeconds } from '@sentry/utils'; import { Span } from '../../../src'; describe('span', () => { @@ -174,6 +175,40 @@ describe('span', () => { }); }); + describe('end', () => { + it('works without endTimestamp', () => { + const span = new Span(); + const now = timestampInSeconds(); + span.end(); + + expect(span.endTimestamp).toBeGreaterThanOrEqual(now); + }); + + it('works with endTimestamp in seconds', () => { + const span = new Span(); + const timestamp = timestampInSeconds() - 1; + span.end(timestamp); + + expect(span.endTimestamp).toEqual(timestamp); + }); + + it('works with endTimestamp in milliseconds', () => { + const span = new Span(); + const timestamp = Date.now() - 1000; + span.end(timestamp); + + expect(span.endTimestamp).toEqual(timestamp / 1000); + }); + + it('works with endTimestamp in array form', () => { + const span = new Span(); + const seconds = Math.floor(timestampInSeconds() - 1); + span.end([seconds, 0]); + + expect(span.endTimestamp).toEqual(seconds); + }); + }); + // Ensure that attributes & data are merged together describe('_getData', () => { it('works without data & attributes', () => { diff --git a/packages/core/test/lib/tracing/trace.test.ts b/packages/core/test/lib/tracing/trace.test.ts index 30eac02c881f..4659ae2e112f 100644 --- a/packages/core/test/lib/tracing/trace.test.ts +++ b/packages/core/test/lib/tracing/trace.test.ts @@ -160,6 +160,14 @@ describe('startSpan', () => { expect(ref.spanRecorder.spans[1].status).toEqual(isError ? 'internal_error' : undefined); }); + it('allows to pass a `startTime`', () => { + const start = startSpan({ name: 'outer', startTime: [1234, 0] }, span => { + return span?.startTimestamp; + }); + + expect(start).toEqual(1234); + }); + it('allows for span to be mutated', async () => { let ref: any = undefined; client.on('finishTransaction', transaction => { @@ -222,6 +230,15 @@ describe('startSpanManual', () => { expect(getCurrentScope()).toBe(initialScope); expect(initialScope.getSpan()).toBe(undefined); }); + + it('allows to pass a `startTime`', () => { + const start = startSpanManual({ name: 'outer', startTime: [1234, 0] }, span => { + span?.end(); + return span?.startTimestamp; + }); + + expect(start).toEqual(1234); + }); }); describe('startInactiveSpan', () => { @@ -248,6 +265,11 @@ describe('startInactiveSpan', () => { expect(initialScope.getSpan()).toBeUndefined(); }); + + it('allows to pass a `startTime`', () => { + const span = startInactiveSpan({ name: 'outer', startTime: [1234, 0] }); + expect(span?.startTimestamp).toEqual(1234); + }); }); describe('continueTrace', () => { diff --git a/packages/core/test/lib/utils/spanUtils.test.ts b/packages/core/test/lib/utils/spanUtils.test.ts index c2ed4dd0d4cd..a6d84cf31166 100644 --- a/packages/core/test/lib/utils/spanUtils.test.ts +++ b/packages/core/test/lib/utils/spanUtils.test.ts @@ -1,5 +1,6 @@ -import { TRACEPARENT_REGEXP } from '@sentry/utils'; +import { TRACEPARENT_REGEXP, timestampInSeconds } from '@sentry/utils'; import { Span, spanToTraceHeader } from '../../../src'; +import { spanTimeInputToSeconds } from '../../../src/utils/spanUtils'; describe('spanToTraceHeader', () => { test('simple', () => { @@ -11,3 +12,37 @@ describe('spanToTraceHeader', () => { expect(spanToTraceHeader(span)).toMatch(TRACEPARENT_REGEXP); }); }); + +describe('spanTimeInputToSeconds', () => { + it('works with undefined', () => { + const now = timestampInSeconds(); + expect(spanTimeInputToSeconds(undefined)).toBeGreaterThanOrEqual(now); + }); + + it('works with a timestamp in seconds', () => { + const timestamp = timestampInSeconds(); + expect(spanTimeInputToSeconds(timestamp)).toEqual(timestamp); + }); + + it('works with a timestamp in milliseconds', () => { + const timestamp = Date.now(); + expect(spanTimeInputToSeconds(timestamp)).toEqual(timestamp / 1000); + }); + + it('works with a Date object', () => { + const timestamp = new Date(); + expect(spanTimeInputToSeconds(timestamp)).toEqual(timestamp.getTime() / 1000); + }); + + it('works with a simple array', () => { + const seconds = Math.floor(timestampInSeconds()); + const timestamp: [number, number] = [seconds, 0]; + expect(spanTimeInputToSeconds(timestamp)).toEqual(seconds); + }); + + it('works with a array with nanoseconds', () => { + const seconds = Math.floor(timestampInSeconds()); + const timestamp: [number, number] = [seconds, 9000]; + expect(spanTimeInputToSeconds(timestamp)).toEqual(seconds + 0.000009); + }); +}); diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index c74715d0041e..655962f592fc 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -89,7 +89,7 @@ export type { // eslint-disable-next-line deprecation/deprecation export type { Severity, SeverityLevel } from './severity'; -export type { Span, SpanContext, SpanOrigin, SpanAttributeValue, SpanAttributes } from './span'; +export type { Span, SpanContext, SpanOrigin, SpanAttributeValue, SpanAttributes, SpanTimeInput } from './span'; export type { StackFrame } from './stackframe'; export type { Stacktrace, StackParser, StackLineParser, StackLineParserFn } from './stacktrace'; export type { TextEncoderInternal } from './textencoder'; diff --git a/packages/types/src/opentelemetry.ts b/packages/types/src/opentelemetry.ts new file mode 100644 index 000000000000..14ceaab0c087 --- /dev/null +++ b/packages/types/src/opentelemetry.ts @@ -0,0 +1,31 @@ +/* + * Copyright The OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// This file contains vendored types from OpenTelemetry + +/** + * Defines High-Resolution Time. + * + * The first number, HrTime[0], is UNIX Epoch time in seconds since 00:00:00 UTC on 1 January 1970. + * The second number, HrTime[1], represents the partial second elapsed since Unix Epoch time represented by first number in nanoseconds. + * For example, 2021-01-01T12:30:10.150Z in UNIX Epoch time in milliseconds is represented as 1609504210150. + * The first number is calculated by converting and truncating the Epoch time in milliseconds to seconds: + * HrTime[0] = Math.trunc(1609504210150 / 1000) = 1609504210. + * The second number is calculated by converting the digits after the decimal point of the subtraction, (1609504210150 / 1000) - HrTime[0], to nanoseconds: + * HrTime[1] = Number((1609504210.150 - HrTime[0]).toFixed(9)) * 1e9 = 150000000. + * This is represented in HrTime format as [1609504210, 150000000]. + */ +export type HrTime = [number, number]; diff --git a/packages/types/src/span.ts b/packages/types/src/span.ts index 3e625dc7dd9f..840fcefe41ef 100644 --- a/packages/types/src/span.ts +++ b/packages/types/src/span.ts @@ -1,6 +1,7 @@ import type { TraceContext } from './context'; import type { Instrumenter } from './instrumenter'; import type { Primitive } from './misc'; +import type { HrTime } from './opentelemetry'; import type { Transaction } from './transaction'; type SpanOriginType = 'manual' | 'auto'; @@ -24,6 +25,9 @@ export type SpanAttributeValue = export type SpanAttributes = Record; +/** This type is aligned with the OpenTelemetry TimeInput type. */ +export type SpanTimeInput = HrTime | number | Date; + /** Interface holding all properties that can be set on a Span on creation. */ export interface SpanContext { /** @@ -159,7 +163,7 @@ export interface Span extends SpanContext { /** * End the current span. */ - end(endTimestamp?: number): void; + end(endTimestamp?: SpanTimeInput): void; /** * Sets the tag attribute on the current span. From 89ca41db586fcd8e36a1c6b2f9d298fe8f051e4e Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Fri, 5 Jan 2024 11:17:00 +0100 Subject: [PATCH 04/43] feat(core): Deprecate `startTransaction()` (#10073) This finally deprecates `startTransaction()`. There are only few remaining usages in our own code base, which we can refactor as we go. I chose to leave usages in E2E/integration tests for now, we can then refactor them when we get rid of these to ensure the behavior remains the same. --------- Co-authored-by: Lukas Stracke --- MIGRATION.md | 11 + .../create-next-app/pages/api/success.ts | 1 + .../node-express-app/src/app.ts | 1 + .../startTransaction/basic-usage/scenario.ts | 1 + .../with-nested-spans/scenario.ts | 1 + .../tracing-new/apollo-graphql/scenario.ts | 1 + .../auto-instrument/mongodb/scenario.ts | 1 + .../mysql/withConnect/scenario.ts | 1 + .../mysql/withoutCallback/scenario.ts | 1 + .../mysql/withoutConnect/scenario.ts | 1 + .../auto-instrument/pg/scenario.ts | 1 + .../suites/tracing-new/prisma-orm/scenario.ts | 1 + .../tracePropagationTargets/scenario.ts | 1 + .../suites/tracing/apollo-graphql/scenario.ts | 1 + .../auto-instrument/mongodb/scenario.ts | 1 + .../tracing/auto-instrument/mysql/scenario.ts | 1 + .../tracing/auto-instrument/pg/scenario.ts | 1 + .../suites/tracing/prisma-orm/scenario.ts | 1 + .../tracePropagationTargets/scenario.ts | 1 + packages/astro/src/index.server.ts | 1 + packages/browser/src/exports.ts | 1 + packages/bun/src/index.ts | 1 + packages/core/src/exports.ts | 3 + packages/core/src/hub.ts | 18 +- packages/core/src/index.ts | 1 + packages/core/src/tracing/trace.ts | 10 +- packages/core/test/lib/tracing/errors.test.ts | 33 ++- packages/deno/src/index.ts | 1 + packages/hub/src/index.ts | 1 + .../nextjs/src/common/utils/wrapperUtils.ts | 4 + packages/nextjs/test/clientSdk.test.ts | 10 +- packages/nextjs/test/serverSdk.test.ts | 5 +- packages/node/src/handlers.ts | 2 + packages/node/src/index.ts | 1 + packages/node/src/integrations/hapi/index.ts | 1 + packages/node/test/integrations/http.test.ts | 11 +- .../node/test/integrations/undici.test.ts | 189 ++++++++++-------- .../opentelemetry-node/src/spanprocessor.ts | 1 + .../test/custom/hubextensions.test.ts | 1 + packages/remix/src/index.server.ts | 1 + packages/remix/src/utils/instrumentServer.ts | 2 + packages/serverless/src/index.ts | 1 + packages/sveltekit/src/server/index.ts | 1 + packages/sveltekit/test/server/handle.test.ts | 36 ++-- .../test/browser/backgroundtab.test.ts | 23 +-- packages/tracing/test/index.test.ts | 1 + packages/types/src/hub.ts | 2 + packages/vercel-edge/src/index.ts | 1 + 48 files changed, 244 insertions(+), 148 deletions(-) diff --git a/MIGRATION.md b/MIGRATION.md index 99e9e3023732..d55a287fac99 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -8,6 +8,17 @@ npx @sentry/migr8@latest This will let you select which updates to run, and automatically update your code. Make sure to still review all code changes! +## Deprecate `startTransaction()` + +In v8, the old performance API `startTransaction()` (as well as `hub.startTransaction()`) will be removed. +Instead, use the new performance APIs: + +* `startSpan()` +* `startSpanManual()` +* `startInactiveSpan()` + +You can [read more about the new performance APIs here](./docs/v8-new-performance-apis.md). + ## Deprecate `Sentry.lastEventId()` and `hub.lastEventId()` `Sentry.lastEventId()` sometimes causes race conditons, so we are deprecating it in favour of the `beforeSend` callback. diff --git a/dev-packages/e2e-tests/test-applications/create-next-app/pages/api/success.ts b/dev-packages/e2e-tests/test-applications/create-next-app/pages/api/success.ts index 8a5a53f2e4dc..4b7b8703332a 100644 --- a/dev-packages/e2e-tests/test-applications/create-next-app/pages/api/success.ts +++ b/dev-packages/e2e-tests/test-applications/create-next-app/pages/api/success.ts @@ -3,6 +3,7 @@ import * as Sentry from '@sentry/nextjs'; import type { NextApiRequest, NextApiResponse } from 'next'; export default function handler(req: NextApiRequest, res: NextApiResponse) { + // eslint-disable-next-line deprecation/deprecation const transaction = Sentry.startTransaction({ name: 'test-transaction', op: 'e2e-test' }); Sentry.getCurrentHub().getScope().setSpan(transaction); diff --git a/dev-packages/e2e-tests/test-applications/node-express-app/src/app.ts b/dev-packages/e2e-tests/test-applications/node-express-app/src/app.ts index 269f8df45bbe..8ab1935c723e 100644 --- a/dev-packages/e2e-tests/test-applications/node-express-app/src/app.ts +++ b/dev-packages/e2e-tests/test-applications/node-express-app/src/app.ts @@ -34,6 +34,7 @@ app.get('/test-param/:param', function (req, res) { }); app.get('/test-transaction', async function (req, res) { + // eslint-disable-next-line deprecation/deprecation const transaction = Sentry.startTransaction({ name: 'test-transaction', op: 'e2e-test' }); Sentry.getCurrentScope().setSpan(transaction); diff --git a/dev-packages/node-integration-tests/suites/public-api/startTransaction/basic-usage/scenario.ts b/dev-packages/node-integration-tests/suites/public-api/startTransaction/basic-usage/scenario.ts index 1e4931a2bae7..70596da19716 100644 --- a/dev-packages/node-integration-tests/suites/public-api/startTransaction/basic-usage/scenario.ts +++ b/dev-packages/node-integration-tests/suites/public-api/startTransaction/basic-usage/scenario.ts @@ -9,6 +9,7 @@ Sentry.init({ tracesSampleRate: 1.0, }); +// eslint-disable-next-line deprecation/deprecation const transaction = Sentry.startTransaction({ name: 'test_transaction_1' }); transaction.end(); diff --git a/dev-packages/node-integration-tests/suites/public-api/startTransaction/with-nested-spans/scenario.ts b/dev-packages/node-integration-tests/suites/public-api/startTransaction/with-nested-spans/scenario.ts index a340de7b21fe..97fc6874770f 100644 --- a/dev-packages/node-integration-tests/suites/public-api/startTransaction/with-nested-spans/scenario.ts +++ b/dev-packages/node-integration-tests/suites/public-api/startTransaction/with-nested-spans/scenario.ts @@ -8,6 +8,7 @@ Sentry.init({ tracesSampleRate: 1.0, }); +// eslint-disable-next-line deprecation/deprecation const transaction = Sentry.startTransaction({ name: 'test_transaction_1' }); const span_1 = transaction.startChild({ op: 'span_1', diff --git a/dev-packages/node-integration-tests/suites/tracing-new/apollo-graphql/scenario.ts b/dev-packages/node-integration-tests/suites/tracing-new/apollo-graphql/scenario.ts index b37bd6df6fc9..6e9ea3a5f097 100644 --- a/dev-packages/node-integration-tests/suites/tracing-new/apollo-graphql/scenario.ts +++ b/dev-packages/node-integration-tests/suites/tracing-new/apollo-graphql/scenario.ts @@ -27,6 +27,7 @@ const server = new ApolloServer({ resolvers, }); +// eslint-disable-next-line deprecation/deprecation const transaction = Sentry.startTransaction({ name: 'test_transaction', op: 'transaction' }); Sentry.getCurrentScope().setSpan(transaction); diff --git a/dev-packages/node-integration-tests/suites/tracing-new/auto-instrument/mongodb/scenario.ts b/dev-packages/node-integration-tests/suites/tracing-new/auto-instrument/mongodb/scenario.ts index 36f8c3503832..63949c64d531 100644 --- a/dev-packages/node-integration-tests/suites/tracing-new/auto-instrument/mongodb/scenario.ts +++ b/dev-packages/node-integration-tests/suites/tracing-new/auto-instrument/mongodb/scenario.ts @@ -16,6 +16,7 @@ const client = new MongoClient(process.env.MONGO_URL || '', { }); async function run(): Promise { + // eslint-disable-next-line deprecation/deprecation const transaction = Sentry.startTransaction({ name: 'Test Transaction', op: 'transaction', diff --git a/dev-packages/node-integration-tests/suites/tracing-new/auto-instrument/mysql/withConnect/scenario.ts b/dev-packages/node-integration-tests/suites/tracing-new/auto-instrument/mysql/withConnect/scenario.ts index 9b033e72a669..a1b2343e4a61 100644 --- a/dev-packages/node-integration-tests/suites/tracing-new/auto-instrument/mysql/withConnect/scenario.ts +++ b/dev-packages/node-integration-tests/suites/tracing-new/auto-instrument/mysql/withConnect/scenario.ts @@ -19,6 +19,7 @@ connection.connect(function (err: unknown) { } }); +// eslint-disable-next-line deprecation/deprecation const transaction = Sentry.startTransaction({ op: 'transaction', name: 'Test Transaction', diff --git a/dev-packages/node-integration-tests/suites/tracing-new/auto-instrument/mysql/withoutCallback/scenario.ts b/dev-packages/node-integration-tests/suites/tracing-new/auto-instrument/mysql/withoutCallback/scenario.ts index 23d07f346875..aa6d8ca9ade3 100644 --- a/dev-packages/node-integration-tests/suites/tracing-new/auto-instrument/mysql/withoutCallback/scenario.ts +++ b/dev-packages/node-integration-tests/suites/tracing-new/auto-instrument/mysql/withoutCallback/scenario.ts @@ -19,6 +19,7 @@ connection.connect(function (err: unknown) { } }); +// eslint-disable-next-line deprecation/deprecation const transaction = Sentry.startTransaction({ op: 'transaction', name: 'Test Transaction', diff --git a/dev-packages/node-integration-tests/suites/tracing-new/auto-instrument/mysql/withoutConnect/scenario.ts b/dev-packages/node-integration-tests/suites/tracing-new/auto-instrument/mysql/withoutConnect/scenario.ts index bf9e4bf90e35..9a4b2dc4141a 100644 --- a/dev-packages/node-integration-tests/suites/tracing-new/auto-instrument/mysql/withoutConnect/scenario.ts +++ b/dev-packages/node-integration-tests/suites/tracing-new/auto-instrument/mysql/withoutConnect/scenario.ts @@ -13,6 +13,7 @@ const connection = mysql.createConnection({ password: 'docker', }); +// eslint-disable-next-line deprecation/deprecation const transaction = Sentry.startTransaction({ op: 'transaction', name: 'Test Transaction', diff --git a/dev-packages/node-integration-tests/suites/tracing-new/auto-instrument/pg/scenario.ts b/dev-packages/node-integration-tests/suites/tracing-new/auto-instrument/pg/scenario.ts index b41be87c9550..ad90c387ed20 100644 --- a/dev-packages/node-integration-tests/suites/tracing-new/auto-instrument/pg/scenario.ts +++ b/dev-packages/node-integration-tests/suites/tracing-new/auto-instrument/pg/scenario.ts @@ -8,6 +8,7 @@ Sentry.init({ integrations: [...Sentry.autoDiscoverNodePerformanceMonitoringIntegrations()], }); +// eslint-disable-next-line deprecation/deprecation const transaction = Sentry.startTransaction({ op: 'transaction', name: 'Test Transaction', diff --git a/dev-packages/node-integration-tests/suites/tracing-new/prisma-orm/scenario.ts b/dev-packages/node-integration-tests/suites/tracing-new/prisma-orm/scenario.ts index 20847871e7a1..26c3b81f6e44 100644 --- a/dev-packages/node-integration-tests/suites/tracing-new/prisma-orm/scenario.ts +++ b/dev-packages/node-integration-tests/suites/tracing-new/prisma-orm/scenario.ts @@ -13,6 +13,7 @@ Sentry.init({ }); async function run(): Promise { + // eslint-disable-next-line deprecation/deprecation const transaction = Sentry.startTransaction({ name: 'Test Transaction', op: 'transaction', diff --git a/dev-packages/node-integration-tests/suites/tracing-new/tracePropagationTargets/scenario.ts b/dev-packages/node-integration-tests/suites/tracing-new/tracePropagationTargets/scenario.ts index 7c86686cbba8..2a8a34dae4f9 100644 --- a/dev-packages/node-integration-tests/suites/tracing-new/tracePropagationTargets/scenario.ts +++ b/dev-packages/node-integration-tests/suites/tracing-new/tracePropagationTargets/scenario.ts @@ -10,6 +10,7 @@ Sentry.init({ integrations: [new Sentry.Integrations.Http({ tracing: true })], }); +// eslint-disable-next-line deprecation/deprecation const transaction = Sentry.startTransaction({ name: 'test_transaction' }); Sentry.getCurrentScope().setSpan(transaction); diff --git a/dev-packages/node-integration-tests/suites/tracing/apollo-graphql/scenario.ts b/dev-packages/node-integration-tests/suites/tracing/apollo-graphql/scenario.ts index 7b34ffab0613..aac11a641032 100644 --- a/dev-packages/node-integration-tests/suites/tracing/apollo-graphql/scenario.ts +++ b/dev-packages/node-integration-tests/suites/tracing/apollo-graphql/scenario.ts @@ -29,6 +29,7 @@ const server = new ApolloServer({ resolvers, }); +// eslint-disable-next-line deprecation/deprecation const transaction = Sentry.startTransaction({ name: 'test_transaction', op: 'transaction' }); Sentry.getCurrentScope().setSpan(transaction); diff --git a/dev-packages/node-integration-tests/suites/tracing/auto-instrument/mongodb/scenario.ts b/dev-packages/node-integration-tests/suites/tracing/auto-instrument/mongodb/scenario.ts index cff8329d22a3..f11e7f39d923 100644 --- a/dev-packages/node-integration-tests/suites/tracing/auto-instrument/mongodb/scenario.ts +++ b/dev-packages/node-integration-tests/suites/tracing/auto-instrument/mongodb/scenario.ts @@ -17,6 +17,7 @@ const client = new MongoClient(process.env.MONGO_URL || '', { }); async function run(): Promise { + // eslint-disable-next-line deprecation/deprecation const transaction = Sentry.startTransaction({ name: 'Test Transaction', op: 'transaction', diff --git a/dev-packages/node-integration-tests/suites/tracing/auto-instrument/mysql/scenario.ts b/dev-packages/node-integration-tests/suites/tracing/auto-instrument/mysql/scenario.ts index 30f9fb368b3a..de77c5fa7c8a 100644 --- a/dev-packages/node-integration-tests/suites/tracing/auto-instrument/mysql/scenario.ts +++ b/dev-packages/node-integration-tests/suites/tracing/auto-instrument/mysql/scenario.ts @@ -20,6 +20,7 @@ connection.connect(function (err: unknown) { } }); +// eslint-disable-next-line deprecation/deprecation const transaction = Sentry.startTransaction({ op: 'transaction', name: 'Test Transaction', diff --git a/dev-packages/node-integration-tests/suites/tracing/auto-instrument/pg/scenario.ts b/dev-packages/node-integration-tests/suites/tracing/auto-instrument/pg/scenario.ts index 95248c82f075..1963c61fc31b 100644 --- a/dev-packages/node-integration-tests/suites/tracing/auto-instrument/pg/scenario.ts +++ b/dev-packages/node-integration-tests/suites/tracing/auto-instrument/pg/scenario.ts @@ -9,6 +9,7 @@ Sentry.init({ tracesSampleRate: 1.0, }); +// eslint-disable-next-line deprecation/deprecation const transaction = Sentry.startTransaction({ op: 'transaction', name: 'Test Transaction', diff --git a/dev-packages/node-integration-tests/suites/tracing/prisma-orm/scenario.ts b/dev-packages/node-integration-tests/suites/tracing/prisma-orm/scenario.ts index 7e8a7c6eca5f..087d5ce860f2 100644 --- a/dev-packages/node-integration-tests/suites/tracing/prisma-orm/scenario.ts +++ b/dev-packages/node-integration-tests/suites/tracing/prisma-orm/scenario.ts @@ -15,6 +15,7 @@ Sentry.init({ }); async function run(): Promise { + // eslint-disable-next-line deprecation/deprecation const transaction = Sentry.startTransaction({ name: 'Test Transaction', op: 'transaction', diff --git a/dev-packages/node-integration-tests/suites/tracing/tracePropagationTargets/scenario.ts b/dev-packages/node-integration-tests/suites/tracing/tracePropagationTargets/scenario.ts index 9fdeba1fcb95..f1e395992b46 100644 --- a/dev-packages/node-integration-tests/suites/tracing/tracePropagationTargets/scenario.ts +++ b/dev-packages/node-integration-tests/suites/tracing/tracePropagationTargets/scenario.ts @@ -12,6 +12,7 @@ Sentry.init({ integrations: [new Sentry.Integrations.Http({ tracing: true })], }); +// eslint-disable-next-line deprecation/deprecation const transaction = Sentry.startTransaction({ name: 'test_transaction' }); Sentry.getCurrentScope().setSpan(transaction); diff --git a/packages/astro/src/index.server.ts b/packages/astro/src/index.server.ts index 6a1011053007..b038e215496d 100644 --- a/packages/astro/src/index.server.ts +++ b/packages/astro/src/index.server.ts @@ -32,6 +32,7 @@ export { Hub, makeMain, Scope, + // eslint-disable-next-line deprecation/deprecation startTransaction, SDK_VERSION, setContext, diff --git a/packages/browser/src/exports.ts b/packages/browser/src/exports.ts index 0d87e7898283..f63fe20fdead 100644 --- a/packages/browser/src/exports.ts +++ b/packages/browser/src/exports.ts @@ -45,6 +45,7 @@ export { lastEventId, makeMain, Scope, + // eslint-disable-next-line deprecation/deprecation startTransaction, getActiveSpan, startSpan, diff --git a/packages/bun/src/index.ts b/packages/bun/src/index.ts index 964e34560bdf..749299badd74 100644 --- a/packages/bun/src/index.ts +++ b/packages/bun/src/index.ts @@ -52,6 +52,7 @@ export { makeMain, runWithAsyncContext, Scope, + // eslint-disable-next-line deprecation/deprecation startTransaction, SDK_VERSION, setContext, diff --git a/packages/core/src/exports.ts b/packages/core/src/exports.ts index d479a3093d51..09989215ef3e 100644 --- a/packages/core/src/exports.ts +++ b/packages/core/src/exports.ts @@ -190,11 +190,14 @@ export function withScope(callback: (scope: Scope) => T): T { * default values). See {@link Options.tracesSampler}. * * @returns The transaction which was just started + * + * @deprecated Use `startSpan()`, `startSpanManual()` or `startInactiveSpan()` instead. */ export function startTransaction( context: TransactionContext, customSamplingContext?: CustomSamplingContext, ): ReturnType { + // eslint-disable-next-line deprecation/deprecation return getCurrentHub().startTransaction({ ...context }, customSamplingContext); } diff --git a/packages/core/src/hub.ts b/packages/core/src/hub.ts index 07f1310f94a2..d05b80410d06 100644 --- a/packages/core/src/hub.ts +++ b/packages/core/src/hub.ts @@ -440,7 +440,23 @@ export class Hub implements HubInterface { } /** - * @inheritDoc + * Starts a new `Transaction` and returns it. This is the entry point to manual tracing instrumentation. + * + * A tree structure can be built by adding child spans to the transaction, and child spans to other spans. To start a + * new child span within the transaction or any span, call the respective `.startChild()` method. + * + * Every child span must be finished before the transaction is finished, otherwise the unfinished spans are discarded. + * + * The transaction must be finished with a call to its `.end()` method, at which point the transaction with all its + * finished child spans will be sent to Sentry. + * + * @param context Properties of the new `Transaction`. + * @param customSamplingContext Information given to the transaction sampling function (along with context-dependent + * default values). See {@link Options.tracesSampler}. + * + * @returns The transaction which was just started + * + * @deprecated Use `startSpan()`, `startSpanManual()` or `startInactiveSpan()` instead. */ public startTransaction(context: TransactionContext, customSamplingContext?: CustomSamplingContext): Transaction { const result = this._callExtensionMethod('startTransaction', context, customSamplingContext); diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 20748ff11589..2c423f0744c3 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -19,6 +19,7 @@ export { flush, // eslint-disable-next-line deprecation/deprecation lastEventId, + // eslint-disable-next-line deprecation/deprecation startTransaction, setContext, setExtra, diff --git a/packages/core/src/tracing/trace.ts b/packages/core/src/tracing/trace.ts index e31b27cfb6ff..d5b6aef1d1fe 100644 --- a/packages/core/src/tracing/trace.ts +++ b/packages/core/src/tracing/trace.ts @@ -157,7 +157,10 @@ export function startInactiveSpan(context: StartSpanOptions): Span | undefined { const ctx = normalizeContext(context); const hub = getCurrentHub(); const parentSpan = getActiveSpan(); - return parentSpan ? parentSpan.startChild(ctx) : hub.startTransaction(ctx); + return parentSpan + ? parentSpan.startChild(ctx) + : // eslint-disable-next-line deprecation/deprecation + hub.startTransaction(ctx); } /** @@ -236,7 +239,10 @@ function createChildSpanOrTransaction( if (!hasTracingEnabled()) { return undefined; } - return parentSpan ? parentSpan.startChild(ctx) : hub.startTransaction(ctx); + return parentSpan + ? parentSpan.startChild(ctx) + : // eslint-disable-next-line deprecation/deprecation + hub.startTransaction(ctx); } /** diff --git a/packages/core/test/lib/tracing/errors.test.ts b/packages/core/test/lib/tracing/errors.test.ts index 60b5db5c0c1d..d2714901d39e 100644 --- a/packages/core/test/lib/tracing/errors.test.ts +++ b/packages/core/test/lib/tracing/errors.test.ts @@ -1,5 +1,5 @@ import { BrowserClient } from '@sentry/browser'; -import { Hub, addTracingExtensions, makeMain } from '@sentry/core'; +import { Hub, addTracingExtensions, makeMain, startInactiveSpan, startSpan } from '@sentry/core'; import type { HandlerDataError, HandlerDataUnhandledRejection } from '@sentry/types'; import { getDefaultBrowserClientOptions } from '../../../../tracing/test/testutils'; @@ -30,19 +30,14 @@ beforeAll(() => { }); describe('registerErrorHandlers()', () => { - let hub: Hub; beforeEach(() => { mockAddGlobalErrorInstrumentationHandler.mockClear(); mockAddGlobalUnhandledRejectionInstrumentationHandler.mockClear(); - const options = getDefaultBrowserClientOptions(); - hub = new Hub(new BrowserClient(options)); + const options = getDefaultBrowserClientOptions({ enableTracing: true }); + const hub = new Hub(new BrowserClient(options)); makeMain(hub); }); - afterEach(() => { - hub.getScope().setSpan(undefined); - }); - it('registers error instrumentation', () => { registerErrorInstrumentation(); expect(mockAddGlobalErrorInstrumentationHandler).toHaveBeenCalledTimes(1); @@ -53,7 +48,8 @@ describe('registerErrorHandlers()', () => { it('does not set status if transaction is not on scope', () => { registerErrorInstrumentation(); - const transaction = hub.startTransaction({ name: 'test' }); + + const transaction = startInactiveSpan({ name: 'test' })!; expect(transaction.status).toBe(undefined); mockErrorCallback({} as HandlerDataError); @@ -66,22 +62,19 @@ describe('registerErrorHandlers()', () => { it('sets status for transaction on scope on error', () => { registerErrorInstrumentation(); - const transaction = hub.startTransaction({ name: 'test' }); - hub.getScope().setSpan(transaction); - - mockErrorCallback({} as HandlerDataError); - expect(transaction.status).toBe('internal_error'); - transaction.end(); + startSpan({ name: 'test' }, span => { + mockErrorCallback({} as HandlerDataError); + expect(span?.status).toBe('internal_error'); + }); }); it('sets status for transaction on scope on unhandledrejection', () => { registerErrorInstrumentation(); - const transaction = hub.startTransaction({ name: 'test' }); - hub.getScope().setSpan(transaction); - mockUnhandledRejectionCallback({}); - expect(transaction.status).toBe('internal_error'); - transaction.end(); + startSpan({ name: 'test' }, span => { + mockUnhandledRejectionCallback({}); + expect(span?.status).toBe('internal_error'); + }); }); }); diff --git a/packages/deno/src/index.ts b/packages/deno/src/index.ts index 2530e2f7bdc5..f70d511ae8a3 100644 --- a/packages/deno/src/index.ts +++ b/packages/deno/src/index.ts @@ -51,6 +51,7 @@ export { makeMain, runWithAsyncContext, Scope, + // eslint-disable-next-line deprecation/deprecation startTransaction, SDK_VERSION, setContext, diff --git a/packages/hub/src/index.ts b/packages/hub/src/index.ts index 057d0e6a9975..579b5b932e7e 100644 --- a/packages/hub/src/index.ts +++ b/packages/hub/src/index.ts @@ -118,6 +118,7 @@ export const configureScope = configureScopeCore; /** * @deprecated This export has moved to @sentry/core. The @sentry/hub package will be removed in v8. */ +// eslint-disable-next-line deprecation/deprecation export const startTransaction = startTransactionCore; /** diff --git a/packages/nextjs/src/common/utils/wrapperUtils.ts b/packages/nextjs/src/common/utils/wrapperUtils.ts index e25220ce61c2..da0c11df8640 100644 --- a/packages/nextjs/src/common/utils/wrapperUtils.ts +++ b/packages/nextjs/src/common/utils/wrapperUtils.ts @@ -100,6 +100,8 @@ export function withTracedServerSideDataFetcher Pr if (platformSupportsStreaming()) { let spanToContinue: Span; if (previousSpan === undefined) { + // TODO: Refactor this to use `startSpan()` + // eslint-disable-next-line deprecation/deprecation const newTransaction = startTransaction( { op: 'http.server', @@ -136,6 +138,8 @@ export function withTracedServerSideDataFetcher Pr status: 'ok', }); } else { + // TODO: Refactor this to use `startSpan()` + // eslint-disable-next-line deprecation/deprecation dataFetcherSpan = startTransaction({ op: 'function.nextjs', name: `${options.dataFetchingMethodName} (${options.dataFetcherRouteName})`, diff --git a/packages/nextjs/test/clientSdk.test.ts b/packages/nextjs/test/clientSdk.test.ts index 1b35f82cbfe8..fd43f1bee9ad 100644 --- a/packages/nextjs/test/clientSdk.test.ts +++ b/packages/nextjs/test/clientSdk.test.ts @@ -1,6 +1,6 @@ import { BaseClient, getCurrentHub } from '@sentry/core'; import * as SentryReact from '@sentry/react'; -import { BrowserTracing, WINDOW } from '@sentry/react'; +import { BrowserTracing, WINDOW, getCurrentScope } from '@sentry/react'; import type { Integration } from '@sentry/types'; import type { UserIntegrationsFunction } from '@sentry/utils'; import { logger } from '@sentry/utils'; @@ -89,8 +89,12 @@ describe('Client init()', () => { const hub = getCurrentHub(); const transportSend = jest.spyOn(hub.getClient()!.getTransport()!, 'send'); - const transaction = hub.startTransaction({ name: '/404' }); - transaction.end(); + // Ensure we have no current span, so our next span is a transaction + getCurrentScope().setSpan(undefined); + + SentryReact.startSpan({ name: '/404' }, () => { + // noop + }); expect(transportSend).not.toHaveBeenCalled(); expect(captureEvent.mock.results[0].value).toBeUndefined(); diff --git a/packages/nextjs/test/serverSdk.test.ts b/packages/nextjs/test/serverSdk.test.ts index 0813d4931874..6201ddf34a53 100644 --- a/packages/nextjs/test/serverSdk.test.ts +++ b/packages/nextjs/test/serverSdk.test.ts @@ -105,8 +105,9 @@ describe('Server init()', () => { const hub = getCurrentHub(); const transportSend = jest.spyOn(hub.getClient()!.getTransport()!, 'send'); - const transaction = hub.startTransaction({ name: '/404' }); - transaction.end(); + SentryNode.startSpan({ name: '/404' }, () => { + // noop + }); // We need to flush because the event processor pipeline is async whereas transaction.end() is sync. await SentryNode.flush(); diff --git a/packages/node/src/handlers.ts b/packages/node/src/handlers.ts index 9a4bb08bfb4b..6d4c6f5a4494 100644 --- a/packages/node/src/handlers.ts +++ b/packages/node/src/handlers.ts @@ -63,6 +63,8 @@ export function tracingHandler(): ( const [name, source] = extractPathForTransaction(req, { path: true, method: true }); const transaction = continueTrace({ sentryTrace, baggage }, ctx => + // TODO: Refactor this to use `startSpan()` + // eslint-disable-next-line deprecation/deprecation startTransaction( { name, diff --git a/packages/node/src/index.ts b/packages/node/src/index.ts index e712a4fc0d0d..47206462b937 100644 --- a/packages/node/src/index.ts +++ b/packages/node/src/index.ts @@ -51,6 +51,7 @@ export { makeMain, runWithAsyncContext, Scope, + // eslint-disable-next-line deprecation/deprecation startTransaction, SDK_VERSION, setContext, diff --git a/packages/node/src/integrations/hapi/index.ts b/packages/node/src/integrations/hapi/index.ts index 732839d3995c..409470680a73 100644 --- a/packages/node/src/integrations/hapi/index.ts +++ b/packages/node/src/integrations/hapi/index.ts @@ -75,6 +75,7 @@ export const hapiTracingPlugin = { baggage: request.headers['baggage'] || undefined, }, transactionContext => { + // eslint-disable-next-line deprecation/deprecation return startTransaction({ ...transactionContext, op: 'hapi.request', diff --git a/packages/node/test/integrations/http.test.ts b/packages/node/test/integrations/http.test.ts index 42eb9391ec52..91d5a6c0e20d 100644 --- a/packages/node/test/integrations/http.test.ts +++ b/packages/node/test/integrations/http.test.ts @@ -1,6 +1,8 @@ import * as http from 'http'; import * as https from 'https'; -import type { Span, Transaction } from '@sentry/core'; +import type { Span } from '@sentry/core'; +import { Transaction } from '@sentry/core'; +import { startInactiveSpan } from '@sentry/core'; import * as sentryCore from '@sentry/core'; import { Hub, addTracingExtensions } from '@sentry/core'; import type { TransactionContext } from '@sentry/types'; @@ -36,6 +38,7 @@ describe('tracing', () => { ...customOptions, }); const hub = new Hub(new NodeClient(options)); + sentryCore.makeMain(hub); addTracingExtensions(); hub.getScope().setUser({ @@ -47,12 +50,14 @@ describe('tracing', () => { jest.spyOn(sentryCore, 'getCurrentScope').mockImplementation(() => hub.getScope()); jest.spyOn(sentryCore, 'getClient').mockReturnValue(hub.getClient()); - const transaction = hub.startTransaction({ + const transaction = startInactiveSpan({ name: 'dogpark', traceId: '12312012123120121231201212312012', ...customContext, }); + expect(transaction).toBeInstanceOf(Transaction); + hub.getScope().setSpan(transaction); return transaction; @@ -367,7 +372,7 @@ describe('tracing', () => { function createTransactionAndPutOnScope(hub: Hub) { addTracingExtensions(); - const transaction = hub.startTransaction({ name: 'dogpark' }); + const transaction = startInactiveSpan({ name: 'dogpark' }); hub.getScope().setSpan(transaction); return transaction; } diff --git a/packages/node/test/integrations/undici.test.ts b/packages/node/test/integrations/undici.test.ts index 078d90c98721..1d7f847b2503 100644 --- a/packages/node/test/integrations/undici.test.ts +++ b/packages/node/test/integrations/undici.test.ts @@ -1,5 +1,5 @@ import * as http from 'http'; -import type { Transaction } from '@sentry/core'; +import { Transaction, startSpan } from '@sentry/core'; import { spanToTraceHeader } from '@sentry/core'; import { Hub, makeMain, runWithAsyncContext } from '@sentry/core'; import type { fetch as FetchType } from 'undici'; @@ -106,65 +106,73 @@ conditionalTest({ min: 16 })('Undici integration', () => { }, ], ])('creates a span with a %s', async (_: string, request, requestInit, expected) => { - const transaction = hub.startTransaction({ name: 'test-transaction' }) as Transaction; - hub.getScope().setSpan(transaction); + await startSpan({ name: 'outer-span' }, async outerSpan => { + await fetch(request, requestInit); - await fetch(request, requestInit); + expect(outerSpan).toBeInstanceOf(Transaction); + const spans = (outerSpan as Transaction).spanRecorder?.spans || []; - expect(transaction.spanRecorder?.spans.length).toBe(2); + expect(spans.length).toBe(2); - const span = transaction.spanRecorder?.spans[1]; - expect(span).toEqual(expect.objectContaining(expected)); + const span = spans[1]; + expect(span).toEqual(expect.objectContaining(expected)); + }); }); it('creates a span with internal errors', async () => { - const transaction = hub.startTransaction({ name: 'test-transaction' }) as Transaction; - hub.getScope().setSpan(transaction); + await startSpan({ name: 'outer-span' }, async outerSpan => { + try { + await fetch('http://a-url-that-no-exists.com'); + } catch (e) { + // ignore + } - try { - await fetch('http://a-url-that-no-exists.com'); - } catch (e) { - // ignore - } + expect(outerSpan).toBeInstanceOf(Transaction); + const spans = (outerSpan as Transaction).spanRecorder?.spans || []; - expect(transaction.spanRecorder?.spans.length).toBe(2); + expect(spans.length).toBe(2); - const span = transaction.spanRecorder?.spans[1]; - expect(span).toEqual(expect.objectContaining({ status: 'internal_error' })); + const span = spans[1]; + expect(span).toEqual(expect.objectContaining({ status: 'internal_error' })); + }); }); it('creates a span for invalid looking urls', async () => { - const transaction = hub.startTransaction({ name: 'test-transaction' }) as Transaction; - hub.getScope().setSpan(transaction); - - try { - // Intentionally add // to the url - // fetch accepts this URL, but throws an error later on - await fetch('http://a-url-that-no-exists.com//'); - } catch (e) { - // ignore - } - - expect(transaction.spanRecorder?.spans.length).toBe(2); - - const span = transaction.spanRecorder?.spans[1]; - expect(span).toEqual(expect.objectContaining({ description: 'GET http://a-url-that-no-exists.com//' })); - expect(span).toEqual(expect.objectContaining({ status: 'internal_error' })); + await startSpan({ name: 'outer-span' }, async outerSpan => { + try { + // Intentionally add // to the url + // fetch accepts this URL, but throws an error later on + await fetch('http://a-url-that-no-exists.com//'); + } catch (e) { + // ignore + } + + expect(outerSpan).toBeInstanceOf(Transaction); + const spans = (outerSpan as Transaction).spanRecorder?.spans || []; + + expect(spans.length).toBe(2); + + const span = spans[1]; + expect(span).toEqual(expect.objectContaining({ description: 'GET http://a-url-that-no-exists.com//' })); + expect(span).toEqual(expect.objectContaining({ status: 'internal_error' })); + }); }); it('does not create a span for sentry requests', async () => { - const transaction = hub.startTransaction({ name: 'test-transaction' }) as Transaction; - hub.getScope().setSpan(transaction); + await startSpan({ name: 'outer-span' }, async outerSpan => { + try { + await fetch(`${SENTRY_DSN}/sub/route`, { + method: 'POST', + }); + } catch (e) { + // ignore + } - try { - await fetch(`${SENTRY_DSN}/sub/route`, { - method: 'POST', - }); - } catch (e) { - // ignore - } + expect(outerSpan).toBeInstanceOf(Transaction); + const spans = (outerSpan as Transaction).spanRecorder?.spans || []; - expect(transaction.spanRecorder?.spans.length).toBe(1); + expect(spans.length).toBe(1); + }); }); it('does not create a span if there is no active spans', async () => { @@ -178,20 +186,22 @@ conditionalTest({ min: 16 })('Undici integration', () => { }); it('does create a span if `shouldCreateSpanForRequest` is defined', async () => { - const transaction = hub.startTransaction({ name: 'test-transaction' }) as Transaction; - hub.getScope().setSpan(transaction); + await startSpan({ name: 'outer-span' }, async outerSpan => { + expect(outerSpan).toBeInstanceOf(Transaction); + const spans = (outerSpan as Transaction).spanRecorder?.spans || []; - const undoPatch = patchUndici({ shouldCreateSpanForRequest: url => url.includes('yes') }); + const undoPatch = patchUndici({ shouldCreateSpanForRequest: url => url.includes('yes') }); - await fetch('http://localhost:18100/no', { method: 'POST' }); + await fetch('http://localhost:18100/no', { method: 'POST' }); - expect(transaction.spanRecorder?.spans.length).toBe(1); + expect(spans.length).toBe(1); - await fetch('http://localhost:18100/yes', { method: 'POST' }); + await fetch('http://localhost:18100/yes', { method: 'POST' }); - expect(transaction.spanRecorder?.spans.length).toBe(2); + expect(spans.length).toBe(2); - undoPatch(); + undoPatch(); + }); }); // This flakes on CI for some reason: https://github.com/getsentry/sentry-javascript/pull/8449 @@ -200,18 +210,20 @@ conditionalTest({ min: 16 })('Undici integration', () => { expect.assertions(3); await runWithAsyncContext(async () => { - const transaction = hub.startTransaction({ name: 'test-transaction' }) as Transaction; - hub.getScope().setSpan(transaction); + await startSpan({ name: 'outer-span' }, async outerSpan => { + expect(outerSpan).toBeInstanceOf(Transaction); + const spans = (outerSpan as Transaction).spanRecorder?.spans || []; - await fetch('http://localhost:18100', { method: 'POST' }); + await fetch('http://localhost:18100', { method: 'POST' }); - expect(transaction.spanRecorder?.spans.length).toBe(2); - const span = transaction.spanRecorder?.spans[1]; + expect(spans.length).toBe(2); + const span = spans[1]; - expect(requestHeaders['sentry-trace']).toEqual(spanToTraceHeader(span!)); - expect(requestHeaders['baggage']).toEqual( - `sentry-environment=production,sentry-public_key=0,sentry-trace_id=${transaction.traceId},sentry-sample_rate=1,sentry-transaction=test-transaction`, - ); + expect(requestHeaders['sentry-trace']).toEqual(spanToTraceHeader(span!)); + expect(requestHeaders['baggage']).toEqual( + `sentry-environment=production,sentry-public_key=0,sentry-trace_id=${span.traceId},sentry-sample_rate=1,sentry-transaction=test-transaction`, + ); + }); }); }); @@ -233,59 +245,62 @@ conditionalTest({ min: 16 })('Undici integration', () => { // This flakes on CI for some reason: https://github.com/getsentry/sentry-javascript/pull/8449 // eslint-disable-next-line jest/no-disabled-tests it.skip('attaches headers if `shouldCreateSpanForRequest` does not create a span using propagation context', async () => { - const transaction = hub.startTransaction({ name: 'test-transaction' }) as Transaction; const scope = hub.getScope(); const propagationContext = scope.getPropagationContext(); - scope.setSpan(transaction); + await startSpan({ name: 'outer-span' }, async outerSpan => { + expect(outerSpan).toBeInstanceOf(Transaction); - const undoPatch = patchUndici({ shouldCreateSpanForRequest: url => url.includes('yes') }); + const undoPatch = patchUndici({ shouldCreateSpanForRequest: url => url.includes('yes') }); - await fetch('http://localhost:18100/no', { method: 'POST' }); + await fetch('http://localhost:18100/no', { method: 'POST' }); - expect(requestHeaders['sentry-trace']).toBeDefined(); - expect(requestHeaders['baggage']).toBeDefined(); + expect(requestHeaders['sentry-trace']).toBeDefined(); + expect(requestHeaders['baggage']).toBeDefined(); - expect(requestHeaders['sentry-trace'].includes(propagationContext.traceId)).toBe(true); - const firstSpanId = requestHeaders['sentry-trace'].split('-')[1]; + expect(requestHeaders['sentry-trace'].includes(propagationContext.traceId)).toBe(true); + const firstSpanId = requestHeaders['sentry-trace'].split('-')[1]; - await fetch('http://localhost:18100/yes', { method: 'POST' }); + await fetch('http://localhost:18100/yes', { method: 'POST' }); - expect(requestHeaders['sentry-trace']).toBeDefined(); - expect(requestHeaders['baggage']).toBeDefined(); + expect(requestHeaders['sentry-trace']).toBeDefined(); + expect(requestHeaders['baggage']).toBeDefined(); - expect(requestHeaders['sentry-trace'].includes(propagationContext.traceId)).toBe(false); + expect(requestHeaders['sentry-trace'].includes(propagationContext.traceId)).toBe(false); - const secondSpanId = requestHeaders['sentry-trace'].split('-')[1]; - expect(firstSpanId).not.toBe(secondSpanId); + const secondSpanId = requestHeaders['sentry-trace'].split('-')[1]; + expect(firstSpanId).not.toBe(secondSpanId); - undoPatch(); + undoPatch(); + }); }); // This flakes on CI for some reason: https://github.com/getsentry/sentry-javascript/pull/8449 // eslint-disable-next-line jest/no-disabled-tests it.skip('uses tracePropagationTargets', async () => { - const transaction = hub.startTransaction({ name: 'test-transaction' }) as Transaction; - hub.getScope().setSpan(transaction); - const client = new NodeClient({ ...DEFAULT_OPTIONS, tracePropagationTargets: ['/yes'] }); hub.bindClient(client); - expect(transaction.spanRecorder?.spans.length).toBe(1); + await startSpan({ name: 'outer-span' }, async outerSpan => { + expect(outerSpan).toBeInstanceOf(Transaction); + const spans = (outerSpan as Transaction).spanRecorder?.spans || []; + + expect(spans.length).toBe(1); - await fetch('http://localhost:18100/no', { method: 'POST' }); + await fetch('http://localhost:18100/no', { method: 'POST' }); - expect(transaction.spanRecorder?.spans.length).toBe(2); + expect(spans.length).toBe(2); - expect(requestHeaders['sentry-trace']).toBeUndefined(); - expect(requestHeaders['baggage']).toBeUndefined(); + expect(requestHeaders['sentry-trace']).toBeUndefined(); + expect(requestHeaders['baggage']).toBeUndefined(); - await fetch('http://localhost:18100/yes', { method: 'POST' }); + await fetch('http://localhost:18100/yes', { method: 'POST' }); - expect(transaction.spanRecorder?.spans.length).toBe(3); + expect(spans.length).toBe(3); - expect(requestHeaders['sentry-trace']).toBeDefined(); - expect(requestHeaders['baggage']).toBeDefined(); + expect(requestHeaders['sentry-trace']).toBeDefined(); + expect(requestHeaders['baggage']).toBeDefined(); + }); }); it('adds a breadcrumb on request', async () => { diff --git a/packages/opentelemetry-node/src/spanprocessor.ts b/packages/opentelemetry-node/src/spanprocessor.ts index bb2372a3c2b4..5b1a1684c9cf 100644 --- a/packages/opentelemetry-node/src/spanprocessor.ts +++ b/packages/opentelemetry-node/src/spanprocessor.ts @@ -66,6 +66,7 @@ export class SentrySpanProcessor implements OtelSpanProcessor { setSentrySpan(otelSpanId, sentryChildSpan); } else { const traceCtx = getTraceData(otelSpan, parentContext); + // eslint-disable-next-line deprecation/deprecation const transaction = getCurrentHub().startTransaction({ name: otelSpan.name, ...traceCtx, diff --git a/packages/opentelemetry/test/custom/hubextensions.test.ts b/packages/opentelemetry/test/custom/hubextensions.test.ts index 44b7b941161d..6e246763a92a 100644 --- a/packages/opentelemetry/test/custom/hubextensions.test.ts +++ b/packages/opentelemetry/test/custom/hubextensions.test.ts @@ -14,6 +14,7 @@ describe('hubextensions', () => { const mockConsole = jest.spyOn(console, 'warn').mockImplementation(() => {}); + // eslint-disable-next-line deprecation/deprecation const transaction = getCurrentHub().startTransaction({ name: 'test' }); expect(transaction).toEqual({}); diff --git a/packages/remix/src/index.server.ts b/packages/remix/src/index.server.ts index 9bbe5f03641e..b28ce95caa61 100644 --- a/packages/remix/src/index.server.ts +++ b/packages/remix/src/index.server.ts @@ -31,6 +31,7 @@ export { Hub, makeMain, Scope, + // eslint-disable-next-line deprecation/deprecation startTransaction, SDK_VERSION, setContext, diff --git a/packages/remix/src/utils/instrumentServer.ts b/packages/remix/src/utils/instrumentServer.ts index f557542e64ce..e07710dc340a 100644 --- a/packages/remix/src/utils/instrumentServer.ts +++ b/packages/remix/src/utils/instrumentServer.ts @@ -394,6 +394,8 @@ export function startRequestHandlerTransaction( ); hub.getScope().setPropagationContext(propagationContext); + // TODO: Refactor this to `startSpan()` + // eslint-disable-next-line deprecation/deprecation const transaction = hub.startTransaction({ name, op: 'http.server', diff --git a/packages/serverless/src/index.ts b/packages/serverless/src/index.ts index 488ffec7a1ec..ce076283b635 100644 --- a/packages/serverless/src/index.ts +++ b/packages/serverless/src/index.ts @@ -41,6 +41,7 @@ export { setTag, setTags, setUser, + // eslint-disable-next-line deprecation/deprecation startTransaction, withScope, NodeClient, diff --git a/packages/sveltekit/src/server/index.ts b/packages/sveltekit/src/server/index.ts index 2086b3515551..61419c196736 100644 --- a/packages/sveltekit/src/server/index.ts +++ b/packages/sveltekit/src/server/index.ts @@ -29,6 +29,7 @@ export { Hub, makeMain, Scope, + // eslint-disable-next-line deprecation/deprecation startTransaction, SDK_VERSION, setContext, diff --git a/packages/sveltekit/test/server/handle.test.ts b/packages/sveltekit/test/server/handle.test.ts index cca809006d27..9f974a6bbdd1 100644 --- a/packages/sveltekit/test/server/handle.test.ts +++ b/packages/sveltekit/test/server/handle.test.ts @@ -358,34 +358,34 @@ describe('addSentryCodeToPage', () => { it('adds meta tags and the fetch proxy script if there is an active transaction', () => { const transformPageChunk = addSentryCodeToPage({}); - const transaction = hub.startTransaction({ name: 'test' }); - hub.getScope().setSpan(transaction); - const transformed = transformPageChunk({ html, done: true }) as string; + SentryNode.startSpan({ name: 'test' }, () => { + const transformed = transformPageChunk({ html, done: true }) as string; - expect(transformed).toContain('${FETCH_PROXY_SCRIPT}`); + expect(transformed).toContain('${FETCH_PROXY_SCRIPT}`); + }); }); it('adds a nonce attribute to the script if the `fetchProxyScriptNonce` option is specified', () => { const transformPageChunk = addSentryCodeToPage({ fetchProxyScriptNonce: '123abc' }); - const transaction = hub.startTransaction({ name: 'test' }); - hub.getScope().setSpan(transaction); - const transformed = transformPageChunk({ html, done: true }) as string; + SentryNode.startSpan({ name: 'test' }, () => { + const transformed = transformPageChunk({ html, done: true }) as string; - expect(transformed).toContain('${FETCH_PROXY_SCRIPT}`); + expect(transformed).toContain('${FETCH_PROXY_SCRIPT}`); + }); }); it('does not add the fetch proxy script if the `injectFetchProxyScript` option is false', () => { const transformPageChunk = addSentryCodeToPage({ injectFetchProxyScript: false }); - const transaction = hub.startTransaction({ name: 'test' }); - hub.getScope().setSpan(transaction); - const transformed = transformPageChunk({ html, done: true }) as string; + SentryNode.startSpan({ name: 'test' }, () => { + const transformed = transformPageChunk({ html, done: true }) as string; - expect(transformed).toContain('${FETCH_PROXY_SCRIPT}`); + expect(transformed).toContain('${FETCH_PROXY_SCRIPT}`); + }); }); }); diff --git a/packages/tracing-internal/test/browser/backgroundtab.test.ts b/packages/tracing-internal/test/browser/backgroundtab.test.ts index 2687d59069c5..704f3ba89b1c 100644 --- a/packages/tracing-internal/test/browser/backgroundtab.test.ts +++ b/packages/tracing-internal/test/browser/backgroundtab.test.ts @@ -1,4 +1,4 @@ -import { Hub, makeMain } from '@sentry/core'; +import { Hub, makeMain, startSpan } from '@sentry/core'; import { JSDOM } from 'jsdom'; import { addExtensionMethods } from '../../../tracing/src'; @@ -47,16 +47,15 @@ conditionalTest({ min: 10 })('registerBackgroundTabDetection', () => { it('finishes a transaction on visibility change', () => { registerBackgroundTabDetection(); - const transaction = hub.startTransaction({ name: 'test' }); - hub.getScope().setSpan(transaction); - - // Simulate document visibility hidden event - // @ts-expect-error need to override global document - global.document.hidden = true; - events.visibilitychange(); - - expect(transaction.status).toBe('cancelled'); - expect(transaction.tags.visibilitychange).toBe('document.hidden'); - expect(transaction.endTimestamp).toBeDefined(); + startSpan({ name: 'test' }, span => { + // Simulate document visibility hidden event + // @ts-expect-error need to override global document + global.document.hidden = true; + events.visibilitychange(); + + expect(span?.status).toBe('cancelled'); + expect(span?.tags.visibilitychange).toBe('document.hidden'); + expect(span?.endTimestamp).toBeDefined(); + }); }); }); diff --git a/packages/tracing/test/index.test.ts b/packages/tracing/test/index.test.ts index 79a61cd66dd6..e30bb92c0e5d 100644 --- a/packages/tracing/test/index.test.ts +++ b/packages/tracing/test/index.test.ts @@ -5,6 +5,7 @@ import { BrowserTracing, Integrations } from '../src'; describe('index', () => { it('patches the global hub to add an implementation for `Hub.startTransaction` as a side effect', () => { const hub = getCurrentHub(); + // eslint-disable-next-line deprecation/deprecation const transaction = hub.startTransaction({ name: 'test', endTimestamp: 123 }); expect(transaction).toBeDefined(); }); diff --git a/packages/types/src/hub.ts b/packages/types/src/hub.ts index f5ec2ea4fbb2..f15d17e3013d 100644 --- a/packages/types/src/hub.ts +++ b/packages/types/src/hub.ts @@ -214,6 +214,8 @@ export interface Hub { * default values). See {@link Options.tracesSampler}. * * @returns The transaction which was just started + * + * @deprecated Use `startSpan()`, `startSpanManual()` or `startInactiveSpan()` instead. */ startTransaction(context: TransactionContext, customSamplingContext?: CustomSamplingContext): Transaction; diff --git a/packages/vercel-edge/src/index.ts b/packages/vercel-edge/src/index.ts index 0dd3e722c6bf..ff8d97fbf398 100644 --- a/packages/vercel-edge/src/index.ts +++ b/packages/vercel-edge/src/index.ts @@ -51,6 +51,7 @@ export { makeMain, runWithAsyncContext, Scope, + // eslint-disable-next-line deprecation/deprecation startTransaction, SDK_VERSION, setContext, From 644d697c85d25b8c849125be2d0ed21785ff81ac Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Fri, 5 Jan 2024 13:03:39 +0100 Subject: [PATCH 05/43] feat(core): Add `span.isRecording()` instead of `span.sampled` (#10034) To align this with OTEL API. --- MIGRATION.md | 1 + packages/astro/test/server/meta.test.ts | 6 +++--- packages/browser/src/profiling/utils.ts | 2 +- .../test/unit/profiling/hubextensions.test.ts | 6 +++--- .../bun/test/integrations/bunserver.test.ts | 2 +- packages/core/src/tracing/hubextensions.ts | 5 +++-- packages/core/src/tracing/sampling.ts | 7 +++++++ packages/core/src/tracing/span.ts | 8 ++++++++ packages/core/src/utils/spanUtils.ts | 2 +- packages/core/test/lib/tracing/span.test.ts | 17 +++++++++++++++++ .../src/browser/browsertracing.ts | 1 + packages/types/src/span.ts | 8 ++++++++ 12 files changed, 54 insertions(+), 11 deletions(-) diff --git a/MIGRATION.md b/MIGRATION.md index d55a287fac99..b12399fe655c 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -57,6 +57,7 @@ In v8, the Span class is heavily reworked. The following properties & methods ar * `span.setName(newName)`: Use `span.updateName(newName)` instead. * `span.toTraceparent()`: use `spanToTraceHeader(span)` util instead. * `span.getTraceContext()`: Use `spanToTraceContext(span)` utility function instead. +* `span.sampled`: Use `span.isRecording()` instead. ## Deprecate `pushScope` & `popScope` in favor of `withScope` diff --git a/packages/astro/test/server/meta.test.ts b/packages/astro/test/server/meta.test.ts index 69caef326936..0586507b570c 100644 --- a/packages/astro/test/server/meta.test.ts +++ b/packages/astro/test/server/meta.test.ts @@ -4,7 +4,7 @@ import { vi } from 'vitest'; import { getTracingMetaTags, isValidBaggageString } from '../../src/server/meta'; const mockedSpan = { - sampled: true, + isRecording: () => true, traceId: '12345678901234567890123456789012', spanId: '1234567890123456', transaction: { @@ -70,7 +70,7 @@ describe('getTracingMetaTags', () => { const tags = getTracingMetaTags( // @ts-expect-error - only passing a partial span object { - sampled: true, + isRecording: () => true, traceId: '12345678901234567890123456789012', spanId: '1234567890123456', transaction: undefined, @@ -93,7 +93,7 @@ describe('getTracingMetaTags', () => { const tags = getTracingMetaTags( // @ts-expect-error - only passing a partial span object { - sampled: true, + isRecording: () => true, traceId: '12345678901234567890123456789012', spanId: '1234567890123456', transaction: undefined, diff --git a/packages/browser/src/profiling/utils.ts b/packages/browser/src/profiling/utils.ts index f2fdc5e4c10d..9114884384b7 100644 --- a/packages/browser/src/profiling/utils.ts +++ b/packages/browser/src/profiling/utils.ts @@ -515,7 +515,7 @@ export function shouldProfileTransaction(transaction: Transaction): boolean { return false; } - if (!transaction.sampled) { + if (!transaction.isRecording()) { if (DEBUG_BUILD) { logger.log('[Profiling] Discarding profile because transaction was not sampled.'); } diff --git a/packages/browser/test/unit/profiling/hubextensions.test.ts b/packages/browser/test/unit/profiling/hubextensions.test.ts index 26d836b12b02..d0f9d488ec91 100644 --- a/packages/browser/test/unit/profiling/hubextensions.test.ts +++ b/packages/browser/test/unit/profiling/hubextensions.test.ts @@ -67,7 +67,7 @@ describe('BrowserProfilingIntegration', () => { // @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; + const mockTransaction = { isRecording: () => true } as Transaction; expect(() => onProfilingStartRouteTransaction(mockTransaction)).not.toThrow(); }); it('does not throw if constructor throws', () => { @@ -80,8 +80,8 @@ describe('BrowserProfilingIntegration', () => { } } - // set sampled to true so that profiling does not early return - const mockTransaction = { sampled: true } as Transaction; + // set isRecording to true so that profiling does not early return + const mockTransaction = { isRecording: () => true } as Transaction; // @ts-expect-error override with our own constructor global.window.Profiler = Profiler; diff --git a/packages/bun/test/integrations/bunserver.test.ts b/packages/bun/test/integrations/bunserver.test.ts index 1113612ace81..4fe00180377c 100644 --- a/packages/bun/test/integrations/bunserver.test.ts +++ b/packages/bun/test/integrations/bunserver.test.ts @@ -80,7 +80,7 @@ describe('Bun Serve Integration', () => { client.on('finishTransaction', transaction => { expect(transaction.traceId).toBe(TRACE_ID); expect(transaction.parentSpanId).toBe(PARENT_SPAN_ID); - expect(transaction.sampled).toBe(true); + expect(transaction.isRecording()).toBe(true); expect(transaction.metadata?.dynamicSamplingContext).toStrictEqual({ version: '1.0', environment: 'production' }); }); diff --git a/packages/core/src/tracing/hubextensions.ts b/packages/core/src/tracing/hubextensions.ts index 7047a54f1d72..fa4fd078b9ed 100644 --- a/packages/core/src/tracing/hubextensions.ts +++ b/packages/core/src/tracing/hubextensions.ts @@ -55,6 +55,7 @@ function _startTransaction( The transaction will not be sampled. Please use the ${configInstrumenter} instrumentation to start transactions.`, ); + // eslint-disable-next-line deprecation/deprecation transactionContext.sampled = false; } @@ -64,7 +65,7 @@ The transaction will not be sampled. Please use the ${configInstrumenter} instru transactionContext, ...customSamplingContext, }); - if (transaction.sampled) { + if (transaction.isRecording()) { transaction.initSpanRecorder(options._experiments && (options._experiments.maxSpans as number)); } if (client && client.emit) { @@ -94,7 +95,7 @@ export function startIdleTransaction( transactionContext, ...customSamplingContext, }); - if (transaction.sampled) { + if (transaction.isRecording()) { transaction.initSpanRecorder(options._experiments && (options._experiments.maxSpans as number)); } if (client && client.emit) { diff --git a/packages/core/src/tracing/sampling.ts b/packages/core/src/tracing/sampling.ts index 6c6ab19bf7c9..739303e1fe8c 100644 --- a/packages/core/src/tracing/sampling.ts +++ b/packages/core/src/tracing/sampling.ts @@ -21,13 +21,16 @@ export function sampleTransaction( ): T { // nothing to do if tracing is not enabled if (!hasTracingEnabled(options)) { + // eslint-disable-next-line deprecation/deprecation transaction.sampled = false; return transaction; } // if the user has forced a sampling decision by passing a `sampled` value in their transaction context, go with that + // eslint-disable-next-line deprecation/deprecation if (transaction.sampled !== undefined) { transaction.setMetadata({ + // eslint-disable-next-line deprecation/deprecation sampleRate: Number(transaction.sampled), }); return transaction; @@ -60,6 +63,7 @@ export function sampleTransaction( // only valid values are booleans or numbers between 0 and 1.) if (!isValidSampleRate(sampleRate)) { DEBUG_BUILD && logger.warn('[Tracing] Discarding transaction because of invalid sample rate.'); + // eslint-disable-next-line deprecation/deprecation transaction.sampled = false; return transaction; } @@ -74,15 +78,18 @@ export function sampleTransaction( : 'a negative sampling decision was inherited or tracesSampleRate is set to 0' }`, ); + // eslint-disable-next-line deprecation/deprecation transaction.sampled = false; return transaction; } // Now we roll the dice. Math.random is inclusive of 0, but not of 1, so strict < is safe here. In case sampleRate is // a boolean, the < comparison will cause it to be automatically cast to 1 if it's true and 0 if it's false. + // eslint-disable-next-line deprecation/deprecation transaction.sampled = Math.random() < (sampleRate as number | boolean); // if we're not going to keep it, we're done + // eslint-disable-next-line deprecation/deprecation if (!transaction.sampled) { DEBUG_BUILD && logger.log( diff --git a/packages/core/src/tracing/span.ts b/packages/core/src/tracing/span.ts index c4e91a38f9c7..a3126e5763b0 100644 --- a/packages/core/src/tracing/span.ts +++ b/packages/core/src/tracing/span.ts @@ -154,6 +154,7 @@ export class Span implements SpanInterface { } // We want to include booleans as well here if ('sampled' in spanContext) { + // eslint-disable-next-line deprecation/deprecation this.sampled = spanContext.sampled; } if (spanContext.op) { @@ -193,6 +194,7 @@ export class Span implements SpanInterface { const childSpan = new Span({ ...spanContext, parentSpanId: this.spanId, + // eslint-disable-next-line deprecation/deprecation sampled: this.sampled, traceId: this.traceId, }); @@ -351,6 +353,7 @@ export class Span implements SpanInterface { this.endTimestamp = spanContext.endTimestamp; this.op = spanContext.op; this.parentSpanId = spanContext.parentSpanId; + // eslint-disable-next-line deprecation/deprecation this.sampled = spanContext.sampled; this.spanId = spanContext.spanId || this.spanId; this.startTimestamp = spanContext.startTimestamp || this.startTimestamp; @@ -400,6 +403,11 @@ export class Span implements SpanInterface { }); } + /** @inheritdoc */ + public isRecording(): boolean { + return !this.endTimestamp && !!this.sampled; + } + /** * Get the merged data for this span. * For now, this combines `data` and `attributes` together, diff --git a/packages/core/src/utils/spanUtils.ts b/packages/core/src/utils/spanUtils.ts index 9a99dd247bb6..9c4dcff17b62 100644 --- a/packages/core/src/utils/spanUtils.ts +++ b/packages/core/src/utils/spanUtils.ts @@ -24,7 +24,7 @@ export function spanToTraceContext(span: Span): TraceContext { * Convert a Span to a Sentry trace header. */ export function spanToTraceHeader(span: Span): string { - return generateSentryTraceHeader(span.traceId, span.spanId, span.sampled); + return generateSentryTraceHeader(span.traceId, span.spanId, span.isRecording()); } /** diff --git a/packages/core/test/lib/tracing/span.test.ts b/packages/core/test/lib/tracing/span.test.ts index d439d6eb6ab1..1d36b237175a 100644 --- a/packages/core/test/lib/tracing/span.test.ts +++ b/packages/core/test/lib/tracing/span.test.ts @@ -209,6 +209,23 @@ describe('span', () => { }); }); + describe('isRecording', () => { + it('returns true for sampled span', () => { + const span = new Span({ sampled: true }); + expect(span.isRecording()).toEqual(true); + }); + + it('returns false for sampled, finished span', () => { + const span = new Span({ sampled: true, endTimestamp: Date.now() }); + expect(span.isRecording()).toEqual(false); + }); + + it('returns false for unsampled span', () => { + const span = new Span({ sampled: false }); + expect(span.isRecording()).toEqual(false); + }); + }); + // Ensure that attributes & data are merged together describe('_getData', () => { it('works without data & attributes', () => { diff --git a/packages/tracing-internal/src/browser/browsertracing.ts b/packages/tracing-internal/src/browser/browsertracing.ts index 3b99dd9603d6..d52d0e306379 100644 --- a/packages/tracing-internal/src/browser/browsertracing.ts +++ b/packages/tracing-internal/src/browser/browsertracing.ts @@ -333,6 +333,7 @@ export class BrowserTracing implements Integration { this._latestRouteName = finalContext.name; this._latestRouteSource = finalContext.metadata && finalContext.metadata.source; + // eslint-disable-next-line deprecation/deprecation if (finalContext.sampled === false) { DEBUG_BUILD && logger.log(`[Tracing] Will not send ${finalContext.op} transaction because of beforeNavigate.`); } diff --git a/packages/types/src/span.ts b/packages/types/src/span.ts index 840fcefe41ef..4ad0e5aa1110 100644 --- a/packages/types/src/span.ts +++ b/packages/types/src/span.ts @@ -58,6 +58,8 @@ export interface SpanContext { /** * Was this span chosen to be sent as part of the sample? + * + * @deprecated Use `isRecording()` instead. */ sampled?: boolean; @@ -268,4 +270,10 @@ export interface Span extends SpanContext { trace_id: string; origin?: SpanOrigin; }; + + /** + * If this is span is actually recording data. + * This will return false if tracing is disabled, this span was not sampled or if the span is already finished. + */ + isRecording(): boolean; } From 118b5c70e3392e86473052151a2932d4394600e7 Mon Sep 17 00:00:00 2001 From: Luca Forstner Date: Fri, 5 Jan 2024 13:29:16 +0100 Subject: [PATCH 06/43] test(browser): Replace flakey integration test (#10075) --- .../eventBreadcrumbs/subject.js | 3 ++ .../captureException/eventBreadcrumbs/test.ts | 23 +++++++++++ .../transactionBreadcrumbs/subject.js | 8 ++++ .../transactionBreadcrumbs/test.ts | 22 ++++++++++ .../browser/test/integration/suites/api.js | 41 ------------------- 5 files changed, 56 insertions(+), 41 deletions(-) create mode 100644 dev-packages/browser-integration-tests/suites/public-api/captureException/eventBreadcrumbs/subject.js create mode 100644 dev-packages/browser-integration-tests/suites/public-api/captureException/eventBreadcrumbs/test.ts create mode 100644 dev-packages/browser-integration-tests/suites/public-api/captureException/transactionBreadcrumbs/subject.js create mode 100644 dev-packages/browser-integration-tests/suites/public-api/captureException/transactionBreadcrumbs/test.ts diff --git a/dev-packages/browser-integration-tests/suites/public-api/captureException/eventBreadcrumbs/subject.js b/dev-packages/browser-integration-tests/suites/public-api/captureException/eventBreadcrumbs/subject.js new file mode 100644 index 000000000000..2926745ae8d5 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/public-api/captureException/eventBreadcrumbs/subject.js @@ -0,0 +1,3 @@ +Sentry.captureMessage('a'); + +Sentry.captureException(new Error('test_simple_breadcrumb_error')); diff --git a/dev-packages/browser-integration-tests/suites/public-api/captureException/eventBreadcrumbs/test.ts b/dev-packages/browser-integration-tests/suites/public-api/captureException/eventBreadcrumbs/test.ts new file mode 100644 index 000000000000..831c255cb8f8 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/public-api/captureException/eventBreadcrumbs/test.ts @@ -0,0 +1,23 @@ +import { expect } from '@playwright/test'; +import type { Event } from '@sentry/types'; + +import { sentryTest } from '../../../../utils/fixtures'; +import { getMultipleSentryEnvelopeRequests } from '../../../../utils/helpers'; + +sentryTest( + 'should capture recorded transactions as breadcrumbs for the following event sent', + async ({ getLocalTestPath, page }) => { + const url = await getLocalTestPath({ testDir: __dirname }); + + const events = await getMultipleSentryEnvelopeRequests(page, 2, { url }); + + const errorEvent = events.find(event => event.exception?.values?.[0].value === 'test_simple_breadcrumb_error')!; + + expect(errorEvent.breadcrumbs).toHaveLength(1); + expect(errorEvent.breadcrumbs?.[0]).toMatchObject({ + category: 'sentry.event', + event_id: expect.any(String), + level: expect.any(String), + }); + }, +); diff --git a/dev-packages/browser-integration-tests/suites/public-api/captureException/transactionBreadcrumbs/subject.js b/dev-packages/browser-integration-tests/suites/public-api/captureException/transactionBreadcrumbs/subject.js new file mode 100644 index 000000000000..86bf5ab29abd --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/public-api/captureException/transactionBreadcrumbs/subject.js @@ -0,0 +1,8 @@ +Sentry.captureEvent({ + event_id: 'aa3ff046696b4bc6b609ce6d28fde9e2', + message: 'someMessage', + transaction: 'wat', + type: 'transaction', +}); + +Sentry.captureException(new Error('test_simple_breadcrumb_error')); diff --git a/dev-packages/browser-integration-tests/suites/public-api/captureException/transactionBreadcrumbs/test.ts b/dev-packages/browser-integration-tests/suites/public-api/captureException/transactionBreadcrumbs/test.ts new file mode 100644 index 000000000000..c69437183591 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/public-api/captureException/transactionBreadcrumbs/test.ts @@ -0,0 +1,22 @@ +import { expect } from '@playwright/test'; +import type { Event } from '@sentry/types'; + +import { sentryTest } from '../../../../utils/fixtures'; +import { getMultipleSentryEnvelopeRequests } from '../../../../utils/helpers'; + +sentryTest( + 'should capture recorded transactions as breadcrumbs for the following event sent', + async ({ getLocalTestPath, page }) => { + const url = await getLocalTestPath({ testDir: __dirname }); + + const events = await getMultipleSentryEnvelopeRequests(page, 2, { url }); + + const errorEvent = events.find(event => event.exception?.values?.[0].value === 'test_simple_breadcrumb_error')!; + + expect(errorEvent.breadcrumbs).toHaveLength(1); + expect(errorEvent.breadcrumbs?.[0]).toMatchObject({ + category: 'sentry.transaction', + message: expect.any(String), + }); + }, +); diff --git a/packages/browser/test/integration/suites/api.js b/packages/browser/test/integration/suites/api.js index 9cb59277c6e0..462659e75ac7 100644 --- a/packages/browser/test/integration/suites/api.js +++ b/packages/browser/test/integration/suites/api.js @@ -20,47 +20,6 @@ describe('API', function () { }); }); - it('should capture Sentry internal event as breadcrumbs for the following event sent', function () { - return runInSandbox(sandbox, { manual: true }, function () { - window.allowSentryBreadcrumbs = true; - Sentry.captureMessage('a'); - Sentry.captureMessage('b'); - // For the loader - Sentry.flush && Sentry.flush(2000); - window.finalizeManualTest(); - }).then(function (summary) { - assert.equal(summary.events.length, 2); - assert.equal(summary.breadcrumbs.length, 2); - assert.equal(summary.events[1].breadcrumbs[0].category, 'sentry.event'); - assert.equal(summary.events[1].breadcrumbs[0].event_id, summary.events[0].event_id); - assert.equal(summary.events[1].breadcrumbs[0].level, summary.events[0].level); - }); - }); - - it('should capture Sentry internal transaction as breadcrumbs for the following event sent', function () { - return runInSandbox(sandbox, { manual: true }, function () { - window.allowSentryBreadcrumbs = true; - Sentry.captureEvent({ - event_id: 'aa3ff046696b4bc6b609ce6d28fde9e2', - message: 'someMessage', - transaction: 'wat', - type: 'transaction', - }); - Sentry.captureMessage('c'); - // For the loader - Sentry.flush && Sentry.flush(2000); - window.finalizeManualTest(); - }).then(function (summary) { - // We have a length of one here since transactions don't go through beforeSend - // and we add events to summary in beforeSend - assert.equal(summary.events.length, 1); - assert.equal(summary.breadcrumbs.length, 2); - assert.equal(summary.events[0].breadcrumbs[0].category, 'sentry.transaction'); - assert.isNotEmpty(summary.events[0].breadcrumbs[0].event_id); - assert.isUndefined(summary.events[0].breadcrumbs[0].level); - }); - }); - it('should generate a synthetic trace for captureException w/ non-errors', function () { return runInSandbox(sandbox, function () { throwNonError(); From fffa9322caada6d730b54022b831bd3071d71a61 Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Fri, 5 Jan 2024 13:39:07 +0100 Subject: [PATCH 07/43] feat(core): Deprecate `Hub.shouldSendDefaultPii` (#10062) Deprecates the unnecessary `Hub. shouldSendDefaultPii` method which can be easily replaced by accessing client options directly. --- MIGRATION.md | 8 +++++++- packages/core/src/hub.ts | 3 +++ packages/types/src/hub.ts | 3 +++ 3 files changed, 13 insertions(+), 1 deletion(-) diff --git a/MIGRATION.md b/MIGRATION.md index b12399fe655c..a5c4677541fc 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -21,7 +21,7 @@ You can [read more about the new performance APIs here](./docs/v8-new-performanc ## Deprecate `Sentry.lastEventId()` and `hub.lastEventId()` -`Sentry.lastEventId()` sometimes causes race conditons, so we are deprecating it in favour of the `beforeSend` callback. +`Sentry.lastEventId()` sometimes causes race conditions, so we are deprecating it in favour of the `beforeSend` callback. ```js // Before @@ -48,6 +48,12 @@ Sentry.init({ }); ``` +## Deprecated fields on `Hub` + +In v8, the Hub class will be removed. The following methods are therefore deprecated: + +- `hub.shouldSendDefaultPii()`: Access Sentry client option via `Sentry.getClient().getOptions().sendDefaultPii` instead + ## Deprecated fields on `Span` and `Transaction` In v8, the Span class is heavily reworked. The following properties & methods are thus deprecated: diff --git a/packages/core/src/hub.ts b/packages/core/src/hub.ts index d05b80410d06..1db3b01e7b0e 100644 --- a/packages/core/src/hub.ts +++ b/packages/core/src/hub.ts @@ -548,6 +548,9 @@ Sentry.init({...}); /** * Returns if default PII should be sent to Sentry and propagated in ourgoing requests * when Tracing is used. + * + * @deprecated Use top-level `getClient().getOptions().sendDefaultPii` instead. This function + * only unnecessarily increased API surface but only wrapped accessing the option. */ public shouldSendDefaultPii(): boolean { const client = this.getClient(); diff --git a/packages/types/src/hub.ts b/packages/types/src/hub.ts index f15d17e3013d..17e839f3f46e 100644 --- a/packages/types/src/hub.ts +++ b/packages/types/src/hub.ts @@ -247,6 +247,9 @@ export interface Hub { /** * Returns if default PII should be sent to Sentry and propagated in ourgoing requests * when Tracing is used. + * + * @deprecated Use top-level `getClient().getOptions().sendDefaultPii` instead. This function + * only unnecessarily increased API surface but only wrapped accessing the option. */ shouldSendDefaultPii(): boolean; } From 3545c686bf94b89ece6a7dc8f4403bfe039b9bf0 Mon Sep 17 00:00:00 2001 From: Oleh Aloshkin Date: Fri, 5 Jan 2024 14:23:27 +0100 Subject: [PATCH 08/43] feat(utils): Add `parameterize` function (#9145) Add `parameterize` fuction to utils package that takes params from a format string and attaches them as extra properties to a string. These are picked up during event processing and a `LogEntry` item is added to the event. --- .../parameterized_message/subject.js | 6 ++++ .../parameterized_message/test.ts | 22 +++++++++++++ .../parameterized_message/scenario.ts | 12 +++++++ .../parameterized_message/test.ts | 13 ++++++++ packages/browser/src/client.ts | 3 +- packages/browser/src/eventbuilder.ts | 33 +++++++++++++++---- packages/core/src/baseclient.ts | 10 ++++-- packages/core/src/server-runtime-client.ts | 3 +- packages/core/test/mocks/client.ts | 3 +- packages/replay/test/utils/TestClient.ts | 10 ++++-- .../tracing-internal/test/utils/TestClient.ts | 10 ++++-- packages/types/src/client.ts | 3 +- packages/types/src/event.ts | 4 +++ packages/types/src/index.ts | 1 + packages/types/src/parameterize.ts | 4 +++ packages/utils/src/eventbuilder.ts | 17 ++++++++-- packages/utils/src/index.ts | 1 + packages/utils/src/is.ts | 20 +++++++++-- packages/utils/src/parameterize.ts | 17 ++++++++++ packages/utils/test/parameterize.test.ts | 27 +++++++++++++++ 20 files changed, 196 insertions(+), 23 deletions(-) create mode 100644 dev-packages/browser-integration-tests/suites/public-api/captureMessage/parameterized_message/subject.js create mode 100644 dev-packages/browser-integration-tests/suites/public-api/captureMessage/parameterized_message/test.ts create mode 100644 dev-packages/node-integration-tests/suites/public-api/captureMessage/parameterized_message/scenario.ts create mode 100644 dev-packages/node-integration-tests/suites/public-api/captureMessage/parameterized_message/test.ts create mode 100644 packages/types/src/parameterize.ts create mode 100644 packages/utils/src/parameterize.ts create mode 100644 packages/utils/test/parameterize.test.ts diff --git a/dev-packages/browser-integration-tests/suites/public-api/captureMessage/parameterized_message/subject.js b/dev-packages/browser-integration-tests/suites/public-api/captureMessage/parameterized_message/subject.js new file mode 100644 index 000000000000..a86616cd52dc --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/public-api/captureMessage/parameterized_message/subject.js @@ -0,0 +1,6 @@ +import { parameterize } from '@sentry/utils'; + +const x = 'first'; +const y = 'second'; + +Sentry.captureMessage(parameterize`This is a log statement with ${x} and ${y} params`); diff --git a/dev-packages/browser-integration-tests/suites/public-api/captureMessage/parameterized_message/test.ts b/dev-packages/browser-integration-tests/suites/public-api/captureMessage/parameterized_message/test.ts new file mode 100644 index 000000000000..4c948d439bff --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/public-api/captureMessage/parameterized_message/test.ts @@ -0,0 +1,22 @@ +import { expect } from '@playwright/test'; +import type { Event } from '@sentry/types'; + +import { sentryTest } from '../../../../utils/fixtures'; +import { getFirstSentryEnvelopeRequest } from '../../../../utils/helpers'; + +sentryTest('should capture a parameterized representation of the message', async ({ getLocalTestPath, page }) => { + const bundle = process.env.PW_BUNDLE; + + if (bundle && bundle.startsWith('bundle_')) { + sentryTest.skip(); + } + + const url = await getLocalTestPath({ testDir: __dirname }); + + const eventData = await getFirstSentryEnvelopeRequest(page, url); + + expect(eventData.logentry).toStrictEqual({ + message: 'This is a log statement with %s and %s params', + params: ['first', 'second'], + }); +}); diff --git a/dev-packages/node-integration-tests/suites/public-api/captureMessage/parameterized_message/scenario.ts b/dev-packages/node-integration-tests/suites/public-api/captureMessage/parameterized_message/scenario.ts new file mode 100644 index 000000000000..db81bb18d331 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/public-api/captureMessage/parameterized_message/scenario.ts @@ -0,0 +1,12 @@ +import * as Sentry from '@sentry/node'; +import { parameterize } from '@sentry/utils'; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', +}); + +const x = 'first'; +const y = 'second'; + +Sentry.captureMessage(parameterize`This is a log statement with ${x} and ${y} params`); diff --git a/dev-packages/node-integration-tests/suites/public-api/captureMessage/parameterized_message/test.ts b/dev-packages/node-integration-tests/suites/public-api/captureMessage/parameterized_message/test.ts new file mode 100644 index 000000000000..d9015987187f --- /dev/null +++ b/dev-packages/node-integration-tests/suites/public-api/captureMessage/parameterized_message/test.ts @@ -0,0 +1,13 @@ +import { TestEnv, assertSentryEvent } from '../../../../utils'; + +test('should capture a parameterized representation of the message', async () => { + const env = await TestEnv.init(__dirname); + const event = await env.getEnvelopeRequest(); + + assertSentryEvent(event[2], { + logentry: { + message: 'This is a log statement with %s and %s params', + params: ['first', 'second'], + }, + }); +}); diff --git a/packages/browser/src/client.ts b/packages/browser/src/client.ts index c8d4e4ce2f08..14d4660b8482 100644 --- a/packages/browser/src/client.ts +++ b/packages/browser/src/client.ts @@ -7,6 +7,7 @@ import type { Event, EventHint, Options, + ParameterizedString, Severity, SeverityLevel, UserFeedback, @@ -84,7 +85,7 @@ export class BrowserClient extends BaseClient { * @inheritDoc */ public eventFromMessage( - message: string, + message: ParameterizedString, // eslint-disable-next-line deprecation/deprecation level: Severity | SeverityLevel = 'info', hint?: EventHint, diff --git a/packages/browser/src/eventbuilder.ts b/packages/browser/src/eventbuilder.ts index 6955fbfa26fe..9e72fb288cb4 100644 --- a/packages/browser/src/eventbuilder.ts +++ b/packages/browser/src/eventbuilder.ts @@ -1,5 +1,14 @@ import { getClient } from '@sentry/core'; -import type { Event, EventHint, Exception, Severity, SeverityLevel, StackFrame, StackParser } from '@sentry/types'; +import type { + Event, + EventHint, + Exception, + ParameterizedString, + Severity, + SeverityLevel, + StackFrame, + StackParser, +} from '@sentry/types'; import { addExceptionMechanism, addExceptionTypeValue, @@ -9,6 +18,7 @@ import { isError, isErrorEvent, isEvent, + isParameterizedString, isPlainObject, normalizeToSize, resolvedSyncPromise, @@ -167,7 +177,7 @@ export function eventFromException( */ export function eventFromMessage( stackParser: StackParser, - message: string, + message: ParameterizedString, // eslint-disable-next-line deprecation/deprecation level: Severity | SeverityLevel = 'info', hint?: EventHint, @@ -264,23 +274,32 @@ export function eventFromUnknownInput( */ export function eventFromString( stackParser: StackParser, - input: string, + message: ParameterizedString, syntheticException?: Error, attachStacktrace?: boolean, ): Event { - const event: Event = { - message: input, - }; + const event: Event = {}; if (attachStacktrace && syntheticException) { const frames = parseStackFrames(stackParser, syntheticException); if (frames.length) { event.exception = { - values: [{ value: input, stacktrace: { frames } }], + values: [{ value: message, stacktrace: { frames } }], }; } } + if (isParameterizedString(message)) { + const { __sentry_template_string__, __sentry_template_values__ } = message; + + event.logentry = { + message: __sentry_template_string__, + params: __sentry_template_values__, + }; + return event; + } + + event.message = message; return event; } diff --git a/packages/core/src/baseclient.ts b/packages/core/src/baseclient.ts index 75b736bbf803..628df591248d 100644 --- a/packages/core/src/baseclient.ts +++ b/packages/core/src/baseclient.ts @@ -19,6 +19,7 @@ import type { MetricBucketItem, MetricsAggregator, Outcome, + ParameterizedString, PropagationContext, SdkMetadata, Session, @@ -36,6 +37,7 @@ import { addItemToEnvelope, checkOrSetAlreadyCaught, createAttachmentEnvelopeItem, + isParameterizedString, isPlainObject, isPrimitive, isThenable, @@ -182,7 +184,7 @@ export abstract class BaseClient implements Client { * @inheritDoc */ public captureMessage( - message: string, + message: ParameterizedString, // eslint-disable-next-line deprecation/deprecation level?: Severity | SeverityLevel, hint?: EventHint, @@ -190,8 +192,10 @@ export abstract class BaseClient implements Client { ): string | undefined { let eventId: string | undefined = hint && hint.event_id; + const eventMessage = isParameterizedString(message) ? message : String(message); + const promisedEvent = isPrimitive(message) - ? this.eventFromMessage(String(message), level, hint) + ? this.eventFromMessage(eventMessage, level, hint) : this.eventFromException(message, hint); this._process( @@ -816,7 +820,7 @@ export abstract class BaseClient implements Client { * @inheritDoc */ public abstract eventFromMessage( - _message: string, + _message: ParameterizedString, // eslint-disable-next-line deprecation/deprecation _level?: Severity | SeverityLevel, _hint?: EventHint, diff --git a/packages/core/src/server-runtime-client.ts b/packages/core/src/server-runtime-client.ts index 68e1eb065d89..44b1eea5d388 100644 --- a/packages/core/src/server-runtime-client.ts +++ b/packages/core/src/server-runtime-client.ts @@ -6,6 +6,7 @@ import type { Event, EventHint, MonitorConfig, + ParameterizedString, SerializedCheckIn, Severity, SeverityLevel, @@ -63,7 +64,7 @@ export class ServerRuntimeClient< * @inheritDoc */ public eventFromMessage( - message: string, + message: ParameterizedString, // eslint-disable-next-line deprecation/deprecation level: Severity | SeverityLevel = 'info', hint?: EventHint, diff --git a/packages/core/test/mocks/client.ts b/packages/core/test/mocks/client.ts index 3fe26f9d0bec..7cb1e08cecba 100644 --- a/packages/core/test/mocks/client.ts +++ b/packages/core/test/mocks/client.ts @@ -5,6 +5,7 @@ import type { EventHint, Integration, Outcome, + ParameterizedString, Session, Severity, SeverityLevel, @@ -76,7 +77,7 @@ export class TestClient extends BaseClient { } public eventFromMessage( - message: string, + message: ParameterizedString, // eslint-disable-next-line deprecation/deprecation level: Severity | SeverityLevel = 'info', ): PromiseLike { diff --git a/packages/replay/test/utils/TestClient.ts b/packages/replay/test/utils/TestClient.ts index ad39b82084a9..da131aec8fd2 100644 --- a/packages/replay/test/utils/TestClient.ts +++ b/packages/replay/test/utils/TestClient.ts @@ -1,5 +1,11 @@ import { BaseClient, createTransport, initAndBind } from '@sentry/core'; -import type { BrowserClientReplayOptions, ClientOptions, Event, SeverityLevel } from '@sentry/types'; +import type { + BrowserClientReplayOptions, + ClientOptions, + Event, + ParameterizedString, + SeverityLevel, +} from '@sentry/types'; import { resolvedSyncPromise } from '@sentry/utils'; export interface TestClientOptions extends ClientOptions, BrowserClientReplayOptions {} @@ -24,7 +30,7 @@ export class TestClient extends BaseClient { }); } - public eventFromMessage(message: string, level: SeverityLevel = 'info'): PromiseLike { + public eventFromMessage(message: ParameterizedString, level: SeverityLevel = 'info'): PromiseLike { return resolvedSyncPromise({ message, level }); } } diff --git a/packages/tracing-internal/test/utils/TestClient.ts b/packages/tracing-internal/test/utils/TestClient.ts index ad39b82084a9..da131aec8fd2 100644 --- a/packages/tracing-internal/test/utils/TestClient.ts +++ b/packages/tracing-internal/test/utils/TestClient.ts @@ -1,5 +1,11 @@ import { BaseClient, createTransport, initAndBind } from '@sentry/core'; -import type { BrowserClientReplayOptions, ClientOptions, Event, SeverityLevel } from '@sentry/types'; +import type { + BrowserClientReplayOptions, + ClientOptions, + Event, + ParameterizedString, + SeverityLevel, +} from '@sentry/types'; import { resolvedSyncPromise } from '@sentry/utils'; export interface TestClientOptions extends ClientOptions, BrowserClientReplayOptions {} @@ -24,7 +30,7 @@ export class TestClient extends BaseClient { }); } - public eventFromMessage(message: string, level: SeverityLevel = 'info'): PromiseLike { + public eventFromMessage(message: ParameterizedString, level: SeverityLevel = 'info'): PromiseLike { return resolvedSyncPromise({ message, level }); } } diff --git a/packages/types/src/client.ts b/packages/types/src/client.ts index bfb657d135fa..c9c37349306d 100644 --- a/packages/types/src/client.ts +++ b/packages/types/src/client.ts @@ -10,6 +10,7 @@ import type { FeedbackEvent } from './feedback'; import type { Integration, IntegrationClass } from './integration'; import type { MetricBucketItem } from './metrics'; import type { ClientOptions } from './options'; +import type { ParameterizedString } from './parameterize'; import type { Scope } from './scope'; import type { SdkMetadata } from './sdkmetadata'; import type { Session, SessionAggregates } from './session'; @@ -159,7 +160,7 @@ export interface Client { /** Creates an {@link Event} from primitive inputs to `captureMessage`. */ eventFromMessage( - message: string, + message: ParameterizedString, // eslint-disable-next-line deprecation/deprecation level?: Severity | SeverityLevel, hint?: EventHint, diff --git a/packages/types/src/event.ts b/packages/types/src/event.ts index f04386968280..b9e908371f1e 100644 --- a/packages/types/src/event.ts +++ b/packages/types/src/event.ts @@ -20,6 +20,10 @@ import type { User } from './user'; export interface Event { event_id?: string; message?: string; + logentry?: { + message?: string; + params?: string[]; + }; timestamp?: number; start_timestamp?: number; // eslint-disable-next-line deprecation/deprecation diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index 655962f592fc..a6d259870714 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -140,3 +140,4 @@ export type { export type { BrowserClientReplayOptions, BrowserClientProfilingOptions } from './browseroptions'; export type { CheckIn, MonitorConfig, FinishedCheckIn, InProgressCheckIn, SerializedCheckIn } from './checkin'; export type { MetricsAggregator, MetricBucketItem, MetricInstance } from './metrics'; +export type { ParameterizedString } from './parameterize'; diff --git a/packages/types/src/parameterize.ts b/packages/types/src/parameterize.ts new file mode 100644 index 000000000000..a94daa3684db --- /dev/null +++ b/packages/types/src/parameterize.ts @@ -0,0 +1,4 @@ +export type ParameterizedString = string & { + __sentry_template_string__?: string; + __sentry_template_values__?: string[]; +}; diff --git a/packages/utils/src/eventbuilder.ts b/packages/utils/src/eventbuilder.ts index 28b2d94b0c4f..2a65a6d014cf 100644 --- a/packages/utils/src/eventbuilder.ts +++ b/packages/utils/src/eventbuilder.ts @@ -6,13 +6,14 @@ import type { Extras, Hub, Mechanism, + ParameterizedString, Severity, SeverityLevel, StackFrame, StackParser, } from '@sentry/types'; -import { isError, isPlainObject } from './is'; +import { isError, isParameterizedString, isPlainObject } from './is'; import { addExceptionMechanism, addExceptionTypeValue } from './misc'; import { normalizeToSize } from './normalize'; import { extractExceptionKeysForMessage } from './object'; @@ -127,7 +128,7 @@ export function eventFromUnknownInput( */ export function eventFromMessage( stackParser: StackParser, - message: string, + message: ParameterizedString, // eslint-disable-next-line deprecation/deprecation level: Severity | SeverityLevel = 'info', hint?: EventHint, @@ -136,7 +137,6 @@ export function eventFromMessage( const event: Event = { event_id: hint && hint.event_id, level, - message, }; if (attachStacktrace && hint && hint.syntheticException) { @@ -153,5 +153,16 @@ export function eventFromMessage( } } + if (isParameterizedString(message)) { + const { __sentry_template_string__, __sentry_template_values__ } = message; + + event.logentry = { + message: __sentry_template_string__, + params: __sentry_template_values__, + }; + return event; + } + + event.message = message; return event; } diff --git a/packages/utils/src/index.ts b/packages/utils/src/index.ts index d19991b7d401..5483f2aa7e41 100644 --- a/packages/utils/src/index.ts +++ b/packages/utils/src/index.ts @@ -32,6 +32,7 @@ export * from './url'; export * from './userIntegrations'; export * from './cache'; export * from './eventbuilder'; +export * from './parameterize'; export * from './anr'; export * from './lru'; export * from './buildPolyfills'; diff --git a/packages/utils/src/is.ts b/packages/utils/src/is.ts index 61a94053a265..12225b9c8b60 100644 --- a/packages/utils/src/is.ts +++ b/packages/utils/src/is.ts @@ -1,7 +1,7 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/explicit-module-boundary-types */ -import type { PolymorphicEvent, Primitive } from '@sentry/types'; +import type { ParameterizedString, PolymorphicEvent, Primitive } from '@sentry/types'; // eslint-disable-next-line @typescript-eslint/unbound-method const objectToString = Object.prototype.toString; @@ -78,6 +78,22 @@ export function isString(wat: unknown): wat is string { return isBuiltin(wat, 'String'); } +/** + * Checks whether given string is parameterized + * {@link isParameterizedString}. + * + * @param wat A value to be checked. + * @returns A boolean representing the result. + */ +export function isParameterizedString(wat: unknown): wat is ParameterizedString { + return ( + typeof wat === 'object' && + wat !== null && + '__sentry_template_string__' in wat && + '__sentry_template_values__' in wat + ); +} + /** * Checks whether given value is a primitive (undefined, null, number, boolean, string, bigint, symbol) * {@link isPrimitive}. @@ -86,7 +102,7 @@ export function isString(wat: unknown): wat is string { * @returns A boolean representing the result. */ export function isPrimitive(wat: unknown): wat is Primitive { - return wat === null || (typeof wat !== 'object' && typeof wat !== 'function'); + return wat === null || isParameterizedString(wat) || (typeof wat !== 'object' && typeof wat !== 'function'); } /** diff --git a/packages/utils/src/parameterize.ts b/packages/utils/src/parameterize.ts new file mode 100644 index 000000000000..2cfa63e92677 --- /dev/null +++ b/packages/utils/src/parameterize.ts @@ -0,0 +1,17 @@ +import type { ParameterizedString } from '@sentry/types'; + +/** + * Tagged template function which returns paramaterized representation of the message + * For example: parameterize`This is a log statement with ${x} and ${y} params`, would return: + * "__sentry_template_string__": "My raw message with interpreted strings like %s", + * "__sentry_template_values__": ["this"] + * @param strings An array of string values splitted between expressions + * @param values Expressions extracted from template string + * @returns String with template information in __sentry_template_string__ and __sentry_template_values__ properties + */ +export function parameterize(strings: TemplateStringsArray, ...values: string[]): ParameterizedString { + const formatted = new String(String.raw(strings, ...values)) as ParameterizedString; + formatted.__sentry_template_string__ = strings.join('\x00').replace(/%/g, '%%').replace(/\0/g, '%s'); + formatted.__sentry_template_values__ = values; + return formatted; +} diff --git a/packages/utils/test/parameterize.test.ts b/packages/utils/test/parameterize.test.ts new file mode 100644 index 000000000000..a199e0939271 --- /dev/null +++ b/packages/utils/test/parameterize.test.ts @@ -0,0 +1,27 @@ +import type { ParameterizedString } from '@sentry/types'; + +import { parameterize } from '../src/parameterize'; + +describe('parameterize()', () => { + test('works with empty string', () => { + const string = new String() as ParameterizedString; + string.__sentry_template_string__ = ''; + string.__sentry_template_values__ = []; + + const formatted = parameterize``; + expect(formatted.__sentry_template_string__).toEqual(''); + expect(formatted.__sentry_template_values__).toEqual([]); + }); + + test('works as expected with template literals', () => { + const x = 'first'; + const y = 'second'; + const string = new String() as ParameterizedString; + string.__sentry_template_string__ = 'This is a log statement with %s and %s params'; + string.__sentry_template_values__ = ['first', 'second']; + + const formatted = parameterize`This is a log statement with ${x} and ${y} params`; + expect(formatted.__sentry_template_string__).toEqual(string.__sentry_template_string__); + expect(formatted.__sentry_template_values__).toEqual(string.__sentry_template_values__); + }); +}); From ecb4f7f8d23a6d20e902d78b39f1d09577771239 Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Fri, 5 Jan 2024 14:28:19 +0100 Subject: [PATCH 09/43] feat(core): Deprecate session APIs on hub and add global replacements (#10054) Deprecates and adds top-level replacements for * `hub.startSession` -> `Sentry.startSession` * `hub.endSession` -> `Sentry.endSession` * `hub.captureException` -> `Sentry.captureSession` These APIs were mostly used in browser and node `startSessionTracking` functions which were called on `Sentry.init`. I updated these usages with the global replacements and it seems integration tests are happy with it. For now, we not only put the session onto the isolation scope but also onto the current scope to avoid changing event processing and stuff like the ANR worker, as well as avoid "soft-breaking" users who use `hub.(get|set)Session`. --- MIGRATION.md | 7 +- .../sessions/v7-hub-start-session/init.js | 14 +++ .../v7-hub-start-session/template.html | 9 ++ .../sessions/v7-hub-start-session/test.ts | 39 +++++++ packages/browser/src/sdk.ts | 25 +---- packages/core/src/exports.ts | 104 +++++++++++++++++- packages/core/src/hub.ts | 6 + packages/core/src/index.ts | 3 + packages/core/src/session.ts | 1 - packages/core/test/lib/exports.test.ts | 103 ++++++++++++++++- packages/hub/test/hub.test.ts | 100 +++++++++++++++++ packages/node-experimental/src/sdk/api.ts | 61 +--------- packages/node-experimental/src/sdk/hub.ts | 4 +- packages/node-experimental/src/sdk/init.ts | 4 +- packages/node/src/sdk.ts | 13 ++- packages/types/src/hub.ts | 9 +- 16 files changed, 408 insertions(+), 94 deletions(-) create mode 100644 dev-packages/browser-integration-tests/suites/sessions/v7-hub-start-session/init.js create mode 100644 dev-packages/browser-integration-tests/suites/sessions/v7-hub-start-session/template.html create mode 100644 dev-packages/browser-integration-tests/suites/sessions/v7-hub-start-session/test.ts diff --git a/MIGRATION.md b/MIGRATION.md index a5c4677541fc..30eb4e4ca1f4 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -52,7 +52,12 @@ Sentry.init({ In v8, the Hub class will be removed. The following methods are therefore deprecated: -- `hub.shouldSendDefaultPii()`: Access Sentry client option via `Sentry.getClient().getOptions().sendDefaultPii` instead +* `hub.startTransaction()`: See [Deprecation of `startTransaction`](#deprecate-starttransaction) +* `hub.lastEventId()`: See [Deprecation of `lastEventId`](#deprecate-sentrylasteventid-and-hublasteventid) +* `hub.startSession()`: Use top-level `Sentry.startSession()` instead +* `hub.endSession()`: Use top-level `Sentry.endSession()` instead +* `hub.captureSession()`: Use top-level `Sentry.captureSession()` instead +* `hub.shouldSendDefaultPii()`: Access Sentry client option via `Sentry.getClient().getOptions().sendDefaultPii` instead ## Deprecated fields on `Span` and `Transaction` diff --git a/dev-packages/browser-integration-tests/suites/sessions/v7-hub-start-session/init.js b/dev-packages/browser-integration-tests/suites/sessions/v7-hub-start-session/init.js new file mode 100644 index 000000000000..4958e35f2198 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/sessions/v7-hub-start-session/init.js @@ -0,0 +1,14 @@ +import * as Sentry from '@sentry/browser'; + +window.Sentry = Sentry; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '0.1', + // intentionally disabling this, we want to leverage the deprecated hub API + autoSessionTracking: false, +}); + +// simulate old startSessionTracking behavior +Sentry.getCurrentHub().startSession({ ignoreDuration: true }); +Sentry.getCurrentHub().captureSession(); diff --git a/dev-packages/browser-integration-tests/suites/sessions/v7-hub-start-session/template.html b/dev-packages/browser-integration-tests/suites/sessions/v7-hub-start-session/template.html new file mode 100644 index 000000000000..77906444cbce --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/sessions/v7-hub-start-session/template.html @@ -0,0 +1,9 @@ + + + + + + + Navigate + + diff --git a/dev-packages/browser-integration-tests/suites/sessions/v7-hub-start-session/test.ts b/dev-packages/browser-integration-tests/suites/sessions/v7-hub-start-session/test.ts new file mode 100644 index 000000000000..8a48f161c93b --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/sessions/v7-hub-start-session/test.ts @@ -0,0 +1,39 @@ +import type { Route } from '@playwright/test'; +import { expect } from '@playwright/test'; +import type { SessionContext } from '@sentry/types'; + +import { sentryTest } from '../../../utils/fixtures'; +import { getFirstSentryEnvelopeRequest } from '../../../utils/helpers'; + +sentryTest('should start a new session on pageload.', async ({ getLocalTestPath, page }) => { + const url = await getLocalTestPath({ testDir: __dirname }); + const session = await getFirstSentryEnvelopeRequest(page, url); + + expect(session).toBeDefined(); + expect(session.init).toBe(true); + expect(session.errors).toBe(0); + expect(session.status).toBe('ok'); +}); + +sentryTest('should start a new session with navigation.', async ({ getLocalTestPath, page, browserName }) => { + // Navigations get CORS error on Firefox and WebKit as we're using `file://` protocol. + if (browserName !== 'chromium') { + sentryTest.skip(); + } + + const url = await getLocalTestPath({ testDir: __dirname }); + await page.route('**/foo', (route: Route) => route.fulfill({ path: `${__dirname}/dist/index.html` })); + + const initSession = await getFirstSentryEnvelopeRequest(page, url); + + await page.click('#navigate'); + + const newSession = await getFirstSentryEnvelopeRequest(page, url); + + expect(newSession).toBeDefined(); + expect(newSession.init).toBe(true); + expect(newSession.errors).toBe(0); + expect(newSession.status).toBe('ok'); + expect(newSession.sid).toBeDefined(); + expect(initSession.sid).not.toBe(newSession.sid); +}); diff --git a/packages/browser/src/sdk.ts b/packages/browser/src/sdk.ts index 579bb57e9698..451cce98e3b7 100644 --- a/packages/browser/src/sdk.ts +++ b/packages/browser/src/sdk.ts @@ -1,11 +1,13 @@ import type { Hub } from '@sentry/core'; import { Integrations as CoreIntegrations, + captureSession, getClient, getCurrentHub, getIntegrationsToSetup, getReportDialogEndpoint, initAndBind, + startSession, } from '@sentry/core'; import type { UserFeedback } from '@sentry/types'; import { @@ -250,11 +252,6 @@ export function wrap(fn: (...args: any) => any): any { return internalWrap(fn)(); } -function startSessionOnHub(hub: Hub): void { - hub.startSession({ ignoreDuration: true }); - hub.captureSession(); -} - /** * Enable automatic Session Tracking for the initial page load. */ @@ -264,29 +261,19 @@ function startSessionTracking(): void { return; } - const hub = getCurrentHub(); - - // The only way for this to be false is for there to be a version mismatch between @sentry/browser (>= 6.0.0) and - // @sentry/hub (< 5.27.0). In the simple case, there won't ever be such a mismatch, because the two packages are - // pinned at the same version in package.json, but there are edge cases where it's possible. See - // https://github.com/getsentry/sentry-javascript/issues/3207 and - // https://github.com/getsentry/sentry-javascript/issues/3234 and - // https://github.com/getsentry/sentry-javascript/issues/3278. - if (!hub.captureSession) { - return; - } - // The session duration for browser sessions does not track a meaningful // concept that can be used as a metric. // Automatically captured sessions are akin to page views, and thus we // discard their duration. - startSessionOnHub(hub); + startSession({ ignoreDuration: true }); + captureSession(); // We want to create a session for every navigation as well addHistoryInstrumentationHandler(({ from, to }) => { // Don't create an additional session for the initial route or if the location did not change if (from !== undefined && from !== to) { - startSessionOnHub(getCurrentHub()); + startSession({ ignoreDuration: true }); + captureSession(); } }); } diff --git a/packages/core/src/exports.ts b/packages/core/src/exports.ts index 09989215ef3e..bc37a633c16e 100644 --- a/packages/core/src/exports.ts +++ b/packages/core/src/exports.ts @@ -12,17 +12,21 @@ import type { FinishedCheckIn, MonitorConfig, Primitive, + Session, + SessionContext, Severity, SeverityLevel, TransactionContext, User, } from '@sentry/types'; -import { isThenable, logger, timestampInSeconds, uuid4 } from '@sentry/utils'; +import { GLOBAL_OBJ, isThenable, logger, timestampInSeconds, uuid4 } from '@sentry/utils'; +import { DEFAULT_ENVIRONMENT } from './constants'; import { DEBUG_BUILD } from './debug-build'; import type { Hub } from './hub'; -import { getCurrentHub } from './hub'; +import { getCurrentHub, getIsolationScope } from './hub'; import type { Scope } from './scope'; +import { closeSession, makeSession, updateSession } from './session'; import type { ExclusiveEventHintOrCaptureContext } from './utils/prepareEvent'; import { parseEventHintOrCaptureContext } from './utils/prepareEvent'; @@ -322,3 +326,99 @@ export function getClient(): C | undefined { export function getCurrentScope(): Scope { return getCurrentHub().getScope(); } + +/** + * Start a session on the current isolation scope. + * + * @param context (optional) additional properties to be applied to the returned session object + * + * @returns the new active session + */ +export function startSession(context?: SessionContext): Session { + const client = getClient(); + const isolationScope = getIsolationScope(); + const currentScope = getCurrentScope(); + + const { release, environment = DEFAULT_ENVIRONMENT } = (client && client.getOptions()) || {}; + + // Will fetch userAgent if called from browser sdk + const { userAgent } = GLOBAL_OBJ.navigator || {}; + + const session = makeSession({ + release, + environment, + user: isolationScope.getUser(), + ...(userAgent && { userAgent }), + ...context, + }); + + // End existing session if there's one + const currentSession = isolationScope.getSession(); + if (currentSession && currentSession.status === 'ok') { + updateSession(currentSession, { status: 'exited' }); + } + + endSession(); + + // Afterwards we set the new session on the scope + isolationScope.setSession(session); + + // TODO (v8): Remove this and only use the isolation scope(?). + // For v7 though, we can't "soft-break" people using getCurrentHub().getScope().setSession() + currentScope.setSession(session); + + return session; +} + +/** + * End the session on the current isolation scope. + */ +export function endSession(): void { + const isolationScope = getIsolationScope(); + const currentScope = getCurrentScope(); + + const session = isolationScope.getSession(); + if (session) { + closeSession(session); + } + _sendSessionUpdate(); + + // the session is over; take it off of the scope + isolationScope.setSession(); + + // TODO (v8): Remove this and only use the isolation scope(?). + // For v7 though, we can't "soft-break" people using getCurrentHub().getScope().setSession() + currentScope.setSession(); +} + +/** + * Sends the current Session on the scope + */ +function _sendSessionUpdate(): void { + const isolationScope = getIsolationScope(); + const currentScope = getCurrentScope(); + const client = getClient(); + // TODO (v8): Remove currentScope and only use the isolation scope(?). + // For v7 though, we can't "soft-break" people using getCurrentHub().getScope().setSession() + const session = currentScope.getSession() || isolationScope.getSession(); + if (session && client && client.captureSession) { + client.captureSession(session); + } +} + +/** + * Sends the current session on the scope to Sentry + * + * @param end If set the session will be marked as exited and removed from the scope. + * Defaults to `false`. + */ +export function captureSession(end: boolean = false): void { + // both send the update and pull the session from the scope + if (end) { + endSession(); + return; + } + + // only send the update + _sendSessionUpdate(); +} diff --git a/packages/core/src/hub.ts b/packages/core/src/hub.ts index 1db3b01e7b0e..9012ab057e92 100644 --- a/packages/core/src/hub.ts +++ b/packages/core/src/hub.ts @@ -487,10 +487,13 @@ Sentry.init({...}); /** * @inheritDoc + * + * @deprecated Use top level `captureSession` instead. */ public captureSession(endSession: boolean = false): void { // both send the update and pull the session from the scope if (endSession) { + // eslint-disable-next-line deprecation/deprecation return this.endSession(); } @@ -500,6 +503,7 @@ Sentry.init({...}); /** * @inheritDoc + * @deprecated Use top level `endSession` instead. */ public endSession(): void { const layer = this.getStackTop(); @@ -516,6 +520,7 @@ Sentry.init({...}); /** * @inheritDoc + * @deprecated Use top level `startSession` instead. */ public startSession(context?: SessionContext): Session { const { scope, client } = this.getStackTop(); @@ -537,6 +542,7 @@ Sentry.init({...}); if (currentSession && currentSession.status === 'ok') { updateSession(currentSession, { status: 'exited' }); } + // eslint-disable-next-line deprecation/deprecation this.endSession(); // Afterwards we set the new session on the scope diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 2c423f0744c3..1585e119d249 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -30,6 +30,9 @@ export { withScope, getClient, getCurrentScope, + startSession, + endSession, + captureSession, } from './exports'; export { getCurrentHub, diff --git a/packages/core/src/session.ts b/packages/core/src/session.ts index 4c82ef60ffd4..4d3786bab9c5 100644 --- a/packages/core/src/session.ts +++ b/packages/core/src/session.ts @@ -1,6 +1,5 @@ import type { SerializedSession, Session, SessionContext, SessionStatus } from '@sentry/types'; import { dropUndefinedKeys, timestampInSeconds, uuid4 } from '@sentry/utils'; - /** * Creates a new `Session` object by setting certain default parameters. If optional @param context * is passed, the passed properties are applied to the session object. diff --git a/packages/core/test/lib/exports.test.ts b/packages/core/test/lib/exports.test.ts index 7a4ec6987dd1..c16073255030 100644 --- a/packages/core/test/lib/exports.test.ts +++ b/packages/core/test/lib/exports.test.ts @@ -1,4 +1,14 @@ -import { Hub, Scope, getCurrentScope, makeMain, withScope } from '../../src'; +import { + Hub, + Scope, + captureSession, + endSession, + getCurrentScope, + getIsolationScope, + makeMain, + startSession, + withScope, +} from '../../src'; import { TestClient, getDefaultTestClientOptions } from '../mocks/client'; function getTestClient(): TestClient { @@ -125,3 +135,94 @@ describe('withScope', () => { expect(getCurrentScope()).toBe(scope1); }); }); + +describe('session APIs', () => { + beforeEach(() => { + const client = getTestClient(); + const hub = new Hub(client); + makeMain(hub); + }); + + describe('startSession', () => { + it('starts a session', () => { + const session = startSession(); + + expect(session).toMatchObject({ + status: 'ok', + errors: 0, + init: true, + environment: 'production', + ignoreDuration: false, + sid: expect.any(String), + did: undefined, + timestamp: expect.any(Number), + started: expect.any(Number), + duration: expect.any(Number), + toJSON: expect.any(Function), + }); + }); + + it('ends a previously active session and removes it from the scope', () => { + const session1 = startSession(); + + expect(session1.status).toBe('ok'); + expect(getIsolationScope().getSession()).toBe(session1); + + const session2 = startSession(); + + expect(session2.status).toBe('ok'); + expect(session1.status).toBe('exited'); + expect(getIsolationScope().getSession()).toBe(session2); + }); + }); + + describe('endSession', () => { + it('ends a session and removes it from the scope', () => { + const session = startSession(); + + expect(session.status).toBe('ok'); + expect(getIsolationScope().getSession()).toBe(session); + + endSession(); + + expect(session.status).toBe('exited'); + expect(getIsolationScope().getSession()).toBe(undefined); + }); + }); + + describe('captureSession', () => { + it('captures a session without ending it by default', () => { + const session = startSession({ release: '1.0.0' }); + + expect(session.status).toBe('ok'); + expect(session.init).toBe(true); + expect(getIsolationScope().getSession()).toBe(session); + + captureSession(); + + // this flag indicates the session was sent via BaseClient + expect(session.init).toBe(false); + + // session is still active and on the scope + expect(session.status).toBe('ok'); + expect(getIsolationScope().getSession()).toBe(session); + }); + + it('captures a session and ends it if end is `true`', () => { + const session = startSession({ release: '1.0.0' }); + + expect(session.status).toBe('ok'); + expect(session.init).toBe(true); + expect(getIsolationScope().getSession()).toBe(session); + + captureSession(true); + + // this flag indicates the session was sent via BaseClient + expect(session.init).toBe(false); + + // session is still active and on the scope + expect(session.status).toBe('exited'); + expect(getIsolationScope().getSession()).toBe(undefined); + }); + }); +}); diff --git a/packages/hub/test/hub.test.ts b/packages/hub/test/hub.test.ts index fbdc3993b989..08ec6a22130a 100644 --- a/packages/hub/test/hub.test.ts +++ b/packages/hub/test/hub.test.ts @@ -3,6 +3,7 @@ import type { Client, Event, EventType } from '@sentry/types'; +import { getCurrentScope, makeMain } from '@sentry/core'; import { Hub, Scope, getCurrentHub } from '../src'; const clientFn: any = jest.fn(); @@ -18,6 +19,7 @@ function makeClient() { getIntegration: jest.fn(), setupIntegrations: jest.fn(), captureMessage: jest.fn(), + captureSession: jest.fn(), } as unknown as Client; } @@ -453,4 +455,102 @@ describe('Hub', () => { expect(hub.shouldSendDefaultPii()).toBe(true); }); }); + + describe('session APIs', () => { + beforeEach(() => { + const testClient = makeClient(); + const hub = new Hub(testClient); + makeMain(hub); + }); + + describe('startSession', () => { + it('starts a session', () => { + const testClient = makeClient(); + const hub = new Hub(testClient); + makeMain(hub); + const session = hub.startSession(); + + expect(session).toMatchObject({ + status: 'ok', + errors: 0, + init: true, + environment: 'production', + ignoreDuration: false, + sid: expect.any(String), + did: undefined, + timestamp: expect.any(Number), + started: expect.any(Number), + duration: expect.any(Number), + toJSON: expect.any(Function), + }); + }); + + it('ends a previously active session and removes it from the scope', () => { + const testClient = makeClient(); + const hub = new Hub(testClient); + makeMain(hub); + + const session1 = hub.startSession(); + + expect(session1.status).toBe('ok'); + expect(getCurrentScope().getSession()).toBe(session1); + + const session2 = hub.startSession(); + + expect(session2.status).toBe('ok'); + expect(session1.status).toBe('exited'); + expect(getCurrentHub().getScope().getSession()).toBe(session2); + }); + }); + + describe('endSession', () => { + it('ends a session and removes it from the scope', () => { + const testClient = makeClient(); + const hub = new Hub(testClient); + makeMain(hub); + + const session = hub.startSession(); + + expect(session.status).toBe('ok'); + expect(getCurrentScope().getSession()).toBe(session); + + hub.endSession(); + + expect(session.status).toBe('exited'); + expect(getCurrentHub().getScope().getSession()).toBe(undefined); + }); + }); + + describe('captureSession', () => { + it('captures a session without ending it by default', () => { + const testClient = makeClient(); + const hub = new Hub(testClient); + makeMain(hub); + + const session = hub.startSession(); + + expect(session.status).toBe('ok'); + expect(getCurrentScope().getSession()).toBe(session); + + hub.captureSession(); + + expect(testClient.captureSession).toHaveBeenCalledWith(expect.objectContaining({ status: 'ok' })); + }); + + it('captures a session and ends it if end is `true`', () => { + const testClient = makeClient(); + const hub = new Hub(testClient); + makeMain(hub); + + const session = hub.startSession(); + + expect(session.status).toBe('ok'); + expect(hub.getScope().getSession()).toBe(session); + + hub.captureSession(true); + + expect(testClient.captureSession).toHaveBeenCalledWith(expect.objectContaining({ status: 'exited' })); + }); + }); + }); }); diff --git a/packages/node-experimental/src/sdk/api.ts b/packages/node-experimental/src/sdk/api.ts index 71a08dd38552..15882a6f165a 100644 --- a/packages/node-experimental/src/sdk/api.ts +++ b/packages/node-experimental/src/sdk/api.ts @@ -1,7 +1,6 @@ // PUBLIC APIS import { context } from '@opentelemetry/api'; -import { DEFAULT_ENVIRONMENT, closeSession, makeSession, updateSession } from '@sentry/core'; import type { Breadcrumb, BreadcrumbHint, @@ -12,12 +11,11 @@ import type { Extra, Extras, Primitive, - Session, Severity, SeverityLevel, User, } from '@sentry/types'; -import { GLOBAL_OBJ, consoleSandbox, dateTimestampInSeconds } from '@sentry/utils'; +import { consoleSandbox, dateTimestampInSeconds } from '@sentry/utils'; import { getScopesFromContext, setScopesOnContext } from '../utils/contextData'; import type { ExclusiveEventHintOrCaptureContext } from '../utils/prepareEvent'; @@ -168,60 +166,3 @@ export function setContext( ): void { getIsolationScope().setContext(name, context); } - -/** Start a session on the current isolation scope. */ -export function startSession(context?: Session): Session { - const client = getClient(); - const isolationScope = getIsolationScope(); - - const { release, environment = DEFAULT_ENVIRONMENT } = client.getOptions(); - - // Will fetch userAgent if called from browser sdk - const { userAgent } = GLOBAL_OBJ.navigator || {}; - - const session = makeSession({ - release, - environment, - user: isolationScope.getUser(), - ...(userAgent && { userAgent }), - ...context, - }); - - // End existing session if there's one - const currentSession = isolationScope.getSession && isolationScope.getSession(); - if (currentSession && currentSession.status === 'ok') { - updateSession(currentSession, { status: 'exited' }); - } - endSession(); - - // Afterwards we set the new session on the scope - isolationScope.setSession(session); - - return session; -} - -/** End the session on the current isolation scope. */ -export function endSession(): void { - const isolationScope = getIsolationScope(); - const session = isolationScope.getSession(); - if (session) { - closeSession(session); - } - _sendSessionUpdate(); - - // the session is over; take it off of the scope - isolationScope.setSession(); -} - -/** - * Sends the current Session on the scope - */ -function _sendSessionUpdate(): void { - const scope = getCurrentScope(); - const client = getClient(); - - const session = scope.getSession(); - if (session && client.captureSession) { - client.captureSession(session); - } -} diff --git a/packages/node-experimental/src/sdk/hub.ts b/packages/node-experimental/src/sdk/hub.ts index b58548acb326..5de82086f387 100644 --- a/packages/node-experimental/src/sdk/hub.ts +++ b/packages/node-experimental/src/sdk/hub.ts @@ -10,11 +10,11 @@ import type { TransactionContext, } from '@sentry/types'; +import { endSession, startSession } from '@sentry/core'; import { addBreadcrumb, captureEvent, configureScope, - endSession, getClient, getCurrentScope, lastEventId, @@ -24,7 +24,6 @@ import { setTag, setTags, setUser, - startSession, withScope, } from './api'; import { callExtensionMethod, getGlobalCarrier } from './globals'; @@ -121,6 +120,7 @@ export function getCurrentHub(): Hub { captureSession(endSession?: boolean): void { // both send the update and pull the session from the scope if (endSession) { + // eslint-disable-next-line deprecation/deprecation return this.endSession(); } diff --git a/packages/node-experimental/src/sdk/init.ts b/packages/node-experimental/src/sdk/init.ts index 821757a9a246..0f60bccd343c 100644 --- a/packages/node-experimental/src/sdk/init.ts +++ b/packages/node-experimental/src/sdk/init.ts @@ -1,4 +1,4 @@ -import { getIntegrationsToSetup, hasTracingEnabled } from '@sentry/core'; +import { endSession, getIntegrationsToSetup, hasTracingEnabled, startSession } from '@sentry/core'; import { Integrations, defaultIntegrations as defaultNodeIntegrations, @@ -22,7 +22,7 @@ import { Http } from '../integrations/http'; import { NodeFetch } from '../integrations/node-fetch'; import { setOpenTelemetryContextAsyncContextStrategy } from '../otel/asyncContextStrategy'; import type { NodeExperimentalClientOptions, NodeExperimentalOptions } from '../types'; -import { endSession, getClient, getCurrentScope, getGlobalScope, getIsolationScope, startSession } from './api'; +import { getClient, getCurrentScope, getGlobalScope, getIsolationScope } from './api'; import { NodeExperimentalClient } from './client'; import { getGlobalCarrier } from './globals'; import { setLegacyHubOnCarrier } from './hub'; diff --git a/packages/node/src/sdk.ts b/packages/node/src/sdk.ts index c9a1c108dfdd..88a6b1a9074c 100644 --- a/packages/node/src/sdk.ts +++ b/packages/node/src/sdk.ts @@ -1,12 +1,14 @@ /* eslint-disable max-lines */ import { Integrations as CoreIntegrations, + endSession, getClient, - getCurrentHub, getCurrentScope, getIntegrationsToSetup, + getIsolationScope, getMainCarrier, initAndBind, + startSession, } from '@sentry/core'; import type { SessionStatus, StackParser } from '@sentry/types'; import { @@ -244,20 +246,21 @@ export const defaultStackParser: StackParser = createStackParser(nodeStackLinePa * Enable automatic Session Tracking for the node process. */ function startSessionTracking(): void { - const hub = getCurrentHub(); - hub.startSession(); + startSession(); // Emitted in the case of healthy sessions, error of `mechanism.handled: true` and unhandledrejections because // The 'beforeExit' event is not emitted for conditions causing explicit termination, // such as calling process.exit() or uncaught exceptions. // Ref: https://nodejs.org/api/process.html#process_event_beforeexit process.on('beforeExit', () => { - const session = hub.getScope().getSession(); + const session = getIsolationScope().getSession(); const terminalStates: SessionStatus[] = ['exited', 'crashed']; // Only call endSession, if the Session exists on Scope and SessionStatus is not a // Terminal Status i.e. Exited or Crashed because // "When a session is moved away from ok it must not be updated anymore." // Ref: https://develop.sentry.dev/sdk/sessions/ - if (session && !terminalStates.includes(session.status)) hub.endSession(); + if (session && !terminalStates.includes(session.status)) { + endSession(); + } }); } diff --git a/packages/types/src/hub.ts b/packages/types/src/hub.ts index 17e839f3f46e..53d689a826bf 100644 --- a/packages/types/src/hub.ts +++ b/packages/types/src/hub.ts @@ -230,22 +230,29 @@ export interface Hub { * @param context Optional properties of the new `Session`. * * @returns The session which was just started + * + * @deprecated Use top-level `startSession` instead. */ startSession(context?: Session): Session; /** * Ends the session that lives on the current scope and sends it to Sentry + * + * @deprecated Use top-level `endSession` instead. */ endSession(): void; /** * Sends the current session on the scope to Sentry + * * @param endSession If set the session will be marked as exited and removed from the scope + * + * @deprecated Use top-level `captureSession` instead. */ captureSession(endSession?: boolean): void; /** - * Returns if default PII should be sent to Sentry and propagated in ourgoing requests + * Returns if default PII should be sent to Sentry and propagated in outgoing requests * when Tracing is used. * * @deprecated Use top-level `getClient().getOptions().sendDefaultPii` instead. This function From a443517e34da751f9086c2484057b62350d98724 Mon Sep 17 00:00:00 2001 From: Tim Fish Date: Fri, 5 Jan 2024 16:34:12 +0100 Subject: [PATCH 10/43] fix(node): Revert to only use sync debugger for `LocalVariables` (#10077) --- .../src/integrations/local-variables/index.ts | 19 ++----------------- 1 file changed, 2 insertions(+), 17 deletions(-) diff --git a/packages/node/src/integrations/local-variables/index.ts b/packages/node/src/integrations/local-variables/index.ts index 970eaea52719..708b4b41ea24 100644 --- a/packages/node/src/integrations/local-variables/index.ts +++ b/packages/node/src/integrations/local-variables/index.ts @@ -1,21 +1,6 @@ -import { convertIntegrationFnToClass } from '@sentry/core'; -import type { IntegrationFn } from '@sentry/types'; -import { NODE_VERSION } from '../../nodeVersion'; -import type { Options } from './common'; -import { localVariablesAsync } from './local-variables-async'; -import { localVariablesSync } from './local-variables-sync'; - -const INTEGRATION_NAME = 'LocalVariables'; - -/** - * Adds local variables to exception frames - */ -const localVariables: IntegrationFn = (options: Options = {}) => { - return NODE_VERSION.major < 19 ? localVariablesSync(options) : localVariablesAsync(options); -}; +import { LocalVariablesSync } from './local-variables-sync'; /** * Adds local variables to exception frames */ -// eslint-disable-next-line deprecation/deprecation -export const LocalVariables = convertIntegrationFnToClass(INTEGRATION_NAME, localVariables); +export const LocalVariables = LocalVariablesSync; From 6ebebf40b00e6f249a705d9c9e768a8fba812b60 Mon Sep 17 00:00:00 2001 From: Luca Forstner Date: Fri, 5 Jan 2024 16:43:05 +0100 Subject: [PATCH 11/43] test(browser-integration-tests): Skip flakey test on chrome (#10078) --- .../Breadcrumbs/dom/click/test.ts | 105 +++++++++--------- 1 file changed, 55 insertions(+), 50 deletions(-) diff --git a/dev-packages/browser-integration-tests/suites/integrations/Breadcrumbs/dom/click/test.ts b/dev-packages/browser-integration-tests/suites/integrations/Breadcrumbs/dom/click/test.ts index 93ceb1e70001..37ac97c633ca 100644 --- a/dev-packages/browser-integration-tests/suites/integrations/Breadcrumbs/dom/click/test.ts +++ b/dev-packages/browser-integration-tests/suites/integrations/Breadcrumbs/dom/click/test.ts @@ -4,57 +4,62 @@ import type { Event } from '@sentry/types'; import { sentryTest } from '../../../../../utils/fixtures'; import { getFirstSentryEnvelopeRequest } from '../../../../../utils/helpers'; -sentryTest('captures Breadcrumb for clicks & debounces them for a second', async ({ getLocalTestUrl, page }) => { - const url = await getLocalTestUrl({ testDir: __dirname }); - - await page.route('**/foo', route => { - return route.fulfill({ - status: 200, - body: JSON.stringify({ - userNames: ['John', 'Jane'], - }), - headers: { - 'Content-Type': 'application/json', - }, +sentryTest( + 'captures Breadcrumb for clicks & debounces them for a second', + async ({ getLocalTestUrl, page, browserName }) => { + sentryTest.skip(browserName === 'chromium', 'This consistently flakes on chrome.'); + + const url = await getLocalTestUrl({ testDir: __dirname }); + + await page.route('**/foo', route => { + return route.fulfill({ + status: 200, + body: JSON.stringify({ + userNames: ['John', 'Jane'], + }), + headers: { + 'Content-Type': 'application/json', + }, + }); }); - }); - - const promise = getFirstSentryEnvelopeRequest(page); - - await page.goto(url); - - await page.click('#button1'); - // not debounced because other target - await page.click('#button2'); - // This should be debounced - await page.click('#button2'); - - // Wait a second for the debounce to finish - await page.waitForTimeout(1000); - await page.click('#button2'); - - const [eventData] = await Promise.all([promise, page.evaluate('Sentry.captureException("test exception")')]); - - expect(eventData.exception?.values).toHaveLength(1); - - expect(eventData.breadcrumbs).toEqual([ - { - timestamp: expect.any(Number), - category: 'ui.click', - message: 'body > button#button1[type="button"]', - }, - { - timestamp: expect.any(Number), - category: 'ui.click', - message: 'body > button#button2[type="button"]', - }, - { - timestamp: expect.any(Number), - category: 'ui.click', - message: 'body > button#button2[type="button"]', - }, - ]); -}); + + const promise = getFirstSentryEnvelopeRequest(page); + + await page.goto(url); + + await page.click('#button1'); + // not debounced because other target + await page.click('#button2'); + // This should be debounced + await page.click('#button2'); + + // Wait a second for the debounce to finish + await page.waitForTimeout(1000); + await page.click('#button2'); + + const [eventData] = await Promise.all([promise, page.evaluate('Sentry.captureException("test exception")')]); + + expect(eventData.exception?.values).toHaveLength(1); + + expect(eventData.breadcrumbs).toEqual([ + { + timestamp: expect.any(Number), + category: 'ui.click', + message: 'body > button#button1[type="button"]', + }, + { + timestamp: expect.any(Number), + category: 'ui.click', + message: 'body > button#button2[type="button"]', + }, + { + timestamp: expect.any(Number), + category: 'ui.click', + message: 'body > button#button2[type="button"]', + }, + ]); + }, +); sentryTest( 'uses the annotated component name in the breadcrumb messages and adds it to the data object', From bfb61cf6b4b637b27c7c6ba2a30ce76419fdc2a1 Mon Sep 17 00:00:00 2001 From: Billy Vong Date: Fri, 5 Jan 2024 14:38:48 -0330 Subject: [PATCH 12/43] test(browser-integration): Add test for Replay canvas recording (#10079) Adds a test to ensure that Replay canvas recording works. --- .../suites/replay/canvas/records/init.js | 24 +++++++ .../replay/canvas/records/template.html | 26 +++++++ .../suites/replay/canvas/records/test.ts | 71 +++++++++++++++++++ .../suites/replay/canvas/template.html | 9 +++ .../utils/replayHelpers.ts | 7 ++ 5 files changed, 137 insertions(+) create mode 100644 dev-packages/browser-integration-tests/suites/replay/canvas/records/init.js create mode 100644 dev-packages/browser-integration-tests/suites/replay/canvas/records/template.html create mode 100644 dev-packages/browser-integration-tests/suites/replay/canvas/records/test.ts create mode 100644 dev-packages/browser-integration-tests/suites/replay/canvas/template.html diff --git a/dev-packages/browser-integration-tests/suites/replay/canvas/records/init.js b/dev-packages/browser-integration-tests/suites/replay/canvas/records/init.js new file mode 100644 index 000000000000..e530d741a8bc --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/replay/canvas/records/init.js @@ -0,0 +1,24 @@ +import { getCanvasManager } from '@sentry-internal/rrweb'; +import * as Sentry from '@sentry/browser'; + +window.Sentry = Sentry; +window.Replay = new Sentry.Replay({ + flushMinDelay: 50, + flushMaxDelay: 50, + minReplayDuration: 0, + _experiments: { + canvas: { + manager: getCanvasManager, + }, + }, +}); + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + sampleRate: 0, + replaysSessionSampleRate: 1.0, + replaysOnErrorSampleRate: 0.0, + debug: true, + + integrations: [window.Replay], +}); diff --git a/dev-packages/browser-integration-tests/suites/replay/canvas/records/template.html b/dev-packages/browser-integration-tests/suites/replay/canvas/records/template.html new file mode 100644 index 000000000000..5f23d569fcc2 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/replay/canvas/records/template.html @@ -0,0 +1,26 @@ + + + + + + + + + + + + + diff --git a/dev-packages/browser-integration-tests/suites/replay/canvas/records/test.ts b/dev-packages/browser-integration-tests/suites/replay/canvas/records/test.ts new file mode 100644 index 000000000000..372ca8978356 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/replay/canvas/records/test.ts @@ -0,0 +1,71 @@ +import { expect } from '@playwright/test'; + +import { sentryTest } from '../../../../utils/fixtures'; +import { getReplayRecordingContent, shouldSkipReplayTest, waitForReplayRequest } from '../../../../utils/replayHelpers'; + +sentryTest('can record canvas', async ({ getLocalTestUrl, page, browserName }) => { + if (shouldSkipReplayTest() || browserName === 'webkit') { + sentryTest.skip(); + } + + const reqPromise0 = waitForReplayRequest(page, 0); + const reqPromise1 = waitForReplayRequest(page, 1); + const reqPromise2 = waitForReplayRequest(page, 2); + + await page.route('https://dsn.ingest.sentry.io/**/*', route => { + return route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ id: 'test-id' }), + }); + }); + + const url = await getLocalTestUrl({ testDir: __dirname }); + + await page.goto(url); + await reqPromise0; + await Promise.all([page.click('#draw'), reqPromise1]); + + const { incrementalSnapshots } = getReplayRecordingContent(await reqPromise2); + expect(incrementalSnapshots).toEqual( + expect.arrayContaining([ + { + data: { + commands: [ + { + args: [0, 0, 150, 150], + property: 'clearRect', + }, + { + args: [ + { + args: [ + { + data: [ + { + base64: expect.any(String), + rr_type: 'ArrayBuffer', + }, + ], + rr_type: 'Blob', + type: 'image/webp', + }, + ], + rr_type: 'ImageBitmap', + }, + 0, + 0, + ], + property: 'drawImage', + }, + ], + id: 9, + source: 9, + type: 0, + }, + timestamp: 0, + type: 3, + }, + ]), + ); +}); diff --git a/dev-packages/browser-integration-tests/suites/replay/canvas/template.html b/dev-packages/browser-integration-tests/suites/replay/canvas/template.html new file mode 100644 index 000000000000..2b3e2f0b27b4 --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/replay/canvas/template.html @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/dev-packages/browser-integration-tests/utils/replayHelpers.ts b/dev-packages/browser-integration-tests/utils/replayHelpers.ts index 613bd5b447f1..2a951e4a215e 100644 --- a/dev-packages/browser-integration-tests/utils/replayHelpers.ts +++ b/dev-packages/browser-integration-tests/utils/replayHelpers.ts @@ -1,3 +1,4 @@ +/* eslint-disable max-lines */ import type { fullSnapshotEvent, incrementalSnapshotEvent } from '@sentry-internal/rrweb'; import { EventType } from '@sentry-internal/rrweb'; import type { ReplayEventWithTime } from '@sentry/browser'; @@ -5,6 +6,7 @@ import type { InternalEventContext, RecordingEvent, ReplayContainer, + ReplayPluginOptions, Session, } from '@sentry/replay/build/npm/types/types'; import type { Breadcrumb, Event, ReplayEvent, ReplayRecordingMode } from '@sentry/types'; @@ -171,6 +173,8 @@ export function getReplaySnapshot(page: Page): Promise<{ _isPaused: boolean; _isEnabled: boolean; _context: InternalEventContext; + _options: ReplayPluginOptions; + _hasCanvas: boolean; session: Session | undefined; recordingMode: ReplayRecordingMode; }> { @@ -182,6 +186,9 @@ export function getReplaySnapshot(page: Page): Promise<{ _isPaused: replay.isPaused(), _isEnabled: replay.isEnabled(), _context: replay.getContext(), + _options: replay.getOptions(), + // We cannot pass the function through as this is serialized + _hasCanvas: typeof replay.getOptions()._experiments.canvas?.manager === 'function', session: replay.session, recordingMode: replay.recordingMode, }; From 0d0e8500378274537df3a93b686769c8492c7ed9 Mon Sep 17 00:00:00 2001 From: Abhijeet Prasad Date: Fri, 5 Jan 2024 13:41:04 -0500 Subject: [PATCH 13/43] fix(cron): Make name required for instrumentNodeCron option (#10070) This should enforce via TypeScript that users pass in a name, but we can keep the runtime check just in case. --- packages/node/src/cron/node-cron.ts | 4 ++-- packages/node/test/cron.test.ts | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/node/src/cron/node-cron.ts b/packages/node/src/cron/node-cron.ts index 2f422b9a85f8..4495a0b54909 100644 --- a/packages/node/src/cron/node-cron.ts +++ b/packages/node/src/cron/node-cron.ts @@ -2,12 +2,12 @@ import { withMonitor } from '@sentry/core'; import { replaceCronNames } from './common'; export interface NodeCronOptions { - name?: string; + name: string; timezone?: string; } export interface NodeCron { - schedule: (cronExpression: string, callback: () => void, options?: NodeCronOptions) => unknown; + schedule: (cronExpression: string, callback: () => void, options: NodeCronOptions) => unknown; } /** diff --git a/packages/node/test/cron.test.ts b/packages/node/test/cron.test.ts index d37fcf189926..8f479e7a16d4 100644 --- a/packages/node/test/cron.test.ts +++ b/packages/node/test/cron.test.ts @@ -118,6 +118,7 @@ describe('cron check-ins', () => { const cronWithCheckIn = cron.instrumentNodeCron(nodeCron); expect(() => { + // @ts-expect-error Initially missing name cronWithCheckIn.schedule('* * * * *', () => { // }); From 2302fdc145907c1170a56d91b7c3c01765e349d6 Mon Sep 17 00:00:00 2001 From: Tim Fish Date: Sat, 6 Jan 2024 00:10:39 +0100 Subject: [PATCH 14/43] fix(node): `LocalVariables` integration should have correct name (#10084) --- .../integrations/local-variables/local-variables-sync.ts | 2 +- packages/node/test/integrations/localvariables.test.ts | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/node/src/integrations/local-variables/local-variables-sync.ts b/packages/node/src/integrations/local-variables/local-variables-sync.ts index d2b988cca1e9..32dae4599c02 100644 --- a/packages/node/src/integrations/local-variables/local-variables-sync.ts +++ b/packages/node/src/integrations/local-variables/local-variables-sync.ts @@ -208,7 +208,7 @@ function tryNewAsyncSession(): AsyncSession | undefined { } } -const INTEGRATION_NAME = 'LocalVariablesSync'; +const INTEGRATION_NAME = 'LocalVariables'; /** * Adds local variables to exception frames diff --git a/packages/node/test/integrations/localvariables.test.ts b/packages/node/test/integrations/localvariables.test.ts index 94e3ecaea20a..e592c90a3a86 100644 --- a/packages/node/test/integrations/localvariables.test.ts +++ b/packages/node/test/integrations/localvariables.test.ts @@ -169,7 +169,7 @@ describeIf(NODE_VERSION.major >= 18)('LocalVariables', () => { client.setupIntegrations(true); const eventProcessors = client['_eventProcessors']; - const eventProcessor = eventProcessors.find(processor => processor.id === 'LocalVariablesSync'); + const eventProcessor = eventProcessors.find(processor => processor.id === 'LocalVariables'); expect(eventProcessor).toBeDefined(); @@ -306,7 +306,7 @@ describeIf(NODE_VERSION.major >= 18)('LocalVariables', () => { client.setupIntegrations(true); const eventProcessors = client['_eventProcessors']; - const eventProcessor = eventProcessors.find(processor => processor.id === 'LocalVariablesSync'); + const eventProcessor = eventProcessors.find(processor => processor.id === 'LocalVariables'); expect(eventProcessor).toBeDefined(); }); @@ -322,7 +322,7 @@ describeIf(NODE_VERSION.major >= 18)('LocalVariables', () => { client.setupIntegrations(true); const eventProcessors = client['_eventProcessors']; - const eventProcessor = eventProcessors.find(processor => processor.id === 'LocalVariablesSync'); + const eventProcessor = eventProcessors.find(processor => processor.id === 'LocalVariables'); expect(eventProcessor).toBeDefined(); }); From 3fc79168349f03d9bad321ce17e0e1830322e06e Mon Sep 17 00:00:00 2001 From: Abhijeet Prasad Date: Fri, 5 Jan 2024 18:12:27 -0500 Subject: [PATCH 15/43] ref(utils): Clean up timestamp calculation code (#10069) This PR removes the usage of `dynamicRequire` from `packages/utils/src/time.ts` and cleans up our timestamp code to be simpler and reduce bundle size. Removing `dynamicRequire` means that we no longer rely on `perf_hooks` for the `performance` API, but instead try to grab it from `globalThis.performance`. `performance` was added to the global in Node 16, which means we'll fallback to using `Date.now()` in Node 8, 10, 12, 14 (and in v8 just Node 14). I think that is an acceptable tradeoff, we just reduce accuracy for those versions. This does not refactor `browserPerformanceTimeOrigin` code at the bottom of the file, I want to come back and touch that in a follow up PR to reduce the amount of changes made here. I would appreciate reviews/opinions on this, I'm not 100% confident in the changes. --- packages/utils/src/time.ts | 120 ++++++++++--------------------------- 1 file changed, 31 insertions(+), 89 deletions(-) diff --git a/packages/utils/src/time.ts b/packages/utils/src/time.ts index 1c366f09d952..16c7058ae68c 100644 --- a/packages/utils/src/time.ts +++ b/packages/utils/src/time.ts @@ -1,26 +1,6 @@ -import { dynamicRequire, isNodeEnv } from './node'; -import { getGlobalObject } from './worldwide'; +import { GLOBAL_OBJ } from './worldwide'; -// eslint-disable-next-line deprecation/deprecation -const WINDOW = getGlobalObject(); - -/** - * An object that can return the current timestamp in seconds since the UNIX epoch. - */ -interface TimestampSource { - nowSeconds(): number; -} - -/** - * A TimestampSource implementation for environments that do not support the Performance Web API natively. - * - * Note that this TimestampSource does not use a monotonic clock. A call to `nowSeconds` may return a timestamp earlier - * than a previously returned value. We do not try to emulate a monotonic behavior in order to facilitate debugging. It - * is more obvious to explain "why does my span have negative duration" than "why my spans have zero duration". - */ -const dateTimestampSource: TimestampSource = { - nowSeconds: () => Date.now() / 1000, -}; +const ONE_SECOND_IN_MS = 1000; /** * A partial definition of the [Performance Web API]{@link https://developer.mozilla.org/en-US/docs/Web/API/Performance} @@ -37,89 +17,56 @@ interface Performance { now(): number; } +/** + * Returns a timestamp in seconds since the UNIX epoch using the Date API. + * + * TODO(v8): Return type should be rounded. + */ +export function dateTimestampInSeconds(): number { + return Date.now() / ONE_SECOND_IN_MS; +} + /** * Returns a wrapper around the native Performance API browser implementation, or undefined for browsers that do not * support the API. * * Wrapping the native API works around differences in behavior from different browsers. */ -function getBrowserPerformance(): Performance | undefined { - const { performance } = WINDOW; +function createUnixTimestampInSecondsFunc(): () => number { + const { performance } = GLOBAL_OBJ as typeof GLOBAL_OBJ & { performance?: Performance }; if (!performance || !performance.now) { - return undefined; + return dateTimestampInSeconds; } - // Replace performance.timeOrigin with our own timeOrigin based on Date.now(). - // - // This is a partial workaround for browsers reporting performance.timeOrigin such that performance.timeOrigin + - // performance.now() gives a date arbitrarily in the past. - // - // Additionally, computing timeOrigin in this way fills the gap for browsers where performance.timeOrigin is - // undefined. - // - // The assumption that performance.timeOrigin + performance.now() ~= Date.now() is flawed, but we depend on it to - // interact with data coming out of performance entries. - // - // Note that despite recommendations against it in the spec, browsers implement the Performance API with a clock that - // might stop when the computer is asleep (and perhaps under other circumstances). Such behavior causes - // performance.timeOrigin + performance.now() to have an arbitrary skew over Date.now(). In laptop computers, we have - // observed skews that can be as long as days, weeks or months. - // - // See https://github.com/getsentry/sentry-javascript/issues/2590. + // Some browser and environments don't have a timeOrigin, so we fallback to + // using Date.now() to compute the starting time. + const approxStartingTimeOrigin = Date.now() - performance.now(); + const timeOrigin = performance.timeOrigin == undefined ? approxStartingTimeOrigin : performance.timeOrigin; + + // performance.now() is a monotonic clock, which means it starts at 0 when the process begins. To get the current + // wall clock time (actual UNIX timestamp), we need to add the starting time origin and the current time elapsed. // - // BUG: despite our best intentions, this workaround has its limitations. It mostly addresses timings of pageload - // transactions, but ignores the skew built up over time that can aversely affect timestamps of navigation - // transactions of long-lived web pages. - const timeOrigin = Date.now() - performance.now(); - - return { - now: () => performance.now(), - timeOrigin, + // TODO: This does not account for the case where the monotonic clock that powers performance.now() drifts from the + // wall clock time, which causes the returned timestamp to be inaccurate. We should investigate how to detect and + // correct for this. + // See: https://github.com/getsentry/sentry-javascript/issues/2590 + // See: https://github.com/mdn/content/issues/4713 + // See: https://dev.to/noamr/when-a-millisecond-is-not-a-millisecond-3h6 + return () => { + return (timeOrigin + performance.now()) / ONE_SECOND_IN_MS; }; } -/** - * Returns the native Performance API implementation from Node.js. Returns undefined in old Node.js versions that don't - * implement the API. - */ -function getNodePerformance(): Performance | undefined { - try { - const perfHooks = dynamicRequire(module, 'perf_hooks') as { performance: Performance }; - return perfHooks.performance; - } catch (_) { - return undefined; - } -} - -/** - * The Performance API implementation for the current platform, if available. - */ -const platformPerformance: Performance | undefined = isNodeEnv() ? getNodePerformance() : getBrowserPerformance(); - -const timestampSource: TimestampSource = - platformPerformance === undefined - ? dateTimestampSource - : { - nowSeconds: () => (platformPerformance.timeOrigin + platformPerformance.now()) / 1000, - }; - -/** - * Returns a timestamp in seconds since the UNIX epoch using the Date API. - */ -export const dateTimestampInSeconds: () => number = dateTimestampSource.nowSeconds.bind(dateTimestampSource); - /** * Returns a timestamp in seconds since the UNIX epoch using either the Performance or Date APIs, depending on the * availability of the Performance API. * - * See `usingPerformanceAPI` to test whether the Performance API is used. - * * BUG: Note that because of how browsers implement the Performance API, the clock might stop when the computer is * asleep. This creates a skew between `dateTimestampInSeconds` and `timestampInSeconds`. The * skew can grow to arbitrary amounts like days, weeks or months. * See https://github.com/getsentry/sentry-javascript/issues/2590. */ -export const timestampInSeconds: () => number = timestampSource.nowSeconds.bind(timestampSource); +export const timestampInSeconds = createUnixTimestampInSecondsFunc(); /** * Re-exported with an old name for backwards-compatibility. @@ -129,11 +76,6 @@ export const timestampInSeconds: () => number = timestampSource.nowSeconds.bind( */ export const timestampWithMs = timestampInSeconds; -/** - * A boolean that is true when timestampInSeconds uses the Performance API to produce monotonic timestamps. - */ -export const usingPerformanceAPI = platformPerformance !== undefined; - /** * Internal helper to store what is the source of browserPerformanceTimeOrigin below. For debugging only. */ @@ -148,7 +90,7 @@ export const browserPerformanceTimeOrigin = ((): number | undefined => { // performance.timing.navigationStart, which results in poor results in performance data. We only treat time origin // data as reliable if they are within a reasonable threshold of the current time. - const { performance } = WINDOW; + const { performance } = GLOBAL_OBJ as typeof GLOBAL_OBJ & Window; if (!performance || !performance.now) { _browserPerformanceTimeOriginMode = 'none'; return undefined; From 5aac890dfd0909e9cd28e90210957766e96de552 Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Mon, 8 Jan 2024 09:39:03 +0100 Subject: [PATCH 16/43] feat(core): Add `spanToJSON()` method to get span properties (#10074) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This is supposed to be an internal API and not necessarily to be used by users. Naming wise, it's a bit tricky... I went with `JSON` to make it very clear what this is for, but 🤷 --- packages/core/package.json | 7 ++ packages/core/src/index.ts | 5 +- packages/core/src/tracing/span.ts | 26 +++---- packages/core/src/tracing/transaction.ts | 1 + packages/core/src/utils/spanUtils.ts | 38 +++++++++- .../core/test/lib/utils/spanUtils.test.ts | 71 ++++++++++++++++++- .../test/integration/transactions.test.ts | 5 +- .../test/custom/transaction.test.ts | 5 +- .../test/integration/transactions.test.ts | 5 +- packages/types/src/index.ts | 10 ++- packages/types/src/span.ts | 34 +++++---- 11 files changed, 167 insertions(+), 40 deletions(-) diff --git a/packages/core/package.json b/packages/core/package.json index 3ef693023f85..61c2e117e6ea 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -56,5 +56,12 @@ "volta": { "extends": "../../package.json" }, + "madge": { + "detectiveOptions": { + "ts": { + "skipTypeImports": true + } + } + }, "sideEffects": false } diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 1585e119d249..20b6a600a593 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -75,7 +75,10 @@ export { createCheckInEnvelope } from './checkin'; export { hasTracingEnabled } from './utils/hasTracingEnabled'; export { isSentryRequestUrl } from './utils/isSentryRequestUrl'; export { handleCallbackErrors } from './utils/handleCallbackErrors'; -export { spanToTraceHeader } from './utils/spanUtils'; +export { + spanToTraceHeader, + spanToJSON, +} from './utils/spanUtils'; export { DEFAULT_ENVIRONMENT } from './constants'; export { ModuleMetadata } from './integrations/metadata'; export { RequestData } from './integrations/requestdata'; diff --git a/packages/core/src/tracing/span.ts b/packages/core/src/tracing/span.ts index a3126e5763b0..e66f9c5caa04 100644 --- a/packages/core/src/tracing/span.ts +++ b/packages/core/src/tracing/span.ts @@ -6,6 +6,7 @@ import type { SpanAttributeValue, SpanAttributes, SpanContext, + SpanJSON, SpanOrigin, SpanTimeInput, TraceContext, @@ -372,22 +373,9 @@ export class Span implements SpanInterface { } /** - * @inheritDoc + * Get JSON representation of this span. */ - public toJSON(): { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - data?: { [key: string]: any }; - description?: string; - op?: string; - parent_span_id?: string; - span_id: string; - start_timestamp: number; - status?: string; - tags?: { [key: string]: Primitive }; - timestamp?: number; - trace_id: string; - origin?: SpanOrigin; - } { + public getSpanJSON(): SpanJSON { return dropUndefinedKeys({ data: this._getData(), description: this.description, @@ -408,6 +396,14 @@ export class Span implements SpanInterface { return !this.endTimestamp && !!this.sampled; } + /** + * Convert the object to JSON. + * @deprecated Use `spanToJSON(span)` instead. + */ + public toJSON(): SpanJSON { + return this.getSpanJSON(); + } + /** * Get the merged data for this span. * For now, this combines `data` and `attributes` together, diff --git a/packages/core/src/tracing/transaction.ts b/packages/core/src/tracing/transaction.ts index 7142ec3419e7..e0635c7e6b80 100644 --- a/packages/core/src/tracing/transaction.ts +++ b/packages/core/src/tracing/transaction.ts @@ -285,6 +285,7 @@ export class Transaction extends SpanClass implements TransactionInterface { // We don't want to override trace context trace: spanToTraceContext(this), }, + // TODO: Pass spans serialized via `spanToJSON()` here instead in v8. spans: finishedSpans, start_timestamp: this.startTimestamp, tags: this.tags, diff --git a/packages/core/src/utils/spanUtils.ts b/packages/core/src/utils/spanUtils.ts index 9c4dcff17b62..9ecb98a6c990 100644 --- a/packages/core/src/utils/spanUtils.ts +++ b/packages/core/src/utils/spanUtils.ts @@ -1,11 +1,13 @@ -import type { Span, SpanTimeInput, TraceContext } from '@sentry/types'; +import type { Span, SpanJSON, SpanTimeInput, TraceContext } from '@sentry/types'; import { dropUndefinedKeys, generateSentryTraceHeader, timestampInSeconds } from '@sentry/utils'; +import type { Span as SpanClass } from '../tracing/span'; /** * Convert a span to a trace context, which can be sent as the `trace` context in an event. */ export function spanToTraceContext(span: Span): TraceContext { - const { data, description, op, parent_span_id, span_id, status, tags, trace_id, origin } = span.toJSON(); + const { spanId: span_id, traceId: trace_id } = span; + const { data, description, op, parent_span_id, status, tags, origin } = spanToJSON(span); return dropUndefinedKeys({ data, @@ -54,3 +56,35 @@ function ensureTimestampInSeconds(timestamp: number): number { const isMs = timestamp > 9999999999; return isMs ? timestamp / 1000 : timestamp; } + +/** + * Convert a span to a JSON representation. + * Note that all fields returned here are optional and need to be guarded against. + * + * Note: Because of this, we currently have a circular type dependency (which we opted out of in package.json). + * This is not avoidable as we need `spanToJSON` in `spanUtils.ts`, which in turn is needed by `span.ts` for backwards compatibility. + * And `spanToJSON` needs the Span class from `span.ts` to check here. + * TODO v8: When we remove the deprecated stuff from `span.ts`, we can remove the circular dependency again. + */ +export function spanToJSON(span: Span): Partial { + if (spanIsSpanClass(span)) { + return span.getSpanJSON(); + } + + // Fallback: We also check for `.toJSON()` here... + // eslint-disable-next-line deprecation/deprecation + if (typeof span.toJSON === 'function') { + // eslint-disable-next-line deprecation/deprecation + return span.toJSON(); + } + + return {}; +} + +/** + * Sadly, due to circular dependency checks we cannot actually import the Span class here and check for instanceof. + * :( So instead we approximate this by checking if it has the `getSpanJSON` method. + */ +function spanIsSpanClass(span: Span): span is SpanClass { + return typeof (span as SpanClass).getSpanJSON === 'function'; +} diff --git a/packages/core/test/lib/utils/spanUtils.test.ts b/packages/core/test/lib/utils/spanUtils.test.ts index a6d84cf31166..d4b0afc7a7a6 100644 --- a/packages/core/test/lib/utils/spanUtils.test.ts +++ b/packages/core/test/lib/utils/spanUtils.test.ts @@ -1,6 +1,6 @@ import { TRACEPARENT_REGEXP, timestampInSeconds } from '@sentry/utils'; import { Span, spanToTraceHeader } from '../../../src'; -import { spanTimeInputToSeconds } from '../../../src/utils/spanUtils'; +import { spanTimeInputToSeconds, spanToJSON } from '../../../src/utils/spanUtils'; describe('spanToTraceHeader', () => { test('simple', () => { @@ -46,3 +46,72 @@ describe('spanTimeInputToSeconds', () => { expect(spanTimeInputToSeconds(timestamp)).toEqual(seconds + 0.000009); }); }); + +describe('spanToJSON', () => { + it('works with a simple span', () => { + const span = new Span(); + expect(spanToJSON(span)).toEqual({ + span_id: span.spanId, + trace_id: span.traceId, + origin: 'manual', + start_timestamp: span.startTimestamp, + }); + }); + + it('works with a full span', () => { + const span = new Span({ + name: 'test name', + op: 'test op', + parentSpanId: '1234', + spanId: '5678', + status: 'ok', + tags: { + foo: 'bar', + }, + traceId: 'abcd', + origin: 'auto', + startTimestamp: 123, + }); + + expect(spanToJSON(span)).toEqual({ + description: 'test name', + op: 'test op', + parent_span_id: '1234', + span_id: '5678', + status: 'ok', + tags: { + foo: 'bar', + }, + trace_id: 'abcd', + origin: 'auto', + start_timestamp: 123, + }); + }); + + it('works with a custom class without spanToJSON', () => { + const span = { + toJSON: () => { + return { + span_id: 'span_id', + trace_id: 'trace_id', + origin: 'manual', + start_timestamp: 123, + }; + }, + } as unknown as Span; + + expect(spanToJSON(span)).toEqual({ + span_id: 'span_id', + trace_id: 'trace_id', + origin: 'manual', + start_timestamp: 123, + }); + }); + + it('returns empty object if span does not have getter methods', () => { + // eslint-disable-next-line + const span = new Span().toJSON(); + + expect(spanToJSON(span as unknown as Span)).toEqual({}); + }); +}); diff --git a/packages/node-experimental/test/integration/transactions.test.ts b/packages/node-experimental/test/integration/transactions.test.ts index be48f5f9e6b5..5198c532ef79 100644 --- a/packages/node-experimental/test/integration/transactions.test.ts +++ b/packages/node-experimental/test/integration/transactions.test.ts @@ -1,6 +1,7 @@ import { SpanKind, TraceFlags, context, trace } from '@opentelemetry/api'; import type { SpanProcessor } from '@opentelemetry/sdk-trace-base'; import { SemanticAttributes } from '@opentelemetry/semantic-conventions'; +import { spanToJSON } from '@sentry/core'; import { SentrySpanProcessor, getCurrentHub, setPropagationContextOnContext } from '@sentry/opentelemetry'; import type { Integration, PropagationContext, TransactionEvent } from '@sentry/types'; import { logger } from '@sentry/utils'; @@ -145,7 +146,7 @@ describe('Integration | Transactions', () => { // note: Currently, spans do not have any context/span added to them // This is the same behavior as for the "regular" SDKs - expect(spans.map(span => span.toJSON())).toEqual([ + expect(spans.map(span => spanToJSON(span))).toEqual([ { data: { 'otel.kind': 'INTERNAL' }, description: 'inner span 1', @@ -399,7 +400,7 @@ describe('Integration | Transactions', () => { // note: Currently, spans do not have any context/span added to them // This is the same behavior as for the "regular" SDKs - expect(spans.map(span => span.toJSON())).toEqual([ + expect(spans.map(span => spanToJSON(span))).toEqual([ { data: { 'otel.kind': 'INTERNAL' }, description: 'inner span 1', diff --git a/packages/opentelemetry/test/custom/transaction.test.ts b/packages/opentelemetry/test/custom/transaction.test.ts index 043d76235140..5377371b3077 100644 --- a/packages/opentelemetry/test/custom/transaction.test.ts +++ b/packages/opentelemetry/test/custom/transaction.test.ts @@ -1,3 +1,4 @@ +import { spanToJSON } from '@sentry/core'; import { getCurrentHub } from '../../src/custom/hub'; import { OpenTelemetryScope } from '../../src/custom/scope'; import { OpenTelemetryTransaction, startTransaction } from '../../src/custom/transaction'; @@ -157,7 +158,7 @@ describe('startTranscation', () => { spanMetadata: {}, }); - expect(transaction.toJSON()).toEqual( + expect(spanToJSON(transaction)).toEqual( expect.objectContaining({ origin: 'manual', span_id: expect.any(String), @@ -186,7 +187,7 @@ describe('startTranscation', () => { spanMetadata: {}, }); - expect(transaction.toJSON()).toEqual( + expect(spanToJSON(transaction)).toEqual( expect.objectContaining({ origin: 'manual', span_id: 'span1', diff --git a/packages/opentelemetry/test/integration/transactions.test.ts b/packages/opentelemetry/test/integration/transactions.test.ts index 787d251b0558..de5193292cef 100644 --- a/packages/opentelemetry/test/integration/transactions.test.ts +++ b/packages/opentelemetry/test/integration/transactions.test.ts @@ -4,6 +4,7 @@ import { addBreadcrumb, setTag } from '@sentry/core'; import type { PropagationContext, TransactionEvent } from '@sentry/types'; import { logger } from '@sentry/utils'; +import { spanToJSON } from '@sentry/core'; import { getCurrentHub } from '../../src/custom/hub'; import { SentrySpanProcessor } from '../../src/spanProcessor'; import { startInactiveSpan, startSpan } from '../../src/trace'; @@ -142,7 +143,7 @@ describe('Integration | Transactions', () => { // note: Currently, spans do not have any context/span added to them // This is the same behavior as for the "regular" SDKs - expect(spans.map(span => span.toJSON())).toEqual([ + expect(spans.map(span => spanToJSON(span))).toEqual([ { data: { 'otel.kind': 'INTERNAL' }, description: 'inner span 1', @@ -393,7 +394,7 @@ describe('Integration | Transactions', () => { // note: Currently, spans do not have any context/span added to them // This is the same behavior as for the "regular" SDKs - expect(spans.map(span => span.toJSON())).toEqual([ + expect(spans.map(span => spanToJSON(span))).toEqual([ { data: { 'otel.kind': 'INTERNAL' }, description: 'inner span 1', diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index a6d259870714..d47e8407e30c 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -89,7 +89,15 @@ export type { // eslint-disable-next-line deprecation/deprecation export type { Severity, SeverityLevel } from './severity'; -export type { Span, SpanContext, SpanOrigin, SpanAttributeValue, SpanAttributes, SpanTimeInput } from './span'; +export type { + Span, + SpanContext, + SpanOrigin, + SpanAttributeValue, + SpanAttributes, + SpanTimeInput, + SpanJSON, +} from './span'; export type { StackFrame } from './stackframe'; export type { Stacktrace, StackParser, StackLineParser, StackLineParserFn } from './stacktrace'; export type { TextEncoderInternal } from './textencoder'; diff --git a/packages/types/src/span.ts b/packages/types/src/span.ts index 4ad0e5aa1110..af51afd0e5e9 100644 --- a/packages/types/src/span.ts +++ b/packages/types/src/span.ts @@ -28,6 +28,21 @@ export type SpanAttributes = Record; /** This type is aligned with the OpenTelemetry TimeInput type. */ export type SpanTimeInput = HrTime | number | Date; +/** A JSON representation of a span. */ +export interface SpanJSON { + data?: { [key: string]: any }; + description?: string; + op?: string; + parent_span_id?: string; + span_id: string; + start_timestamp: number; + status?: string; + tags?: { [key: string]: Primitive }; + timestamp?: number; + trace_id: string; + origin?: SpanOrigin; +} + /** Interface holding all properties that can be set on a Span on creation. */ export interface SpanContext { /** @@ -256,20 +271,11 @@ export interface Span extends SpanContext { */ getTraceContext(): TraceContext; - /** Convert the object to JSON */ - toJSON(): { - data?: { [key: string]: any }; - description?: string; - op?: string; - parent_span_id?: string; - span_id: string; - start_timestamp: number; - status?: string; - tags?: { [key: string]: Primitive }; - timestamp?: number; - trace_id: string; - origin?: SpanOrigin; - }; + /** + * Convert the object to JSON. + * @deprecated Use `spanToJSON(span)` instead. + */ + toJSON(): SpanJSON; /** * If this is span is actually recording data. From 548d455738c426f6e3dea2fee2466a1f0db632b4 Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Mon, 8 Jan 2024 11:56:29 +0100 Subject: [PATCH 17/43] feat(core): Deprecate `span.startChild()` (#10091) We still have quite a lot of these ourselves, but we can refactor them over time during the v8 preparation. But with this, I think we have the most important deprecations mostly done! --- MIGRATION.md | 4 +- .../create-next-app/pages/api/success.ts | 1 + .../node-express-app/src/app.ts | 1 + .../with-nested-spans/scenario.ts | 5 + packages/angular/src/tracing.ts | 4 + packages/core/src/tracing/span.ts | 5 +- packages/core/src/tracing/trace.ts | 6 +- packages/ember/addon/index.ts | 1 + .../sentry-performance.ts | 5 +- .../pagesRouterRoutingInstrumentation.ts | 1 + .../nextjs/src/common/utils/wrapperUtils.ts | 2 + packages/node/src/integrations/http.ts | 3 +- .../node/src/integrations/undici/index.ts | 1 + packages/node/test/handlers.test.ts | 1 + .../opentelemetry-node/src/spanprocessor.ts | 1 + .../test/propagator.test.ts | 1 + packages/opentelemetry/src/spanExporter.ts | 1 + packages/react/src/profiler.tsx | 5 + packages/remix/src/utils/instrumentServer.ts | 2 + packages/serverless/src/awsservices.ts | 1 + packages/serverless/src/google-cloud-grpc.ts | 1 + packages/serverless/src/google-cloud-http.ts | 1 + packages/svelte/src/performance.ts | 2 + packages/sveltekit/src/client/router.ts | 1 + packages/sveltekit/test/client/router.test.ts | 2 + .../src/browser/metrics/index.ts | 2 + .../src/browser/metrics/utils.ts | 1 + .../tracing-internal/src/browser/request.ts | 3 +- packages/tracing-internal/src/common/fetch.ts | 3 +- .../src/node/integrations/apollo.ts | 1 + .../src/node/integrations/express.ts | 3 + .../src/node/integrations/graphql.ts | 1 + .../src/node/integrations/mongo.ts | 2 + .../src/node/integrations/mysql.ts | 1 + .../src/node/integrations/postgres.ts | 1 + .../test/browser/metrics/index.test.ts | 30 ++--- packages/tracing/test/idletransaction.test.ts | 104 ++++++++++-------- packages/types/src/span.ts | 2 + packages/vue/src/tracing.ts | 2 + 39 files changed, 147 insertions(+), 67 deletions(-) diff --git a/MIGRATION.md b/MIGRATION.md index 30eb4e4ca1f4..1aa0d8ea9270 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -8,9 +8,9 @@ npx @sentry/migr8@latest This will let you select which updates to run, and automatically update your code. Make sure to still review all code changes! -## Deprecate `startTransaction()` +## Deprecate `startTransaction()` & `span.startChild()` -In v8, the old performance API `startTransaction()` (as well as `hub.startTransaction()`) will be removed. +In v8, the old performance API `startTransaction()` (and `hub.startTransaction()`), as well as `span.startChild()`, will be removed. Instead, use the new performance APIs: * `startSpan()` diff --git a/dev-packages/e2e-tests/test-applications/create-next-app/pages/api/success.ts b/dev-packages/e2e-tests/test-applications/create-next-app/pages/api/success.ts index 4b7b8703332a..a54eb321c385 100644 --- a/dev-packages/e2e-tests/test-applications/create-next-app/pages/api/success.ts +++ b/dev-packages/e2e-tests/test-applications/create-next-app/pages/api/success.ts @@ -7,6 +7,7 @@ export default function handler(req: NextApiRequest, res: NextApiResponse) { const transaction = Sentry.startTransaction({ name: 'test-transaction', op: 'e2e-test' }); Sentry.getCurrentHub().getScope().setSpan(transaction); + // eslint-disable-next-line deprecation/deprecation const span = transaction.startChild(); span.end(); diff --git a/dev-packages/e2e-tests/test-applications/node-express-app/src/app.ts b/dev-packages/e2e-tests/test-applications/node-express-app/src/app.ts index 8ab1935c723e..43c952b23594 100644 --- a/dev-packages/e2e-tests/test-applications/node-express-app/src/app.ts +++ b/dev-packages/e2e-tests/test-applications/node-express-app/src/app.ts @@ -38,6 +38,7 @@ app.get('/test-transaction', async function (req, res) { const transaction = Sentry.startTransaction({ name: 'test-transaction', op: 'e2e-test' }); Sentry.getCurrentScope().setSpan(transaction); + // eslint-disable-next-line deprecation/deprecation const span = transaction.startChild(); span.end(); diff --git a/dev-packages/node-integration-tests/suites/public-api/startTransaction/with-nested-spans/scenario.ts b/dev-packages/node-integration-tests/suites/public-api/startTransaction/with-nested-spans/scenario.ts index 97fc6874770f..f82fe81d969a 100644 --- a/dev-packages/node-integration-tests/suites/public-api/startTransaction/with-nested-spans/scenario.ts +++ b/dev-packages/node-integration-tests/suites/public-api/startTransaction/with-nested-spans/scenario.ts @@ -10,6 +10,7 @@ Sentry.init({ // eslint-disable-next-line deprecation/deprecation const transaction = Sentry.startTransaction({ name: 'test_transaction_1' }); +// eslint-disable-next-line deprecation/deprecation const span_1 = transaction.startChild({ op: 'span_1', data: { @@ -23,16 +24,20 @@ for (let i = 0; i < 2000; i++); span_1.end(); // span_2 doesn't finish +// eslint-disable-next-line deprecation/deprecation transaction.startChild({ op: 'span_2' }); for (let i = 0; i < 4000; i++); +// eslint-disable-next-line deprecation/deprecation const span_3 = transaction.startChild({ op: 'span_3' }); for (let i = 0; i < 4000; i++); // span_4 is the child of span_3 but doesn't finish. +// eslint-disable-next-line deprecation/deprecation span_3.startChild({ op: 'span_4', data: { qux: 'quux' } }); // span_5 is another child of span_3 but finishes. +// eslint-disable-next-line deprecation/deprecation span_3.startChild({ op: 'span_5' }).end(); // span_3 also finishes diff --git a/packages/angular/src/tracing.ts b/packages/angular/src/tracing.ts index e65ecd84d8df..c34fa822cb14 100644 --- a/packages/angular/src/tracing.ts +++ b/packages/angular/src/tracing.ts @@ -84,6 +84,7 @@ export class TraceService implements OnDestroy { if (this._routingSpan) { this._routingSpan.end(); } + // eslint-disable-next-line deprecation/deprecation this._routingSpan = activeTransaction.startChild({ description: `${navigationEvent.url}`, op: ANGULAR_ROUTING_OP, @@ -183,6 +184,7 @@ export class TraceDirective implements OnInit, AfterViewInit { const activeTransaction = getActiveTransaction(); if (activeTransaction) { + // eslint-disable-next-line deprecation/deprecation this._tracingSpan = activeTransaction.startChild({ description: `<${this.componentName}>`, op: ANGULAR_INIT_OP, @@ -225,6 +227,7 @@ export function TraceClassDecorator(): ClassDecorator { target.prototype.ngOnInit = function (...args: any[]): ReturnType { const activeTransaction = getActiveTransaction(); if (activeTransaction) { + // eslint-disable-next-line deprecation/deprecation tracingSpan = activeTransaction.startChild({ description: `<${target.name}>`, op: ANGULAR_INIT_OP, @@ -262,6 +265,7 @@ export function TraceMethodDecorator(): MethodDecorator { const now = timestampInSeconds(); const activeTransaction = getActiveTransaction(); if (activeTransaction) { + // eslint-disable-next-line deprecation/deprecation activeTransaction.startChild({ description: `<${target.constructor.name}>`, endTimestamp: now, diff --git a/packages/core/src/tracing/span.ts b/packages/core/src/tracing/span.ts index e66f9c5caa04..a5405ebe0fbf 100644 --- a/packages/core/src/tracing/span.ts +++ b/packages/core/src/tracing/span.ts @@ -187,7 +187,10 @@ export class Span implements SpanInterface { } /** - * @inheritDoc + * Creates a new `Span` while setting the current `Span.id` as `parentSpanId`. + * Also the `sampled` decision will be inherited. + * + * @deprecated Use `startSpan()`, `startSpanManual()` or `startInactiveSpan()` instead. */ public startChild( spanContext?: Pick>, diff --git a/packages/core/src/tracing/trace.ts b/packages/core/src/tracing/trace.ts index d5b6aef1d1fe..95aa3fda451d 100644 --- a/packages/core/src/tracing/trace.ts +++ b/packages/core/src/tracing/trace.ts @@ -158,7 +158,8 @@ export function startInactiveSpan(context: StartSpanOptions): Span | undefined { const hub = getCurrentHub(); const parentSpan = getActiveSpan(); return parentSpan - ? parentSpan.startChild(ctx) + ? // eslint-disable-next-line deprecation/deprecation + parentSpan.startChild(ctx) : // eslint-disable-next-line deprecation/deprecation hub.startTransaction(ctx); } @@ -240,7 +241,8 @@ function createChildSpanOrTransaction( return undefined; } return parentSpan - ? parentSpan.startChild(ctx) + ? // eslint-disable-next-line deprecation/deprecation + parentSpan.startChild(ctx) : // eslint-disable-next-line deprecation/deprecation hub.startTransaction(ctx); } diff --git a/packages/ember/addon/index.ts b/packages/ember/addon/index.ts index 2a5da643c984..4f62a19b3ba3 100644 --- a/packages/ember/addon/index.ts +++ b/packages/ember/addon/index.ts @@ -88,6 +88,7 @@ export const instrumentRoutePerformance = (BaseRoute return result; } currentTransaction + // eslint-disable-next-line deprecation/deprecation .startChild({ op, description, diff --git a/packages/ember/addon/instance-initializers/sentry-performance.ts b/packages/ember/addon/instance-initializers/sentry-performance.ts index 41d10842fa7c..1a502d052774 100644 --- a/packages/ember/addon/instance-initializers/sentry-performance.ts +++ b/packages/ember/addon/instance-initializers/sentry-performance.ts @@ -149,6 +149,7 @@ export function _instrumentEmberRouter( 'routing.instrumentation': '@sentry/ember', }, }); + // eslint-disable-next-line deprecation/deprecation transitionSpan = activeTransaction?.startChild({ op: 'ui.ember.transition', description: `route:${fromRoute} -> route:${toRoute}`, @@ -212,6 +213,7 @@ function _instrumentEmberRunloop(config: EmberSentryConfig): void { if ((now - currentQueueStart) * 1000 >= minQueueDuration) { activeTransaction + // eslint-disable-next-line deprecation/deprecation ?.startChild({ op: `ui.ember.runloop.${queue}`, origin: 'auto.ui.ember', @@ -287,7 +289,7 @@ function processComponentRenderAfter( if (componentRenderDuration * 1000 >= minComponentDuration) { const activeTransaction = getActiveTransaction(); - + // eslint-disable-next-line deprecation/deprecation activeTransaction?.startChild({ op, description: payload.containerKey || payload.object, @@ -373,6 +375,7 @@ function _instrumentInitialLoad(config: EmberSentryConfig): void { const endTimestamp = startTimestamp + measure.duration / 1000; const transaction = getActiveTransaction(); + // eslint-disable-next-line deprecation/deprecation const span = transaction?.startChild({ op: 'ui.ember.init', origin: 'auto.ui.ember', diff --git a/packages/nextjs/src/client/routing/pagesRouterRoutingInstrumentation.ts b/packages/nextjs/src/client/routing/pagesRouterRoutingInstrumentation.ts index 1e31ffaaef0c..5f2064c690e4 100644 --- a/packages/nextjs/src/client/routing/pagesRouterRoutingInstrumentation.ts +++ b/packages/nextjs/src/client/routing/pagesRouterRoutingInstrumentation.ts @@ -186,6 +186,7 @@ export function pagesRouterInstrumentation( // We don't want to finish the navigation transaction on `routeChangeComplete`, since users might want to attach // spans to that transaction even after `routeChangeComplete` is fired (eg. HTTP requests in some useEffect // hooks). Instead, we'll simply let the navigation transaction finish itself (it's an `IdleTransaction`). + // eslint-disable-next-line deprecation/deprecation const nextRouteChangeSpan = navigationTransaction.startChild({ op: 'ui.nextjs.route-change', origin: 'auto.ui.nextjs.pages_router_instrumentation', diff --git a/packages/nextjs/src/common/utils/wrapperUtils.ts b/packages/nextjs/src/common/utils/wrapperUtils.ts index da0c11df8640..0731ab9b326a 100644 --- a/packages/nextjs/src/common/utils/wrapperUtils.ts +++ b/packages/nextjs/src/common/utils/wrapperUtils.ts @@ -131,6 +131,7 @@ export function withTracedServerSideDataFetcher Pr spanToContinue = previousSpan; } + // eslint-disable-next-line deprecation/deprecation dataFetcherSpan = spanToContinue.startChild({ op: 'function.nextjs', description: `${options.dataFetchingMethodName} (${options.dataFetcherRouteName})`, @@ -209,6 +210,7 @@ export async function callDataFetcherTraced Promis // Capture the route, since pre-loading, revalidation, etc might mean that this span may happen during another // route's transaction + // eslint-disable-next-line deprecation/deprecation const span = transaction.startChild({ op: 'function.nextjs', origin: 'auto.function.nextjs', diff --git a/packages/node/src/integrations/http.ts b/packages/node/src/integrations/http.ts index 61046eb8f38d..f083142261dc 100644 --- a/packages/node/src/integrations/http.ts +++ b/packages/node/src/integrations/http.ts @@ -251,7 +251,8 @@ function _createWrappedRequestMethodFactory( const data = getRequestSpanData(requestUrl, requestOptions); const requestSpan = shouldCreateSpan(rawRequestUrl) - ? parentSpan?.startChild({ + ? // eslint-disable-next-line deprecation/deprecation + parentSpan?.startChild({ op: 'http.client', origin: 'auto.http.node.http', description: `${data['http.method']} ${data.url}`, diff --git a/packages/node/src/integrations/undici/index.ts b/packages/node/src/integrations/undici/index.ts index 117ef602ac38..b53281f3c2d1 100644 --- a/packages/node/src/integrations/undici/index.ts +++ b/packages/node/src/integrations/undici/index.ts @@ -310,6 +310,7 @@ function createRequestSpan( if (url.hash) { data['http.fragment'] = url.hash; } + // eslint-disable-next-line deprecation/deprecation return activeSpan?.startChild({ op: 'http.client', origin: 'auto.http.node.undici', diff --git a/packages/node/test/handlers.test.ts b/packages/node/test/handlers.test.ts index 14de421db5f7..91f610ef24da 100644 --- a/packages/node/test/handlers.test.ts +++ b/packages/node/test/handlers.test.ts @@ -424,6 +424,7 @@ describe('tracingHandler', () => { it('waits to finish transaction until all spans are finished, even though `transaction.end()` is registered on `res.finish` event first', done => { const transaction = new Transaction({ name: 'mockTransaction', sampled: true }); transaction.initSpanRecorder(); + // eslint-disable-next-line deprecation/deprecation const span = transaction.startChild({ description: 'reallyCoolHandler', op: 'middleware', diff --git a/packages/opentelemetry-node/src/spanprocessor.ts b/packages/opentelemetry-node/src/spanprocessor.ts index 5b1a1684c9cf..22a3a01b7671 100644 --- a/packages/opentelemetry-node/src/spanprocessor.ts +++ b/packages/opentelemetry-node/src/spanprocessor.ts @@ -56,6 +56,7 @@ export class SentrySpanProcessor implements OtelSpanProcessor { const sentryParentSpan = otelParentSpanId && getSentrySpan(otelParentSpanId); if (sentryParentSpan) { + // eslint-disable-next-line deprecation/deprecation const sentryChildSpan = sentryParentSpan.startChild({ description: otelSpan.name, instrumenter: 'otel', diff --git a/packages/opentelemetry-node/test/propagator.test.ts b/packages/opentelemetry-node/test/propagator.test.ts index 345cd9c6eceb..24e30b1e2bc0 100644 --- a/packages/opentelemetry-node/test/propagator.test.ts +++ b/packages/opentelemetry-node/test/propagator.test.ts @@ -65,6 +65,7 @@ describe('SentryPropagator', () => { if (type === PerfType.Span) { // eslint-disable-next-line @typescript-eslint/no-unused-vars const { spanId, ...ctx } = transactionContext; + // eslint-disable-next-line deprecation/deprecation const span = transaction.startChild({ ...ctx, description: transaction.name }); setSentrySpan(span.spanId, span); } diff --git a/packages/opentelemetry/src/spanExporter.ts b/packages/opentelemetry/src/spanExporter.ts index 8c515fc0afc9..78557e50d33c 100644 --- a/packages/opentelemetry/src/spanExporter.ts +++ b/packages/opentelemetry/src/spanExporter.ts @@ -204,6 +204,7 @@ function createAndFinishSpanForOtelSpan(node: SpanNode, sentryParentSpan: Sentry const { op, description, tags, data, origin } = getSpanData(span); const allData = { ...removeSentryAttributes(attributes), ...data }; + // eslint-disable-next-line deprecation/deprecation const sentrySpan = sentryParentSpan.startChild({ description, op, diff --git a/packages/react/src/profiler.tsx b/packages/react/src/profiler.tsx index 749da5e23167..43a8f166d48b 100644 --- a/packages/react/src/profiler.tsx +++ b/packages/react/src/profiler.tsx @@ -59,6 +59,7 @@ class Profiler extends React.Component { const activeTransaction = getActiveTransaction(); if (activeTransaction) { + // eslint-disable-next-line deprecation/deprecation this._mountSpan = activeTransaction.startChild({ description: `<${name}>`, op: REACT_MOUNT_OP, @@ -85,6 +86,7 @@ class Profiler extends React.Component { const changedProps = Object.keys(updateProps).filter(k => updateProps[k] !== this.props.updateProps[k]); if (changedProps.length > 0) { const now = timestampInSeconds(); + // eslint-disable-next-line deprecation/deprecation this._updateSpan = this._mountSpan.startChild({ data: { changedProps, @@ -116,6 +118,7 @@ class Profiler extends React.Component { if (this._mountSpan && includeRender) { // If we were able to obtain the spanId of the mount activity, we should set the // next activity as a child to the component mount activity. + // eslint-disable-next-line deprecation/deprecation this._mountSpan.startChild({ description: `<${name}>`, endTimestamp: timestampInSeconds(), @@ -183,6 +186,7 @@ function useProfiler( const activeTransaction = getActiveTransaction(); if (activeTransaction) { + // eslint-disable-next-line deprecation/deprecation return activeTransaction.startChild({ description: `<${name}>`, op: REACT_MOUNT_OP, @@ -201,6 +205,7 @@ function useProfiler( return (): void => { if (mountSpan && options.hasRenderSpan) { + // eslint-disable-next-line deprecation/deprecation mountSpan.startChild({ description: `<${name}>`, endTimestamp: timestampInSeconds(), diff --git a/packages/remix/src/utils/instrumentServer.ts b/packages/remix/src/utils/instrumentServer.ts index e07710dc340a..4da8826b131e 100644 --- a/packages/remix/src/utils/instrumentServer.ts +++ b/packages/remix/src/utils/instrumentServer.ts @@ -184,6 +184,7 @@ function makeWrappedDocumentRequestFunction(remixVersion?: number) { const activeTransaction = getActiveTransaction(); try { + // eslint-disable-next-line deprecation/deprecation const span = activeTransaction?.startChild({ op: 'function.remix.document_request', origin: 'auto.function.remix', @@ -239,6 +240,7 @@ function makeWrappedDataFunction( const currentScope = getCurrentScope(); try { + // eslint-disable-next-line deprecation/deprecation const span = activeTransaction?.startChild({ op: `function.remix.${name}`, origin: 'auto.ui.remix', diff --git a/packages/serverless/src/awsservices.ts b/packages/serverless/src/awsservices.ts index e7404b0695c4..7dc3ef010abd 100644 --- a/packages/serverless/src/awsservices.ts +++ b/packages/serverless/src/awsservices.ts @@ -62,6 +62,7 @@ function wrapMakeRequest( const req = orig.call(this, operation, params); req.on('afterBuild', () => { if (transaction) { + // eslint-disable-next-line deprecation/deprecation span = transaction.startChild({ description: describe(this, operation, params), op: 'http.client', diff --git a/packages/serverless/src/google-cloud-grpc.ts b/packages/serverless/src/google-cloud-grpc.ts index 9f2ea37203b4..d3815422e4df 100644 --- a/packages/serverless/src/google-cloud-grpc.ts +++ b/packages/serverless/src/google-cloud-grpc.ts @@ -111,6 +111,7 @@ function fillGrpcFunction(stub: Stub, serviceIdentifier: string, methodName: str const scope = getCurrentScope(); const transaction = scope.getTransaction(); if (transaction) { + // eslint-disable-next-line deprecation/deprecation span = transaction.startChild({ description: `${callType} ${methodName}`, op: `grpc.${serviceIdentifier}`, diff --git a/packages/serverless/src/google-cloud-http.ts b/packages/serverless/src/google-cloud-http.ts index 6b522facf5ae..813d65add0e5 100644 --- a/packages/serverless/src/google-cloud-http.ts +++ b/packages/serverless/src/google-cloud-http.ts @@ -56,6 +56,7 @@ function wrapRequestFunction(orig: RequestFunction): RequestFunction { const transaction = scope.getTransaction(); if (transaction) { const httpMethod = reqOpts.method || 'GET'; + // eslint-disable-next-line deprecation/deprecation span = transaction.startChild({ description: `${httpMethod} ${reqOpts.uri}`, op: `http.client.${identifyService(this.apiEndpoint)}`, diff --git a/packages/svelte/src/performance.ts b/packages/svelte/src/performance.ts index e579b453f033..e3ba158e6f59 100644 --- a/packages/svelte/src/performance.ts +++ b/packages/svelte/src/performance.ts @@ -47,6 +47,7 @@ export function trackComponent(options?: TrackComponentOptions): void { } function recordInitSpan(transaction: Transaction, componentName: string): Span { + // eslint-disable-next-line deprecation/deprecation const initSpan = transaction.startChild({ op: UI_SVELTE_INIT, description: componentName, @@ -75,6 +76,7 @@ function recordUpdateSpans(componentName: string, initSpan?: Span): void { const parentSpan = initSpan && !initSpan.endTimestamp && initSpan.transaction === transaction ? initSpan : transaction; + // eslint-disable-next-line deprecation/deprecation updateSpan = parentSpan.startChild({ op: UI_SVELTE_UPDATE, description: componentName, diff --git a/packages/sveltekit/src/client/router.ts b/packages/sveltekit/src/client/router.ts index 0d327335326b..2250943909ab 100644 --- a/packages/sveltekit/src/client/router.ts +++ b/packages/sveltekit/src/client/router.ts @@ -117,6 +117,7 @@ function instrumentNavigations(startTransactionFn: (context: TransactionContext) // If a routing span is still open from a previous navigation, we finish it. routingSpan.end(); } + // eslint-disable-next-line deprecation/deprecation routingSpan = activeTransaction.startChild({ op: 'ui.sveltekit.routing', description: 'SvelteKit Route Change', diff --git a/packages/sveltekit/test/client/router.test.ts b/packages/sveltekit/test/client/router.test.ts index 648e5344e6c8..cfb8ceb14275 100644 --- a/packages/sveltekit/test/client/router.test.ts +++ b/packages/sveltekit/test/client/router.test.ts @@ -117,6 +117,7 @@ describe('sveltekitRoutingInstrumentation', () => { }, }); + // eslint-disable-next-line deprecation/deprecation expect(returnedTransaction?.startChild).toHaveBeenCalledWith({ op: 'ui.sveltekit.routing', origin: 'auto.ui.sveltekit', @@ -168,6 +169,7 @@ describe('sveltekitRoutingInstrumentation', () => { }, }); + // eslint-disable-next-line deprecation/deprecation expect(returnedTransaction?.startChild).toHaveBeenCalledWith({ op: 'ui.sveltekit.routing', origin: 'auto.ui.sveltekit', diff --git a/packages/tracing-internal/src/browser/metrics/index.ts b/packages/tracing-internal/src/browser/metrics/index.ts index 651246dfb688..beb98783d8fd 100644 --- a/packages/tracing-internal/src/browser/metrics/index.ts +++ b/packages/tracing-internal/src/browser/metrics/index.ts @@ -76,6 +76,7 @@ export function startTrackingLongTasks(): void { const startTime = msToSec((browserPerformanceTimeOrigin as number) + entry.startTime); const duration = msToSec(entry.duration); + // eslint-disable-next-line deprecation/deprecation transaction.startChild({ description: 'Main UI thread blocked', op: 'ui.long-task', @@ -115,6 +116,7 @@ export function startTrackingInteractions(): void { span.data = { 'ui.component_name': componentName }; } + // eslint-disable-next-line deprecation/deprecation transaction.startChild(span); } } diff --git a/packages/tracing-internal/src/browser/metrics/utils.ts b/packages/tracing-internal/src/browser/metrics/utils.ts index 80bf01b9c333..cebabd9abf1c 100644 --- a/packages/tracing-internal/src/browser/metrics/utils.ts +++ b/packages/tracing-internal/src/browser/metrics/utils.ts @@ -18,6 +18,7 @@ export function _startChild(transaction: Transaction, { startTimestamp, ...ctx } transaction.startTimestamp = startTimestamp; } + // eslint-disable-next-line deprecation/deprecation return transaction.startChild({ startTimestamp, ...ctx, diff --git a/packages/tracing-internal/src/browser/request.ts b/packages/tracing-internal/src/browser/request.ts index b85d54c96622..3adbda65cbb4 100644 --- a/packages/tracing-internal/src/browser/request.ts +++ b/packages/tracing-internal/src/browser/request.ts @@ -275,7 +275,8 @@ export function xhrCallback( const span = shouldCreateSpanResult && parentSpan - ? parentSpan.startChild({ + ? // eslint-disable-next-line deprecation/deprecation + parentSpan.startChild({ data: { type: 'xhr', 'http.method': sentryXhrData.method, diff --git a/packages/tracing-internal/src/common/fetch.ts b/packages/tracing-internal/src/common/fetch.ts index dfc81bff05f7..bb7ff90ea21c 100644 --- a/packages/tracing-internal/src/common/fetch.ts +++ b/packages/tracing-internal/src/common/fetch.ts @@ -79,7 +79,8 @@ export function instrumentFetchRequest( const span = shouldCreateSpanResult && parentSpan - ? parentSpan.startChild({ + ? // eslint-disable-next-line deprecation/deprecation + parentSpan.startChild({ data: { url, type: 'fetch', diff --git a/packages/tracing-internal/src/node/integrations/apollo.ts b/packages/tracing-internal/src/node/integrations/apollo.ts index f46de137680a..dec97b0df729 100644 --- a/packages/tracing-internal/src/node/integrations/apollo.ts +++ b/packages/tracing-internal/src/node/integrations/apollo.ts @@ -190,6 +190,7 @@ function wrapResolver( return function (this: unknown, ...args: unknown[]) { const scope = getCurrentHub().getScope(); const parentSpan = scope.getSpan(); + // eslint-disable-next-line deprecation/deprecation const span = parentSpan?.startChild({ description: `${resolverGroupName}.${resolverName}`, op: 'graphql.resolve', diff --git a/packages/tracing-internal/src/node/integrations/express.ts b/packages/tracing-internal/src/node/integrations/express.ts index 7754fff3ad5b..0c7f938b56fb 100644 --- a/packages/tracing-internal/src/node/integrations/express.ts +++ b/packages/tracing-internal/src/node/integrations/express.ts @@ -157,6 +157,7 @@ function wrap(fn: Function, method: Method): (...args: any[]) => void { return function (this: NodeJS.Global, req: unknown, res: ExpressResponse & SentryTracingResponse): void { const transaction = res.__sentry_transaction; if (transaction) { + // eslint-disable-next-line deprecation/deprecation const span = transaction.startChild({ description: fn.name, op: `middleware.express.${method}`, @@ -177,6 +178,7 @@ function wrap(fn: Function, method: Method): (...args: any[]) => void { next: () => void, ): void { const transaction = res.__sentry_transaction; + // eslint-disable-next-line deprecation/deprecation const span = transaction?.startChild({ description: fn.name, op: `middleware.express.${method}`, @@ -197,6 +199,7 @@ function wrap(fn: Function, method: Method): (...args: any[]) => void { next: () => void, ): void { const transaction = res.__sentry_transaction; + // eslint-disable-next-line deprecation/deprecation const span = transaction?.startChild({ description: fn.name, op: `middleware.express.${method}`, diff --git a/packages/tracing-internal/src/node/integrations/graphql.ts b/packages/tracing-internal/src/node/integrations/graphql.ts index 16773daf49b6..bea4370340ee 100644 --- a/packages/tracing-internal/src/node/integrations/graphql.ts +++ b/packages/tracing-internal/src/node/integrations/graphql.ts @@ -54,6 +54,7 @@ export class GraphQL implements LazyLoadedIntegration { const scope = getCurrentHub().getScope(); const parentSpan = scope.getSpan(); + // eslint-disable-next-line deprecation/deprecation const span = parentSpan?.startChild({ description: 'execute', op: 'graphql.execute', diff --git a/packages/tracing-internal/src/node/integrations/mongo.ts b/packages/tracing-internal/src/node/integrations/mongo.ts index 966231db2f74..9451e12b50da 100644 --- a/packages/tracing-internal/src/node/integrations/mongo.ts +++ b/packages/tracing-internal/src/node/integrations/mongo.ts @@ -180,6 +180,7 @@ export class Mongo implements LazyLoadedIntegration { // Check if the operation was passed a callback. (mapReduce requires a different check, as // its (non-callback) arguments can also be functions.) if (typeof lastArg !== 'function' || (operation === 'mapReduce' && args.length === 2)) { + // eslint-disable-next-line deprecation/deprecation const span = parentSpan?.startChild(getSpanContext(this, operation, args)); const maybePromiseOrCursor = orig.call(this, ...args); @@ -211,6 +212,7 @@ export class Mongo implements LazyLoadedIntegration { } } + // eslint-disable-next-line deprecation/deprecation const span = parentSpan?.startChild(getSpanContext(this, operation, args.slice(0, -1))); return orig.call(this, ...args.slice(0, -1), function (err: Error, result: unknown) { diff --git a/packages/tracing-internal/src/node/integrations/mysql.ts b/packages/tracing-internal/src/node/integrations/mysql.ts index c85b0021d89a..d1283a47815c 100644 --- a/packages/tracing-internal/src/node/integrations/mysql.ts +++ b/packages/tracing-internal/src/node/integrations/mysql.ts @@ -106,6 +106,7 @@ export class Mysql implements LazyLoadedIntegration { const scope = getCurrentHub().getScope(); const parentSpan = scope.getSpan(); + // eslint-disable-next-line deprecation/deprecation const span = parentSpan?.startChild({ description: typeof options === 'string' ? options : (options as { sql: string }).sql, op: 'db', diff --git a/packages/tracing-internal/src/node/integrations/postgres.ts b/packages/tracing-internal/src/node/integrations/postgres.ts index 810f07825653..3a5ee78641dc 100644 --- a/packages/tracing-internal/src/node/integrations/postgres.ts +++ b/packages/tracing-internal/src/node/integrations/postgres.ts @@ -128,6 +128,7 @@ export class Postgres implements LazyLoadedIntegration { // ignore } + // eslint-disable-next-line deprecation/deprecation const span = parentSpan?.startChild({ description: typeof config === 'string' ? config : (config as { text: string }).text, op: 'db', diff --git a/packages/tracing-internal/test/browser/metrics/index.test.ts b/packages/tracing-internal/test/browser/metrics/index.test.ts index 20444c7bf4c8..cfb0500fb68a 100644 --- a/packages/tracing-internal/test/browser/metrics/index.test.ts +++ b/packages/tracing-internal/test/browser/metrics/index.test.ts @@ -5,6 +5,7 @@ import { _addMeasureSpans, _addResourceSpans } from '../../../src/browser/metric describe('_addMeasureSpans', () => { const transaction = new Transaction({ op: 'pageload', name: '/' }); beforeEach(() => { + // eslint-disable-next-line deprecation/deprecation transaction.startChild = jest.fn(); }); @@ -21,12 +22,12 @@ describe('_addMeasureSpans', () => { const startTime = 23; const duration = 356; - // eslint-disable-next-line @typescript-eslint/unbound-method + // eslint-disable-next-line @typescript-eslint/unbound-method, deprecation/deprecation expect(transaction.startChild).toHaveBeenCalledTimes(0); _addMeasureSpans(transaction, entry, startTime, duration, timeOrigin); - // eslint-disable-next-line @typescript-eslint/unbound-method + // eslint-disable-next-line @typescript-eslint/unbound-method, deprecation/deprecation expect(transaction.startChild).toHaveBeenCalledTimes(1); - // eslint-disable-next-line @typescript-eslint/unbound-method + // eslint-disable-next-line @typescript-eslint/unbound-method, deprecation/deprecation expect(transaction.startChild).toHaveBeenLastCalledWith({ description: 'measure-1', startTimestamp: timeOrigin + startTime, @@ -40,6 +41,7 @@ describe('_addMeasureSpans', () => { describe('_addResourceSpans', () => { const transaction = new Transaction({ op: 'pageload', name: '/' }); beforeEach(() => { + // eslint-disable-next-line deprecation/deprecation transaction.startChild = jest.fn(); }); @@ -54,7 +56,7 @@ describe('_addResourceSpans', () => { }; _addResourceSpans(transaction, entry, '/assets/to/me', 123, 456, 100); - // eslint-disable-next-line @typescript-eslint/unbound-method + // eslint-disable-next-line @typescript-eslint/unbound-method, deprecation/deprecation expect(transaction.startChild).toHaveBeenCalledTimes(0); }); @@ -68,7 +70,7 @@ describe('_addResourceSpans', () => { }; _addResourceSpans(transaction, entry, '/assets/to/me', 123, 456, 100); - // eslint-disable-next-line @typescript-eslint/unbound-method + // eslint-disable-next-line @typescript-eslint/unbound-method, deprecation/deprecation expect(transaction.startChild).toHaveBeenCalledTimes(0); }); @@ -87,9 +89,9 @@ describe('_addResourceSpans', () => { _addResourceSpans(transaction, entry, '/assets/to/css', startTime, duration, timeOrigin); - // eslint-disable-next-line @typescript-eslint/unbound-method + // eslint-disable-next-line @typescript-eslint/unbound-method, deprecation/deprecation expect(transaction.startChild).toHaveBeenCalledTimes(1); - // eslint-disable-next-line @typescript-eslint/unbound-method + // eslint-disable-next-line @typescript-eslint/unbound-method, deprecation/deprecation expect(transaction.startChild).toHaveBeenLastCalledWith({ data: { ['http.decoded_response_content_length']: entry.decodedBodySize, @@ -135,7 +137,7 @@ describe('_addResourceSpans', () => { }; _addResourceSpans(transaction, entry, '/assets/to/me', 123, 234, 465); - // eslint-disable-next-line @typescript-eslint/unbound-method + // eslint-disable-next-line @typescript-eslint/unbound-method, deprecation/deprecation expect(transaction.startChild).toHaveBeenLastCalledWith( expect.objectContaining({ op, @@ -155,9 +157,9 @@ describe('_addResourceSpans', () => { _addResourceSpans(transaction, entry, '/assets/to/css', 100, 23, 345); - // eslint-disable-next-line @typescript-eslint/unbound-method + // eslint-disable-next-line @typescript-eslint/unbound-method, deprecation/deprecation expect(transaction.startChild).toHaveBeenCalledTimes(1); - // eslint-disable-next-line @typescript-eslint/unbound-method + // eslint-disable-next-line @typescript-eslint/unbound-method, deprecation/deprecation expect(transaction.startChild).toHaveBeenLastCalledWith( expect.objectContaining({ data: { @@ -180,9 +182,9 @@ describe('_addResourceSpans', () => { _addResourceSpans(transaction, entry, '/assets/to/css', 100, 23, 345); - // eslint-disable-next-line @typescript-eslint/unbound-method + // eslint-disable-next-line @typescript-eslint/unbound-method, deprecation/deprecation expect(transaction.startChild).toHaveBeenCalledTimes(1); - // eslint-disable-next-line @typescript-eslint/unbound-method + // eslint-disable-next-line @typescript-eslint/unbound-method, deprecation/deprecation expect(transaction.startChild).toHaveBeenLastCalledWith( expect.objectContaining({ data: {}, @@ -202,9 +204,9 @@ describe('_addResourceSpans', () => { _addResourceSpans(transaction, entry, '/assets/to/css', 100, 23, 345); - // eslint-disable-next-line @typescript-eslint/unbound-method + // eslint-disable-next-line @typescript-eslint/unbound-method, deprecation/deprecation expect(transaction.startChild).toHaveBeenCalledTimes(1); - // eslint-disable-next-line @typescript-eslint/unbound-method + // eslint-disable-next-line @typescript-eslint/unbound-method, deprecation/deprecation expect(transaction.startChild).toHaveBeenLastCalledWith( expect.objectContaining({ data: {}, diff --git a/packages/tracing/test/idletransaction.test.ts b/packages/tracing/test/idletransaction.test.ts index e0c5dd189cff..9d82eb474af3 100644 --- a/packages/tracing/test/idletransaction.test.ts +++ b/packages/tracing/test/idletransaction.test.ts @@ -1,7 +1,14 @@ import { BrowserClient } from '@sentry/browser'; -import { TRACING_DEFAULTS, Transaction } from '@sentry/core'; - -import { Hub, IdleTransaction, Span } from '../../core/src'; +import { + TRACING_DEFAULTS, + Transaction, + getCurrentScope, + startInactiveSpan, + startSpan, + startSpanManual, +} from '@sentry/core'; + +import { Hub, IdleTransaction, Span, makeMain } from '../../core/src'; import { IdleTransactionSpanRecorder } from '../../core/src/tracing/idletransaction'; import { getDefaultBrowserClientOptions } from './testutils'; @@ -10,6 +17,7 @@ let hub: Hub; beforeEach(() => { const options = getDefaultBrowserClientOptions({ dsn, tracesSampleRate: 1 }); hub = new Hub(new BrowserClient(options)); + makeMain(hub); }); describe('IdleTransaction', () => { @@ -104,8 +112,9 @@ describe('IdleTransaction', () => { const mockFinish = jest.spyOn(transaction, 'end'); transaction.initSpanRecorder(10); expect(transaction.activities).toMatchObject({}); + getCurrentScope().setSpan(transaction); - const span = transaction.startChild(); + const span = startInactiveSpan({ name: 'inner' })!; expect(transaction.activities).toMatchObject({ [span.spanId]: true }); expect(mockFinish).toHaveBeenCalledTimes(0); @@ -121,8 +130,9 @@ describe('IdleTransaction', () => { const transaction = new IdleTransaction({ name: 'foo' }, hub); transaction.initSpanRecorder(10); expect(transaction.activities).toMatchObject({}); + getCurrentScope().setSpan(transaction); - transaction.startChild({ startTimestamp: 1234, endTimestamp: 5678 }); + startInactiveSpan({ name: 'inner', startTimestamp: 1234, endTimestamp: 5678 }); expect(transaction.activities).toMatchObject({}); }); @@ -131,16 +141,17 @@ describe('IdleTransaction', () => { const mockFinish = jest.spyOn(transaction, 'end'); transaction.initSpanRecorder(10); expect(transaction.activities).toMatchObject({}); + getCurrentScope().setSpan(transaction); - const span = transaction.startChild(); - const childSpan = span.startChild(); - - expect(transaction.activities).toMatchObject({ [span.spanId]: true, [childSpan.spanId]: true }); - span.end(); - jest.advanceTimersByTime(TRACING_DEFAULTS.idleTimeout + 1); + startSpanManual({ name: 'inner1' }, span => { + const childSpan = startInactiveSpan({ name: 'inner2' })!; + expect(transaction.activities).toMatchObject({ [span!.spanId]: true, [childSpan.spanId]: true }); + span?.end(); + jest.advanceTimersByTime(TRACING_DEFAULTS.idleTimeout + 1); - expect(mockFinish).toHaveBeenCalledTimes(0); - expect(transaction.activities).toMatchObject({ [childSpan.spanId]: true }); + expect(mockFinish).toHaveBeenCalledTimes(0); + expect(transaction.activities).toMatchObject({ [childSpan.spanId]: true }); + }); }); it('calls beforeFinish callback before finishing', () => { @@ -150,12 +161,12 @@ describe('IdleTransaction', () => { transaction.initSpanRecorder(10); transaction.registerBeforeFinishCallback(mockCallback1); transaction.registerBeforeFinishCallback(mockCallback2); + getCurrentScope().setSpan(transaction); expect(mockCallback1).toHaveBeenCalledTimes(0); expect(mockCallback2).toHaveBeenCalledTimes(0); - const span = transaction.startChild(); - span.end(); + startSpan({ name: 'inner' }, () => {}); jest.runOnlyPendingTimers(); expect(mockCallback1).toHaveBeenCalledTimes(1); @@ -167,15 +178,16 @@ describe('IdleTransaction', () => { it('filters spans on finish', () => { const transaction = new IdleTransaction({ name: 'foo', startTimestamp: 1234 }, hub); transaction.initSpanRecorder(10); + getCurrentScope().setSpan(transaction); // regular child - should be kept - const regularSpan = transaction.startChild({ startTimestamp: transaction.startTimestamp + 2 }); + const regularSpan = startInactiveSpan({ name: 'span1', startTimestamp: transaction.startTimestamp + 2 })!; // discardedSpan - startTimestamp is too large - transaction.startChild({ startTimestamp: 645345234 }); + startInactiveSpan({ name: 'span2', startTimestamp: 645345234 }); // Should be cancelled - will not finish - const cancelledSpan = transaction.startChild({ startTimestamp: transaction.startTimestamp + 4 }); + const cancelledSpan = startInactiveSpan({ name: 'span3', startTimestamp: transaction.startTimestamp + 4 })!; regularSpan.end(regularSpan.startTimestamp + 4); transaction.end(transaction.startTimestamp + 10); @@ -200,8 +212,9 @@ describe('IdleTransaction', () => { it('filters out spans that exceed final timeout', () => { const transaction = new IdleTransaction({ name: 'foo', startTimestamp: 1234 }, hub, 1000, 3000); transaction.initSpanRecorder(10); + getCurrentScope().setSpan(transaction); - const span = transaction.startChild({ startTimestamp: transaction.startTimestamp + 2 }); + const span = startInactiveSpan({ name: 'span', startTimestamp: transaction.startTimestamp + 2 })!; span.end(span.startTimestamp + 10 + 30 + 1); transaction.end(transaction.startTimestamp + 50); @@ -235,7 +248,9 @@ describe('IdleTransaction', () => { it('does not finish if a activity is started', () => { const transaction = new IdleTransaction({ name: 'foo', startTimestamp: 1234 }, hub); transaction.initSpanRecorder(10); - transaction.startChild({}); + getCurrentScope().setSpan(transaction); + + startInactiveSpan({ name: 'span' }); jest.advanceTimersByTime(TRACING_DEFAULTS.idleTimeout); expect(transaction.endTimestamp).toBeUndefined(); @@ -245,14 +260,13 @@ describe('IdleTransaction', () => { const idleTimeout = 10; const transaction = new IdleTransaction({ name: 'foo', startTimestamp: 1234 }, hub, idleTimeout); transaction.initSpanRecorder(10); + getCurrentScope().setSpan(transaction); - const span = transaction.startChild({}); - span.end(); + startSpan({ name: 'span1' }, () => {}); jest.advanceTimersByTime(2); - const span2 = transaction.startChild({}); - span2.end(); + startSpan({ name: 'span2' }, () => {}); jest.advanceTimersByTime(8); @@ -263,14 +277,13 @@ describe('IdleTransaction', () => { const idleTimeout = 10; const transaction = new IdleTransaction({ name: 'foo', startTimestamp: 1234 }, hub, idleTimeout); transaction.initSpanRecorder(10); + getCurrentScope().setSpan(transaction); - const span = transaction.startChild({}); - span.end(); + startSpan({ name: 'span1' }, () => {}); jest.advanceTimersByTime(2); - const span2 = transaction.startChild({}); - span2.end(); + startSpan({ name: 'span2' }, () => {}); jest.advanceTimersByTime(10); @@ -283,10 +296,11 @@ describe('IdleTransaction', () => { const idleTimeout = 10; const transaction = new IdleTransaction({ name: 'foo', startTimestamp: 1234 }, hub, idleTimeout); transaction.initSpanRecorder(10); + getCurrentScope().setSpan(transaction); - const firstSpan = transaction.startChild({}); + const firstSpan = startInactiveSpan({ name: 'span1' })!; transaction.cancelIdleTimeout(undefined, { restartOnChildSpanChange: false }); - const secondSpan = transaction.startChild({}); + const secondSpan = startInactiveSpan({ name: 'span2' })!; firstSpan.end(); secondSpan.end(); @@ -297,11 +311,12 @@ describe('IdleTransaction', () => { const idleTimeout = 10; const transaction = new IdleTransaction({ name: 'foo', startTimestamp: 1234 }, hub, idleTimeout); transaction.initSpanRecorder(10); + getCurrentScope().setSpan(transaction); - const firstSpan = transaction.startChild({}); + const firstSpan = startInactiveSpan({ name: 'span1' })!; transaction.cancelIdleTimeout(undefined, { restartOnChildSpanChange: false }); - const secondSpan = transaction.startChild({}); - const thirdSpan = transaction.startChild({}); + const secondSpan = startInactiveSpan({ name: 'span2' })!; + const thirdSpan = startInactiveSpan({ name: 'span3' })!; firstSpan.end(); expect(transaction.endTimestamp).toBeUndefined(); @@ -317,9 +332,9 @@ describe('IdleTransaction', () => { const idleTimeout = 10; const transaction = new IdleTransaction({ name: 'foo', startTimestamp: 1234 }, hub, idleTimeout); transaction.initSpanRecorder(10); + getCurrentScope().setSpan(transaction); - const span = transaction.startChild({}); - span.end(); + startSpan({ name: 'span' }, () => {}); jest.advanceTimersByTime(2); @@ -332,16 +347,15 @@ describe('IdleTransaction', () => { const idleTimeout = 10; const transaction = new IdleTransaction({ name: 'foo', startTimestamp: 1234 }, hub, idleTimeout); transaction.initSpanRecorder(10); + getCurrentScope().setSpan(transaction); - const span = transaction.startChild({}); - span.end(); + startSpan({ name: 'span' }, () => {}); jest.advanceTimersByTime(2); transaction.cancelIdleTimeout(); - const span2 = transaction.startChild({}); - span2.end(); + startSpan({ name: 'span' }, () => {}); jest.advanceTimersByTime(8); expect(transaction.endTimestamp).toBeUndefined(); @@ -380,9 +394,10 @@ describe('IdleTransaction', () => { const transaction = new IdleTransaction({ name: 'foo' }, hub, TRACING_DEFAULTS.idleTimeout); const mockFinish = jest.spyOn(transaction, 'end'); transaction.initSpanRecorder(10); + getCurrentScope().setSpan(transaction); expect(mockFinish).toHaveBeenCalledTimes(0); - transaction.startChild({}); + startInactiveSpan({ name: 'span' }); // Beat 1 jest.advanceTimersByTime(TRACING_DEFAULTS.heartbeatInterval); @@ -401,15 +416,16 @@ describe('IdleTransaction', () => { const transaction = new IdleTransaction({ name: 'foo' }, hub, TRACING_DEFAULTS.idleTimeout, 50000); const mockFinish = jest.spyOn(transaction, 'end'); transaction.initSpanRecorder(10); + getCurrentScope().setSpan(transaction); expect(mockFinish).toHaveBeenCalledTimes(0); - transaction.startChild({}); + startInactiveSpan({ name: 'span' }); // Beat 1 jest.advanceTimersByTime(TRACING_DEFAULTS.heartbeatInterval); expect(mockFinish).toHaveBeenCalledTimes(0); - const span = transaction.startChild(); // push activity + const span = startInactiveSpan({ name: 'span' })!; // push activity // Beat 1 jest.advanceTimersByTime(TRACING_DEFAULTS.heartbeatInterval); @@ -419,8 +435,8 @@ describe('IdleTransaction', () => { jest.advanceTimersByTime(TRACING_DEFAULTS.heartbeatInterval); expect(mockFinish).toHaveBeenCalledTimes(0); - transaction.startChild(); // push activity - transaction.startChild(); // push activity + startInactiveSpan({ name: 'span' }); // push activity + startInactiveSpan({ name: 'span' }); // push activity // Beat 1 jest.advanceTimersByTime(TRACING_DEFAULTS.heartbeatInterval); diff --git a/packages/types/src/span.ts b/packages/types/src/span.ts index af51afd0e5e9..86acb7d82b3a 100644 --- a/packages/types/src/span.ts +++ b/packages/types/src/span.ts @@ -239,6 +239,8 @@ export interface Span extends SpanContext { /** * Creates a new `Span` while setting the current `Span.id` as `parentSpanId`. * Also the `sampled` decision will be inherited. + * + * @deprecated Use `startSpan()`, `startSpanManual()` or `startInactiveSpan()` instead. */ startChild(spanContext?: Pick>): Span; diff --git a/packages/vue/src/tracing.ts b/packages/vue/src/tracing.ts index af93701e74a7..2fede1c740c8 100644 --- a/packages/vue/src/tracing.ts +++ b/packages/vue/src/tracing.ts @@ -77,6 +77,7 @@ export const createTracingMixins = (options: TracingOptions): Mixins => { if (activeTransaction) { this.$_sentryRootSpan = this.$_sentryRootSpan || + // eslint-disable-next-line deprecation/deprecation activeTransaction.startChild({ description: 'Application Render', op: `${VUE_OP}.render`, @@ -111,6 +112,7 @@ export const createTracingMixins = (options: TracingOptions): Mixins => { oldSpan.end(); } + // eslint-disable-next-line deprecation/deprecation this.$_sentrySpans[operation] = activeTransaction.startChild({ description: `Vue <${name}>`, op: `${VUE_OP}.${operation}`, From 2cfb0ef3fa5c40f90c317267a4d10b969994d021 Mon Sep 17 00:00:00 2001 From: Luca Forstner Date: Mon, 8 Jan 2024 12:07:59 +0100 Subject: [PATCH 18/43] feat(core): Deprecate hub capture APIs and add them to `Scope` (#10039) Co-authored-by: Francesco Novy --- packages/core/src/exports.ts | 33 ++- packages/core/src/hub.ts | 46 ++-- packages/core/src/scope.ts | 86 +++++- packages/core/src/tracing/transaction.ts | 1 + .../test/lib/integrations/metadata.test.ts | 1 + packages/core/test/lib/scope.test.ts | 254 +++++++++++++++++- .../deno/test/__snapshots__/mod.test.ts.snap | 8 +- packages/deno/test/mod.test.ts | 8 +- packages/node-experimental/src/sdk/types.ts | 11 - .../test/integration/breadcrumbs.test.ts | 6 + packages/node/test/handlers.test.ts | 1 + packages/node/test/index.test.ts | 1 + .../test/spanprocessor.test.ts | 1 + packages/opentelemetry/src/custom/scope.ts | 1 + .../test/integration/breadcrumbs.test.ts | 7 + .../test/utils/setupEventContextTrace.test.ts | 2 + packages/types/src/scope.ts | 29 ++ packages/vue/src/errorhandler.ts | 4 +- 18 files changed, 434 insertions(+), 66 deletions(-) diff --git a/packages/core/src/exports.ts b/packages/core/src/exports.ts index bc37a633c16e..04b953acfd7e 100644 --- a/packages/core/src/exports.ts +++ b/packages/core/src/exports.ts @@ -30,52 +30,51 @@ import { closeSession, makeSession, updateSession } from './session'; import type { ExclusiveEventHintOrCaptureContext } from './utils/prepareEvent'; import { parseEventHintOrCaptureContext } from './utils/prepareEvent'; -// Note: All functions in this file are typed with a return value of `ReturnType`, -// where HUB_FUNCTION is some method on the Hub class. -// -// This is done to make sure the top level SDK methods stay in sync with the hub methods. -// Although every method here has an explicit return type, some of them (that map to void returns) do not -// contain `return` keywords. This is done to save on bundle size, as `return` is not minifiable. - /** * Captures an exception event and sends it to Sentry. - * This accepts an event hint as optional second parameter. - * Alternatively, you can also pass a CaptureContext directly as second parameter. + * + * @param exception The exception to capture. + * @param hint Optinal additional data to attach to the Sentry event. + * @returns the id of the captured Sentry event. */ export function captureException( // eslint-disable-next-line @typescript-eslint/no-explicit-any exception: any, hint?: ExclusiveEventHintOrCaptureContext, -): ReturnType { +): string { + // eslint-disable-next-line deprecation/deprecation return getCurrentHub().captureException(exception, parseEventHintOrCaptureContext(hint)); } /** * Captures a message event and sends it to Sentry. * - * @param message The message to send to Sentry. - * @param Severity Define the level of the message. - * @returns The generated eventId. + * @param exception The exception to capture. + * @param captureContext Define the level of the message or pass in additional data to attach to the message. + * @returns the id of the captured message. */ export function captureMessage( message: string, // eslint-disable-next-line deprecation/deprecation captureContext?: CaptureContext | Severity | SeverityLevel, -): ReturnType { +): string { // This is necessary to provide explicit scopes upgrade, without changing the original // arity of the `captureMessage(message, level)` method. const level = typeof captureContext === 'string' ? captureContext : undefined; const context = typeof captureContext !== 'string' ? { captureContext } : undefined; + // eslint-disable-next-line deprecation/deprecation return getCurrentHub().captureMessage(message, level, context); } /** * Captures a manually created event and sends it to Sentry. * - * @param event The event to send to Sentry. - * @returns The generated eventId. + * @param exception The event to send to Sentry. + * @param hint Optional additional data to attach to the Sentry event. + * @returns the id of the captured event. */ -export function captureEvent(event: Event, hint?: EventHint): ReturnType { +export function captureEvent(event: Event, hint?: EventHint): string { + // eslint-disable-next-line deprecation/deprecation return getCurrentHub().captureEvent(event, hint); } diff --git a/packages/core/src/hub.ts b/packages/core/src/hub.ts index 9012ab057e92..0787f7c164ed 100644 --- a/packages/core/src/hub.ts +++ b/packages/core/src/hub.ts @@ -166,6 +166,7 @@ export class Hub implements HubInterface { public bindClient(client?: Client): void { const top = this.getStackTop(); top.client = client; + top.scope.setClient(client); if (client && client.setupIntegrations) { client.setupIntegrations(); } @@ -262,27 +263,26 @@ export class Hub implements HubInterface { /** * @inheritDoc + * + * @deprecated Use `Sentry.captureException()` instead. */ public captureException(exception: unknown, hint?: EventHint): string { const eventId = (this._lastEventId = hint && hint.event_id ? hint.event_id : uuid4()); const syntheticException = new Error('Sentry syntheticException'); - this._withClient((client, scope) => { - client.captureException( - exception, - { - originalException: exception, - syntheticException, - ...hint, - event_id: eventId, - }, - scope, - ); + this.getScope().captureException(exception, { + originalException: exception, + syntheticException, + ...hint, + event_id: eventId, }); + return eventId; } /** * @inheritDoc + * + * @deprecated Use `Sentry.captureMessage()` instead. */ public captureMessage( message: string, @@ -292,24 +292,20 @@ export class Hub implements HubInterface { ): string { const eventId = (this._lastEventId = hint && hint.event_id ? hint.event_id : uuid4()); const syntheticException = new Error(message); - this._withClient((client, scope) => { - client.captureMessage( - message, - level, - { - originalException: message, - syntheticException, - ...hint, - event_id: eventId, - }, - scope, - ); + this.getScope().captureMessage(message, level, { + originalException: message, + syntheticException, + ...hint, + event_id: eventId, }); + return eventId; } /** * @inheritDoc + * + * @deprecated Use `Sentry.captureEvent()` instead. */ public captureEvent(event: Event, hint?: EventHint): string { const eventId = hint && hint.event_id ? hint.event_id : uuid4(); @@ -317,9 +313,7 @@ export class Hub implements HubInterface { this._lastEventId = eventId; } - this._withClient((client, scope) => { - client.captureEvent(event, { ...hint, event_id: eventId }, scope); - }); + this.getScope().captureEvent(event, { ...hint, event_id: eventId }); return eventId; } diff --git a/packages/core/src/scope.ts b/packages/core/src/scope.ts index cff498bc85a3..8857329de3dd 100644 --- a/packages/core/src/scope.ts +++ b/packages/core/src/scope.ts @@ -24,7 +24,7 @@ import type { Transaction, User, } from '@sentry/types'; -import { dateTimestampInSeconds, isPlainObject, uuid4 } from '@sentry/utils'; +import { dateTimestampInSeconds, isPlainObject, logger, uuid4 } from '@sentry/utils'; import { getGlobalEventProcessors, notifyEventProcessors } from './eventProcessors'; import { updateSession } from './session'; @@ -581,6 +581,90 @@ export class Scope implements ScopeInterface { return this._propagationContext; } + /** + * Capture an exception for this scope. + * + * @param exception The exception to capture. + * @param hint Optinal additional data to attach to the Sentry event. + * @returns the id of the captured Sentry event. + */ + public captureException(exception: unknown, hint?: EventHint): string { + const eventId = hint && hint.event_id ? hint.event_id : uuid4(); + + if (!this._client) { + logger.warn('No client configured on scope - will not capture exception!'); + return eventId; + } + + const syntheticException = new Error('Sentry syntheticException'); + + this._client.captureException( + exception, + { + originalException: exception, + syntheticException, + ...hint, + event_id: eventId, + }, + this, + ); + + return eventId; + } + + /** + * Capture a message for this scope. + * + * @param message The message to capture. + * @param level An optional severity level to report the message with. + * @param hint Optional additional data to attach to the Sentry event. + * @returns the id of the captured message. + */ + public captureMessage(message: string, level?: SeverityLevel, hint?: EventHint): string { + const eventId = hint && hint.event_id ? hint.event_id : uuid4(); + + if (!this._client) { + logger.warn('No client configured on scope - will not capture message!'); + return eventId; + } + + const syntheticException = new Error(message); + + this._client.captureMessage( + message, + level, + { + originalException: message, + syntheticException, + ...hint, + event_id: eventId, + }, + this, + ); + + return eventId; + } + + /** + * Captures a manually created event for this scope and sends it to Sentry. + * + * @param exception The event to capture. + * @param hint Optional additional data to attach to the Sentry event. + * @returns the id of the captured event. + */ + public captureEvent(event: Event, hint?: EventHint): string { + const eventId = hint && hint.event_id ? hint.event_id : uuid4(); + + if (!this._client) { + logger.warn('No client configured on scope - will not capture event!'); + return eventId; + } + + this._client.captureEvent(event, { ...hint, event_id: eventId }, this); + + return eventId; + } + /** * This will be called on every set call. */ diff --git a/packages/core/src/tracing/transaction.ts b/packages/core/src/tracing/transaction.ts index e0635c7e6b80..60ac3eae074b 100644 --- a/packages/core/src/tracing/transaction.ts +++ b/packages/core/src/tracing/transaction.ts @@ -153,6 +153,7 @@ export class Transaction extends SpanClass implements TransactionInterface { if (!transaction) { return undefined; } + // eslint-disable-next-line deprecation/deprecation return this._hub.captureEvent(transaction); } diff --git a/packages/core/test/lib/integrations/metadata.test.ts b/packages/core/test/lib/integrations/metadata.test.ts index 15678a66fdb6..7e8bfcea9fa4 100644 --- a/packages/core/test/lib/integrations/metadata.test.ts +++ b/packages/core/test/lib/integrations/metadata.test.ts @@ -61,6 +61,7 @@ describe('ModuleMetadata integration', () => { const client = new TestClient(options); const hub = getCurrentHub(); hub.bindClient(client); + // eslint-disable-next-line deprecation/deprecation hub.captureException(new Error('Some error')); }); }); diff --git a/packages/core/test/lib/scope.test.ts b/packages/core/test/lib/scope.test.ts index 3122f3b3e3e5..87af429d0859 100644 --- a/packages/core/test/lib/scope.test.ts +++ b/packages/core/test/lib/scope.test.ts @@ -1,4 +1,4 @@ -import type { Attachment, Breadcrumb, Client } from '@sentry/types'; +import type { Attachment, Breadcrumb, Client, Event } from '@sentry/types'; import { applyScopeDataToEvent } from '../../src'; import { Scope, getGlobalScope, setGlobalScope } from '../../src/scope'; @@ -212,4 +212,256 @@ describe('Scope', () => { expect(clonedScope.getClient()).toBe(fakeClient); }); }); + + describe('.captureException()', () => { + it('should call captureException() on client with newly generated event ID if not explicitly passed in', () => { + const fakeCaptureException = jest.fn(() => 'mock-event-id'); + const fakeClient = { + captureException: fakeCaptureException, + } as unknown as Client; + const scope = new Scope(); + scope.setClient(fakeClient); + + const exception = new Error(); + + scope.captureException(exception); + + expect(fakeCaptureException).toHaveBeenCalledWith( + expect.anything(), + expect.objectContaining({ event_id: expect.any(String) }), + scope, + ); + }); + + it('should return event ID when no client is on the scope', () => { + const scope = new Scope(); + + const exception = new Error(); + + const eventId = scope.captureException(exception); + + expect(eventId).toEqual(expect.any(String)); + }); + + it('should pass exception to captureException() on client', () => { + const fakeCaptureException = jest.fn(() => 'mock-event-id'); + const fakeClient = { + captureException: fakeCaptureException, + } as unknown as Client; + const scope = new Scope(); + scope.setClient(fakeClient); + + const exception = new Error(); + + scope.captureException(exception); + + expect(fakeCaptureException).toHaveBeenCalledWith(exception, expect.anything(), scope); + }); + + it('should call captureException() on client with a synthetic exception', () => { + const fakeCaptureException = jest.fn(() => 'mock-event-id'); + const fakeClient = { + captureException: fakeCaptureException, + } as unknown as Client; + const scope = new Scope(); + scope.setClient(fakeClient); + + scope.captureException(new Error()); + + expect(fakeCaptureException).toHaveBeenCalledWith( + expect.anything(), + expect.objectContaining({ syntheticException: expect.any(Error) }), + scope, + ); + }); + + it('should pass the original exception to captureException() on client', () => { + const fakeCaptureException = jest.fn(() => 'mock-event-id'); + const fakeClient = { + captureException: fakeCaptureException, + } as unknown as Client; + const scope = new Scope(); + scope.setClient(fakeClient); + + const exception = new Error(); + scope.captureException(exception); + + expect(fakeCaptureException).toHaveBeenCalledWith( + expect.anything(), + expect.objectContaining({ originalException: exception }), + scope, + ); + }); + + it('should forward hint to captureException() on client', () => { + const fakeCaptureException = jest.fn(() => 'mock-event-id'); + const fakeClient = { + captureException: fakeCaptureException, + } as unknown as Client; + const scope = new Scope(); + scope.setClient(fakeClient); + + scope.captureException(new Error(), { event_id: 'asdf', data: { foo: 'bar' } }); + + expect(fakeCaptureException).toHaveBeenCalledWith( + expect.anything(), + expect.objectContaining({ event_id: 'asdf', data: { foo: 'bar' } }), + scope, + ); + }); + }); + + describe('.captureMessage()', () => { + it('should call captureMessage() on client with newly generated event ID if not explicitly passed in', () => { + const fakeCaptureMessage = jest.fn(() => 'mock-event-id'); + const fakeClient = { + captureMessage: fakeCaptureMessage, + } as unknown as Client; + const scope = new Scope(); + scope.setClient(fakeClient); + + scope.captureMessage('foo'); + + expect(fakeCaptureMessage).toHaveBeenCalledWith( + expect.anything(), + undefined, + expect.objectContaining({ event_id: expect.any(String) }), + scope, + ); + }); + + it('should return event ID when no client is on the scope', () => { + const scope = new Scope(); + + const eventId = scope.captureMessage('foo'); + + expect(eventId).toEqual(expect.any(String)); + }); + + it('should pass exception to captureMessage() on client', () => { + const fakeCaptureMessage = jest.fn(() => 'mock-event-id'); + const fakeClient = { + captureMessage: fakeCaptureMessage, + } as unknown as Client; + const scope = new Scope(); + scope.setClient(fakeClient); + + scope.captureMessage('bar'); + + expect(fakeCaptureMessage).toHaveBeenCalledWith('bar', undefined, expect.anything(), scope); + }); + + it('should call captureMessage() on client with a synthetic exception', () => { + const fakeCaptureMessage = jest.fn(() => 'mock-event-id'); + const fakeClient = { + captureMessage: fakeCaptureMessage, + } as unknown as Client; + const scope = new Scope(); + scope.setClient(fakeClient); + + scope.captureMessage('foo'); + + expect(fakeCaptureMessage).toHaveBeenCalledWith( + expect.anything(), + undefined, + expect.objectContaining({ syntheticException: expect.any(Error) }), + scope, + ); + }); + + it('should pass the original exception to captureMessage() on client', () => { + const fakeCaptureMessage = jest.fn(() => 'mock-event-id'); + const fakeClient = { + captureMessage: fakeCaptureMessage, + } as unknown as Client; + const scope = new Scope(); + scope.setClient(fakeClient); + + scope.captureMessage('baz'); + + expect(fakeCaptureMessage).toHaveBeenCalledWith( + expect.anything(), + undefined, + expect.objectContaining({ originalException: 'baz' }), + scope, + ); + }); + + it('should forward level and hint to captureMessage() on client', () => { + const fakeCaptureMessage = jest.fn(() => 'mock-event-id'); + const fakeClient = { + captureMessage: fakeCaptureMessage, + } as unknown as Client; + const scope = new Scope(); + scope.setClient(fakeClient); + + scope.captureMessage('asdf', 'fatal', { event_id: 'asdf', data: { foo: 'bar' } }); + + expect(fakeCaptureMessage).toHaveBeenCalledWith( + expect.anything(), + 'fatal', + expect.objectContaining({ event_id: 'asdf', data: { foo: 'bar' } }), + scope, + ); + }); + }); + + describe('.captureEvent()', () => { + it('should call captureEvent() on client with newly generated event ID if not explicitly passed in', () => { + const fakeCaptureEvent = jest.fn(() => 'mock-event-id'); + const fakeClient = { + captureEvent: fakeCaptureEvent, + } as unknown as Client; + const scope = new Scope(); + scope.setClient(fakeClient); + + scope.captureEvent({}); + + expect(fakeCaptureEvent).toHaveBeenCalledWith( + expect.anything(), + expect.objectContaining({ event_id: expect.any(String) }), + scope, + ); + }); + + it('should return event ID when no client is on the scope', () => { + const scope = new Scope(); + + const eventId = scope.captureEvent({}); + + expect(eventId).toEqual(expect.any(String)); + }); + + it('should pass event to captureEvent() on client', () => { + const fakeCaptureEvent = jest.fn(() => 'mock-event-id'); + const fakeClient = { + captureEvent: fakeCaptureEvent, + } as unknown as Client; + const scope = new Scope(); + scope.setClient(fakeClient); + + const event: Event = { event_id: 'asdf' }; + + scope.captureEvent(event); + + expect(fakeCaptureEvent).toHaveBeenCalledWith(event, expect.anything(), scope); + }); + + it('should forward hint to captureEvent() on client', () => { + const fakeCaptureEvent = jest.fn(() => 'mock-event-id'); + const fakeClient = { + captureEvent: fakeCaptureEvent, + } as unknown as Client; + const scope = new Scope(); + scope.setClient(fakeClient); + + scope.captureEvent({}, { event_id: 'asdf', data: { foo: 'bar' } }); + + expect(fakeCaptureEvent).toHaveBeenCalledWith( + expect.anything(), + expect.objectContaining({ event_id: 'asdf', data: { foo: 'bar' } }), + scope, + ); + }); + }); }); diff --git a/packages/deno/test/__snapshots__/mod.test.ts.snap b/packages/deno/test/__snapshots__/mod.test.ts.snap index f278e370312b..607d87b968bc 100644 --- a/packages/deno/test/__snapshots__/mod.test.ts.snap +++ b/packages/deno/test/__snapshots__/mod.test.ts.snap @@ -77,8 +77,8 @@ snapshot[`captureException 1`] = ` lineno: 526, }, { - colno: 24, - context_line: " hub.captureException(something());", + colno: 27, + context_line: " client.captureException(something());", filename: "app:///test/mod.test.ts", function: "", in_app: true, @@ -112,7 +112,7 @@ snapshot[`captureException 1`] = ` post_context: [ " }", "", - " hub.captureException(something());", + " client.captureException(something());", "", " await delay(200);", " await assertSnapshot(t, ev);", @@ -121,7 +121,7 @@ snapshot[`captureException 1`] = ` pre_context: [ "Deno.test('captureException', async t => {", " let ev: sentryTypes.Event | undefined;", - " const [hub] = getTestClient(event => {", + " const [, client] = getTestClient(event => {", " ev = event;", " });", "", diff --git a/packages/deno/test/mod.test.ts b/packages/deno/test/mod.test.ts index a14e04b61ff9..1568584d8281 100644 --- a/packages/deno/test/mod.test.ts +++ b/packages/deno/test/mod.test.ts @@ -35,7 +35,7 @@ function delay(time: number): Promise { Deno.test('captureException', async t => { let ev: sentryTypes.Event | undefined; - const [hub] = getTestClient(event => { + const [, client] = getTestClient(event => { ev = event; }); @@ -43,7 +43,7 @@ Deno.test('captureException', async t => { return new Error('Some unhandled error'); } - hub.captureException(something()); + client.captureException(something()); await delay(200); await assertSnapshot(t, ev); @@ -51,11 +51,11 @@ Deno.test('captureException', async t => { Deno.test('captureMessage', async t => { let ev: sentryTypes.Event | undefined; - const [hub] = getTestClient(event => { + const [, client] = getTestClient(event => { ev = event; }); - hub.captureMessage('Some error message'); + client.captureMessage('Some error message'); await delay(200); await assertSnapshot(t, ev); diff --git a/packages/node-experimental/src/sdk/types.ts b/packages/node-experimental/src/sdk/types.ts index 90c61dceda86..2563989f474f 100644 --- a/packages/node-experimental/src/sdk/types.ts +++ b/packages/node-experimental/src/sdk/types.ts @@ -2,8 +2,6 @@ import type { Attachment, Breadcrumb, Contexts, - Event, - EventHint, EventProcessor, Extras, Hub, @@ -11,7 +9,6 @@ import type { Primitive, PropagationContext, Scope as BaseScope, - Severity, SeverityLevel, User, } from '@sentry/types'; @@ -35,14 +32,6 @@ export interface Scope extends BaseScope { isolationScope: typeof this | undefined; // @ts-expect-error typeof this is what we want here clone(scope?: Scope): typeof this; - captureException(exception: unknown, hint?: EventHint): string; - captureMessage( - message: string, - // eslint-disable-next-line deprecation/deprecation - level?: Severity | SeverityLevel, - hint?: EventHint, - ): string; - captureEvent(event: Event, hint?: EventHint): string; lastEventId(): string | undefined; getScopeData(): ScopeData; } diff --git a/packages/node-experimental/test/integration/breadcrumbs.test.ts b/packages/node-experimental/test/integration/breadcrumbs.test.ts index fea78a353011..c576cc85b11a 100644 --- a/packages/node-experimental/test/integration/breadcrumbs.test.ts +++ b/packages/node-experimental/test/integration/breadcrumbs.test.ts @@ -27,6 +27,7 @@ describe('Integration | breadcrumbs', () => { hub.addBreadcrumb({ timestamp: 123455, message: 'test3' }); const error = new Error('test'); + // eslint-disable-next-line deprecation/deprecation hub.captureException(error); await client.flush(); @@ -118,6 +119,7 @@ describe('Integration | breadcrumbs', () => { hub.addBreadcrumb({ timestamp: 123455, message: 'test3' }); }); + // eslint-disable-next-line deprecation/deprecation hub.captureException(error); }); @@ -171,6 +173,7 @@ describe('Integration | breadcrumbs', () => { hub.addBreadcrumb({ timestamp: 123457, message: 'test2-b' }); }); + // eslint-disable-next-line deprecation/deprecation hub.captureException(error); }); }); @@ -214,6 +217,7 @@ describe('Integration | breadcrumbs', () => { hub.addBreadcrumb({ timestamp: 123457, message: 'test2' }); }); + // eslint-disable-next-line deprecation/deprecation hub.captureException(error); }); @@ -261,6 +265,7 @@ describe('Integration | breadcrumbs', () => { startSpan({ name: 'inner3' }, () => { hub.addBreadcrumb({ timestamp: 123457, message: 'test4' }); + // eslint-disable-next-line deprecation/deprecation hub.captureException(error); startSpan({ name: 'inner4' }, () => { @@ -321,6 +326,7 @@ describe('Integration | breadcrumbs', () => { await new Promise(resolve => setTimeout(resolve, 10)); + // eslint-disable-next-line deprecation/deprecation hub.captureException(error); }); }); diff --git a/packages/node/test/handlers.test.ts b/packages/node/test/handlers.test.ts index 91f610ef24da..ed5592cef5e0 100644 --- a/packages/node/test/handlers.test.ts +++ b/packages/node/test/handlers.test.ts @@ -587,6 +587,7 @@ describe('errorHandler()', () => { // `sentryErrorMiddleware` uses `withScope`, and we need access to the temporary scope it creates, so monkeypatch // `captureException` in order to examine the scope as it exists inside the `withScope` callback + // eslint-disable-next-line deprecation/deprecation hub.captureException = function (this: sentryCore.Hub, _exception: any) { const scope = this.getScope(); expect((scope as any)._sdkProcessingMetadata.request).toEqual(req); diff --git a/packages/node/test/index.test.ts b/packages/node/test/index.test.ts index 30658128d2b4..c2e003b14d94 100644 --- a/packages/node/test/index.test.ts +++ b/packages/node/test/index.test.ts @@ -288,6 +288,7 @@ describe('SentryNode', () => { hub.bindClient(client); expect(getCurrentHub().getClient()).toBe(client); expect(getClient()).toBe(client); + // eslint-disable-next-line deprecation/deprecation hub.captureEvent({ message: 'test domain' }); }); }); diff --git a/packages/opentelemetry-node/test/spanprocessor.test.ts b/packages/opentelemetry-node/test/spanprocessor.test.ts index 69ef554c132c..e9eb7b76f4db 100644 --- a/packages/opentelemetry-node/test/spanprocessor.test.ts +++ b/packages/opentelemetry-node/test/spanprocessor.test.ts @@ -888,6 +888,7 @@ describe('SentrySpanProcessor', () => { tracer.startActiveSpan('GET /users', parentOtelSpan => { tracer.startActiveSpan('SELECT * FROM users;', child => { + // eslint-disable-next-line deprecation/deprecation hub.captureException(new Error('oh nooooo!')); otelSpan = child as OtelSpan; child.end(); diff --git a/packages/opentelemetry/src/custom/scope.ts b/packages/opentelemetry/src/custom/scope.ts index c6bdfb164900..2455d616ff39 100644 --- a/packages/opentelemetry/src/custom/scope.ts +++ b/packages/opentelemetry/src/custom/scope.ts @@ -46,6 +46,7 @@ export class OpenTelemetryScope extends Scope { newScope._attachments = [...this['_attachments']]; newScope._sdkProcessingMetadata = { ...this['_sdkProcessingMetadata'] }; newScope._propagationContext = { ...this['_propagationContext'] }; + newScope._client = this._client; return newScope; } diff --git a/packages/opentelemetry/test/integration/breadcrumbs.test.ts b/packages/opentelemetry/test/integration/breadcrumbs.test.ts index af095be83f76..096f31c6e9bf 100644 --- a/packages/opentelemetry/test/integration/breadcrumbs.test.ts +++ b/packages/opentelemetry/test/integration/breadcrumbs.test.ts @@ -29,6 +29,7 @@ describe('Integration | breadcrumbs', () => { hub.addBreadcrumb({ timestamp: 123455, message: 'test3' }); const error = new Error('test'); + // eslint-disable-next-line deprecation/deprecation hub.captureException(error); await client.flush(); @@ -73,6 +74,7 @@ describe('Integration | breadcrumbs', () => { withScope(() => { hub.addBreadcrumb({ timestamp: 123456, message: 'test2' }); + // eslint-disable-next-line deprecation/deprecation hub.captureException(error); }); @@ -123,6 +125,7 @@ describe('Integration | breadcrumbs', () => { hub.addBreadcrumb({ timestamp: 123455, message: 'test3' }); }); + // eslint-disable-next-line deprecation/deprecation hub.captureException(error); }); @@ -173,6 +176,7 @@ describe('Integration | breadcrumbs', () => { hub.addBreadcrumb({ timestamp: 123457, message: 'test2-b' }); }); + // eslint-disable-next-line deprecation/deprecation hub.captureException(error); }); @@ -215,6 +219,7 @@ describe('Integration | breadcrumbs', () => { hub.addBreadcrumb({ timestamp: 123457, message: 'test2' }); }); + // eslint-disable-next-line deprecation/deprecation hub.captureException(error); }); @@ -262,6 +267,7 @@ describe('Integration | breadcrumbs', () => { startSpan({ name: 'inner3' }, () => { hub.addBreadcrumb({ timestamp: 123457, message: 'test4' }); + // eslint-disable-next-line deprecation/deprecation hub.captureException(error); startSpan({ name: 'inner4' }, () => { @@ -321,6 +327,7 @@ describe('Integration | breadcrumbs', () => { await new Promise(resolve => setTimeout(resolve, 10)); + // eslint-disable-next-line deprecation/deprecation hub.captureException(error); }); diff --git a/packages/opentelemetry/test/utils/setupEventContextTrace.test.ts b/packages/opentelemetry/test/utils/setupEventContextTrace.test.ts index 887a7f6bc803..a73067581b9a 100644 --- a/packages/opentelemetry/test/utils/setupEventContextTrace.test.ts +++ b/packages/opentelemetry/test/utils/setupEventContextTrace.test.ts @@ -45,6 +45,7 @@ describe('setupEventContextTrace', () => { it('works with no active span', async () => { const error = new Error('test'); + // eslint-disable-next-line deprecation/deprecation hub.captureException(error); await client.flush(); @@ -79,6 +80,7 @@ describe('setupEventContextTrace', () => { client.tracer.startActiveSpan('inner', innerSpan => { innerId = innerSpan?.spanContext().spanId; + // eslint-disable-next-line deprecation/deprecation hub.captureException(error); }); }); diff --git a/packages/types/src/scope.ts b/packages/types/src/scope.ts index 5dbdee05260d..7cbe7ff264b5 100644 --- a/packages/types/src/scope.ts +++ b/packages/types/src/scope.ts @@ -2,6 +2,7 @@ import type { Attachment } from './attachment'; import type { Breadcrumb } from './breadcrumb'; import type { Client } from './client'; import type { Context, Contexts } from './context'; +import type { Event, EventHint } from './event'; import type { EventProcessor } from './eventprocessor'; import type { Extra, Extras } from './extra'; import type { Primitive } from './misc'; @@ -229,4 +230,32 @@ export interface Scope { * Get propagation context from the scope, used for distributed tracing */ getPropagationContext(): PropagationContext; + + /** + * Capture an exception for this scope. + * + * @param exception The exception to capture. + * @param hint Optinal additional data to attach to the Sentry event. + * @returns the id of the captured Sentry event. + */ + captureException(exception: unknown, hint?: EventHint): string; + + /** + * Capture a message for this scope. + * + * @param exception The exception to capture. + * @param level An optional severity level to report the message with. + * @param hint Optional additional data to attach to the Sentry event. + * @returns the id of the captured message. + */ + captureMessage(message: string, level?: SeverityLevel, hint?: EventHint): string; + + /** + * Capture a Sentry event for this scope. + * + * @param exception The event to capture. + * @param hint Optional additional data to attach to the Sentry event. + * @returns the id of the captured event. + */ + captureEvent(event: Event, hint?: EventHint): string; } diff --git a/packages/vue/src/errorhandler.ts b/packages/vue/src/errorhandler.ts index 9d09bdf8c181..725f9b56c714 100644 --- a/packages/vue/src/errorhandler.ts +++ b/packages/vue/src/errorhandler.ts @@ -1,4 +1,4 @@ -import { getCurrentHub } from '@sentry/browser'; +import { captureException } from '@sentry/core'; import { consoleSandbox } from '@sentry/utils'; import type { ViewModel, Vue, VueOptions } from './types'; @@ -30,7 +30,7 @@ export const attachErrorHandler = (app: Vue, options: VueOptions): void => { // Capture exception in the next event loop, to make sure that all breadcrumbs are recorded in time. setTimeout(() => { - getCurrentHub().captureException(error, { + captureException(error, { captureContext: { contexts: { vue: metadata } }, mechanism: { handled: false }, }); From 0a9751419bc391bbbf92ce896c2b8636c5526e8e Mon Sep 17 00:00:00 2001 From: Luca Forstner Date: Mon, 8 Jan 2024 13:13:05 +0100 Subject: [PATCH 19/43] ref(node-experimental): Deprecate `lastEventId` on scope (#10093) --- packages/node-experimental/src/sdk/api.ts | 1 + packages/node-experimental/src/sdk/types.ts | 3 +++ 2 files changed, 4 insertions(+) diff --git a/packages/node-experimental/src/sdk/api.ts b/packages/node-experimental/src/sdk/api.ts index 15882a6f165a..722977148c42 100644 --- a/packages/node-experimental/src/sdk/api.ts +++ b/packages/node-experimental/src/sdk/api.ts @@ -59,6 +59,7 @@ export function withIsolationScope(callback: (isolationScope: Scope) => T): T * @deprecated This function will be removed in the next major version of the Sentry SDK. */ export function lastEventId(): string | undefined { + // eslint-disable-next-line deprecation/deprecation return getCurrentScope().lastEventId(); } diff --git a/packages/node-experimental/src/sdk/types.ts b/packages/node-experimental/src/sdk/types.ts index 2563989f474f..57ad321a5470 100644 --- a/packages/node-experimental/src/sdk/types.ts +++ b/packages/node-experimental/src/sdk/types.ts @@ -32,6 +32,9 @@ export interface Scope extends BaseScope { isolationScope: typeof this | undefined; // @ts-expect-error typeof this is what we want here clone(scope?: Scope): typeof this; + /** + * @deprecated This function will be removed in the next major version of the Sentry SDK. + */ lastEventId(): string | undefined; getScopeData(): ScopeData; } From b6c369dbb7d0c757f2e493a5d9894724bba4a86f Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Mon, 8 Jan 2024 14:18:09 +0100 Subject: [PATCH 20/43] feat(core): Allow to pass `scope` to `startSpan` APIs (#10076) Also allow to pass this to `withScope()`, which actually aligns with OTEL as well, where you can also do that. If you pass a scope there, it will use this instead of forking a new one. This should be the last thing needed to refactor some `span.startChild()` occurrences - you can now store the scope you want to fork off, and pass this into `startSpan` as needed. --- packages/core/src/exports.ts | 31 ++++- packages/core/src/tracing/trace.ts | 14 +- packages/core/test/lib/exports.test.ts | 24 ++++ packages/core/test/lib/tracing/trace.test.ts | 120 +++++++++++++----- packages/node-experimental/src/sdk/api.ts | 36 +++++- .../src/utils/contextData.ts | 15 +++ packages/opentelemetry/src/types.ts | 3 +- 7 files changed, 200 insertions(+), 43 deletions(-) diff --git a/packages/core/src/exports.ts b/packages/core/src/exports.ts index 04b953acfd7e..795352735ae8 100644 --- a/packages/core/src/exports.ts +++ b/packages/core/src/exports.ts @@ -12,6 +12,7 @@ import type { FinishedCheckIn, MonitorConfig, Primitive, + Scope as ScopeInterface, Session, SessionContext, Severity, @@ -167,11 +168,33 @@ export function setUser(user: User | null): ReturnType { * pushScope(); * callback(); * popScope(); - * - * @param callback that will be enclosed into push/popScope. */ -export function withScope(callback: (scope: Scope) => T): T { - return getCurrentHub().withScope(callback); +export function withScope(callback: (scope: Scope) => T): T; +/** + * Set the given scope as the active scope in the callback. + */ +export function withScope(scope: ScopeInterface | undefined, callback: (scope: Scope) => T): T; +/** + * Either creates a new active scope, or sets the given scope as active scope in the given callback. + */ +export function withScope( + ...rest: [callback: (scope: Scope) => T] | [scope: ScopeInterface | undefined, callback: (scope: Scope) => T] +): T { + // If a scope is defined, we want to make this the active scope instead of the default one + if (rest.length === 2) { + const [scope, callback] = rest; + if (!scope) { + return getCurrentHub().withScope(callback); + } + + const hub = getCurrentHub(); + return hub.withScope(() => { + hub.getStackTop().scope = scope as Scope; + return callback(scope as Scope); + }); + } + + return getCurrentHub().withScope(rest[0]); } /** diff --git a/packages/core/src/tracing/trace.ts b/packages/core/src/tracing/trace.ts index 95aa3fda451d..bbb1a9dbf131 100644 --- a/packages/core/src/tracing/trace.ts +++ b/packages/core/src/tracing/trace.ts @@ -1,4 +1,4 @@ -import type { Span, SpanTimeInput, TransactionContext } from '@sentry/types'; +import type { Scope, Span, SpanTimeInput, TransactionContext } from '@sentry/types'; import { dropUndefinedKeys, logger, tracingContextFromHeaders } from '@sentry/utils'; import { DEBUG_BUILD } from '../debug-build'; @@ -12,6 +12,9 @@ import { spanTimeInputToSeconds } from '../utils/spanUtils'; interface StartSpanOptions extends TransactionContext { /** A manually specified start time for the created `Span` object. */ startTime?: SpanTimeInput; + + /** If defined, start this span off this scope instead off the current scope. */ + scope?: Scope; } /** @@ -74,9 +77,10 @@ export function trace( export function startSpan(context: StartSpanOptions, callback: (span: Span | undefined) => T): T { const ctx = normalizeContext(context); - return withScope(scope => { + return withScope(context.scope, scope => { const hub = getCurrentHub(); - const parentSpan = scope.getSpan(); + const scopeForSpan = context.scope || scope; + const parentSpan = scopeForSpan.getSpan(); const activeSpan = createChildSpanOrTransaction(hub, parentSpan, ctx); scope.setSpan(activeSpan); @@ -116,7 +120,7 @@ export function startSpanManual( ): T { const ctx = normalizeContext(context); - return withScope(scope => { + return withScope(context.scope, scope => { const hub = getCurrentHub(); const parentSpan = scope.getSpan(); @@ -156,7 +160,7 @@ export function startInactiveSpan(context: StartSpanOptions): Span | undefined { const ctx = normalizeContext(context); const hub = getCurrentHub(); - const parentSpan = getActiveSpan(); + const parentSpan = context.scope ? context.scope.getSpan() : getActiveSpan(); return parentSpan ? // eslint-disable-next-line deprecation/deprecation parentSpan.startChild(ctx) diff --git a/packages/core/test/lib/exports.test.ts b/packages/core/test/lib/exports.test.ts index c16073255030..a02673d15a1f 100644 --- a/packages/core/test/lib/exports.test.ts +++ b/packages/core/test/lib/exports.test.ts @@ -134,6 +134,30 @@ describe('withScope', () => { expect(getCurrentScope()).toBe(scope1); }); + + it('allows to pass a custom scope', () => { + const scope1 = getCurrentScope(); + scope1.setExtra('x1', 'x1'); + + const customScope = new Scope(); + customScope.setExtra('x2', 'x2'); + + withScope(customScope, scope2 => { + expect(scope2).not.toBe(scope1); + expect(scope2).toBe(customScope); + expect(getCurrentScope()).toBe(scope2); + expect(scope2['_extra']).toEqual({ x2: 'x2' }); + }); + + withScope(customScope, scope3 => { + expect(scope3).not.toBe(scope1); + expect(scope3).toBe(customScope); + expect(getCurrentScope()).toBe(scope3); + expect(scope3['_extra']).toEqual({ x2: 'x2' }); + }); + + expect(getCurrentScope()).toBe(scope1); + }); }); describe('session APIs', () => { diff --git a/packages/core/test/lib/tracing/trace.test.ts b/packages/core/test/lib/tracing/trace.test.ts index 4659ae2e112f..1924af197ecf 100644 --- a/packages/core/test/lib/tracing/trace.test.ts +++ b/packages/core/test/lib/tracing/trace.test.ts @@ -1,6 +1,6 @@ -import type { Span } from '@sentry/types'; import { Hub, addTracingExtensions, getCurrentScope, makeMain } from '../../../src'; -import { continueTrace, startInactiveSpan, startSpan, startSpanManual } from '../../../src/tracing'; +import { Scope } from '../../../src/scope'; +import { Span, continueTrace, startInactiveSpan, startSpan, startSpanManual } from '../../../src/tracing'; import { TestClient, getDefaultTestClientOptions } from '../../mocks/client'; beforeAll(() => { @@ -81,18 +81,6 @@ describe('startSpan', () => { expect(ref.status).toEqual(isError ? 'internal_error' : undefined); }); - it('creates & finishes span', async () => { - let _span: Span | undefined; - startSpan({ name: 'GET users/[id]' }, span => { - expect(span).toBeDefined(); - expect(span?.endTimestamp).toBeUndefined(); - _span = span; - }); - - expect(_span).toBeDefined(); - expect(_span?.endTimestamp).toBeDefined(); - }); - it('allows traceparent information to be overriden', async () => { let ref: any = undefined; client.on('finishTransaction', transaction => { @@ -160,14 +148,6 @@ describe('startSpan', () => { expect(ref.spanRecorder.spans[1].status).toEqual(isError ? 'internal_error' : undefined); }); - it('allows to pass a `startTime`', () => { - const start = startSpan({ name: 'outer', startTime: [1234, 0] }, span => { - return span?.startTimestamp; - }); - - expect(start).toEqual(1234); - }); - it('allows for span to be mutated', async () => { let ref: any = undefined; client.on('finishTransaction', transaction => { @@ -189,18 +169,57 @@ describe('startSpan', () => { expect(ref.spanRecorder.spans).toHaveLength(2); expect(ref.spanRecorder.spans[1].op).toEqual('db.query'); }); + }); - it('forks the scope', () => { - const initialScope = getCurrentScope(); + it('creates & finishes span', async () => { + let _span: Span | undefined; + startSpan({ name: 'GET users/[id]' }, span => { + expect(span).toBeDefined(); + expect(span?.endTimestamp).toBeUndefined(); + _span = span as Span; + }); - startSpan({ name: 'GET users/[id]' }, span => { - expect(getCurrentScope()).not.toBe(initialScope); - expect(getCurrentScope().getSpan()).toBe(span); - }); + expect(_span).toBeDefined(); + expect(_span?.endTimestamp).toBeDefined(); + }); - expect(getCurrentScope()).toBe(initialScope); - expect(initialScope.getSpan()).toBe(undefined); + it('allows to pass a `startTime`', () => { + const start = startSpan({ name: 'outer', startTime: [1234, 0] }, span => { + return span?.startTimestamp; }); + + expect(start).toEqual(1234); + }); + + it('forks the scope', () => { + const initialScope = getCurrentScope(); + + startSpan({ name: 'GET users/[id]' }, span => { + expect(getCurrentScope()).not.toBe(initialScope); + expect(getCurrentScope().getSpan()).toBe(span); + }); + + expect(getCurrentScope()).toBe(initialScope); + expect(initialScope.getSpan()).toBe(undefined); + }); + + it('allows to pass a scope', () => { + const initialScope = getCurrentScope(); + + const manualScope = new Scope(); + const parentSpan = new Span({ spanId: 'parent-span-id' }); + manualScope.setSpan(parentSpan); + + startSpan({ name: 'GET users/[id]', scope: manualScope }, span => { + expect(getCurrentScope()).not.toBe(initialScope); + expect(getCurrentScope()).toBe(manualScope); + expect(getCurrentScope().getSpan()).toBe(span); + + expect(span?.parentSpanId).toBe('parent-span-id'); + }); + + expect(getCurrentScope()).toBe(initialScope); + expect(initialScope.getSpan()).toBe(undefined); }); }); @@ -231,6 +250,29 @@ describe('startSpanManual', () => { expect(initialScope.getSpan()).toBe(undefined); }); + it('allows to pass a scope', () => { + const initialScope = getCurrentScope(); + + const manualScope = new Scope(); + const parentSpan = new Span({ spanId: 'parent-span-id' }); + manualScope.setSpan(parentSpan); + + startSpanManual({ name: 'GET users/[id]', scope: manualScope }, (span, finish) => { + expect(getCurrentScope()).not.toBe(initialScope); + expect(getCurrentScope()).toBe(manualScope); + expect(getCurrentScope().getSpan()).toBe(span); + expect(span?.parentSpanId).toBe('parent-span-id'); + + finish(); + + // Is still the active span + expect(getCurrentScope().getSpan()).toBe(span); + }); + + expect(getCurrentScope()).toBe(initialScope); + expect(initialScope.getSpan()).toBe(undefined); + }); + it('allows to pass a `startTime`', () => { const start = startSpanManual({ name: 'outer', startTime: [1234, 0] }, span => { span?.end(); @@ -266,6 +308,24 @@ describe('startInactiveSpan', () => { expect(initialScope.getSpan()).toBeUndefined(); }); + it('allows to pass a scope', () => { + const initialScope = getCurrentScope(); + + const manualScope = new Scope(); + const parentSpan = new Span({ spanId: 'parent-span-id' }); + manualScope.setSpan(parentSpan); + + const span = startInactiveSpan({ name: 'GET users/[id]', scope: manualScope }); + + expect(span).toBeDefined(); + expect(span?.parentSpanId).toBe('parent-span-id'); + expect(initialScope.getSpan()).toBeUndefined(); + + span?.end(); + + expect(initialScope.getSpan()).toBeUndefined(); + }); + it('allows to pass a `startTime`', () => { const span = startInactiveSpan({ name: 'outer', startTime: [1234, 0] }); expect(span?.startTimestamp).toEqual(1234); diff --git a/packages/node-experimental/src/sdk/api.ts b/packages/node-experimental/src/sdk/api.ts index 722977148c42..abb1e37944ac 100644 --- a/packages/node-experimental/src/sdk/api.ts +++ b/packages/node-experimental/src/sdk/api.ts @@ -16,7 +16,7 @@ import type { User, } from '@sentry/types'; import { consoleSandbox, dateTimestampInSeconds } from '@sentry/utils'; -import { getScopesFromContext, setScopesOnContext } from '../utils/contextData'; +import { getContextFromScope, getScopesFromContext, setScopesOnContext } from '../utils/contextData'; import type { ExclusiveEventHintOrCaptureContext } from '../utils/prepareEvent'; import { parseEventHintOrCaptureContext } from '../utils/prepareEvent'; @@ -27,9 +27,39 @@ export { getCurrentScope, getGlobalScope, getIsolationScope, getClient }; export { setCurrentScope, setIsolationScope } from './scope'; /** - * Fork a scope from the current scope, and make it the current scope in the given callback + * Creates a new scope with and executes the given operation within. + * The scope is automatically removed once the operation + * finishes or throws. + * + * This is essentially a convenience function for: + * + * pushScope(); + * callback(); + * popScope(); */ -export function withScope(callback: (scope: Scope) => T): T { +export function withScope(callback: (scope: Scope) => T): T; +/** + * Set the given scope as the active scope in the callback. + */ +export function withScope(scope: Scope | undefined, callback: (scope: Scope) => T): T; +/** + * Either creates a new active scope, or sets the given scope as active scope in the given callback. + */ +export function withScope( + ...rest: [callback: (scope: Scope) => T] | [scope: Scope | undefined, callback: (scope: Scope) => T] +): T { + // If a scope is defined, we want to make this the active scope instead of the default one + if (rest.length === 2) { + const [scope, callback] = rest; + if (!scope) { + return context.with(context.active(), () => callback(getCurrentScope())); + } + + const ctx = getContextFromScope(scope); + return context.with(ctx || context.active(), () => callback(getCurrentScope())); + } + + const callback = rest[0]; return context.with(context.active(), () => callback(getCurrentScope())); } diff --git a/packages/node-experimental/src/utils/contextData.ts b/packages/node-experimental/src/utils/contextData.ts index 5c69f186eb6d..cb77d37a9ae0 100644 --- a/packages/node-experimental/src/utils/contextData.ts +++ b/packages/node-experimental/src/utils/contextData.ts @@ -1,10 +1,13 @@ import type { Context } from '@opentelemetry/api'; import { createContextKey } from '@opentelemetry/api'; +import type { Scope } from '@sentry/types'; import type { CurrentScopes } from '../sdk/types'; export const SENTRY_SCOPES_CONTEXT_KEY = createContextKey('sentry_scopes'); +const SCOPE_CONTEXT_MAP = new WeakMap(); + /** * Try to get the current scopes from the given OTEL context. * This requires a Context Manager that was wrapped with getWrappedContextManager. @@ -18,5 +21,17 @@ export function getScopesFromContext(context: Context): CurrentScopes | undefine * This will return a forked context with the Propagation Context set. */ export function setScopesOnContext(context: Context, scopes: CurrentScopes): Context { + // So we can look up the context from the scope later + SCOPE_CONTEXT_MAP.set(scopes.scope, context); + SCOPE_CONTEXT_MAP.set(scopes.isolationScope, context); + return context.setValue(SENTRY_SCOPES_CONTEXT_KEY, scopes); } + +/** + * Get the context related to a scope. + * TODO v8: Use this for the `trace` functions. + * */ +export function getContextFromScope(scope: Scope): Context | undefined { + return SCOPE_CONTEXT_MAP.get(scope); +} diff --git a/packages/opentelemetry/src/types.ts b/packages/opentelemetry/src/types.ts index fdab000a6e09..168a9f4893a6 100644 --- a/packages/opentelemetry/src/types.ts +++ b/packages/opentelemetry/src/types.ts @@ -1,6 +1,6 @@ import type { Attributes, Span as WriteableSpan, SpanKind, TimeInput, Tracer } from '@opentelemetry/api'; import type { BasicTracerProvider, ReadableSpan, Span } from '@opentelemetry/sdk-trace-base'; -import type { SpanOrigin, TransactionMetadata, TransactionSource } from '@sentry/types'; +import type { Scope, SpanOrigin, TransactionMetadata, TransactionSource } from '@sentry/types'; export interface OpenTelemetryClient { tracer: Tracer; @@ -13,6 +13,7 @@ export interface OpenTelemetrySpanContext { metadata?: Partial; origin?: SpanOrigin; source?: TransactionSource; + scope?: Scope; // Base SpanOptions we support attributes?: Attributes; From 73a8314cb5250fb147d8ae41f69ddab08ce18a2f Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Mon, 8 Jan 2024 14:40:48 +0100 Subject: [PATCH 21/43] feat(core): Add `span.spanContext()` (#10037) This will replace `spanId`, `traceId` and `sampled` lookups. Because the bitwise format of OTEL is a bit weird IMHO to check if a span is sampled, I also added a utility `spanIsSampled(span)` method to abstract this away. --- MIGRATION.md | 2 + .../startTransaction/basic_usage/test.ts | 1 + .../sentry-trace/baggage-header-out/server.ts | 1 + .../baggage-transaction-name/server.ts | 1 + packages/astro/test/server/meta.test.ts | 31 +++- .../test/unit/profiling/hubextensions.test.ts | 24 +++- .../bun/test/integrations/bunserver.test.ts | 2 +- packages/core/src/index.ts | 1 + packages/core/src/tracing/idletransaction.ts | 12 +- packages/core/src/tracing/span.ts | 132 +++++++++++++----- packages/core/src/tracing/transaction.ts | 10 +- packages/core/src/utils/spanUtils.ts | 24 +++- packages/core/test/lib/tracing/span.test.ts | 30 ++++ .../core/test/lib/utils/spanUtils.test.ts | 18 ++- packages/hub/test/scope.test.ts | 11 +- packages/node/test/handlers.test.ts | 2 +- .../node/test/integrations/undici.test.ts | 4 +- .../opentelemetry-node/src/utils/spanMap.ts | 2 +- .../test/propagator.test.ts | 4 +- .../test/spanprocessor.test.ts | 18 +-- .../test/custom/transaction.test.ts | 12 +- .../src/browser/browsertracing.ts | 14 +- .../tracing-internal/src/browser/request.ts | 2 +- packages/tracing-internal/src/common/fetch.ts | 4 +- packages/tracing/test/idletransaction.test.ts | 26 ++-- packages/tracing/test/span.test.ts | 8 +- packages/types/src/index.ts | 2 + packages/types/src/span.ts | 59 +++++++- packages/types/src/transaction.ts | 12 +- 29 files changed, 356 insertions(+), 113 deletions(-) diff --git a/MIGRATION.md b/MIGRATION.md index 1aa0d8ea9270..e29480fb83c5 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -69,6 +69,8 @@ In v8, the Span class is heavily reworked. The following properties & methods ar * `span.toTraceparent()`: use `spanToTraceHeader(span)` util instead. * `span.getTraceContext()`: Use `spanToTraceContext(span)` utility function instead. * `span.sampled`: Use `span.isRecording()` instead. +* `span.spanId`: Use `span.spanContext().spanId` instead. +* `span.traceId`: Use `span.spanContext().traceId` instead. ## Deprecate `pushScope` & `popScope` in favor of `withScope` diff --git a/dev-packages/browser-integration-tests/suites/public-api/startTransaction/basic_usage/test.ts b/dev-packages/browser-integration-tests/suites/public-api/startTransaction/basic_usage/test.ts index 2fba18b0804b..04e39e012639 100644 --- a/dev-packages/browser-integration-tests/suites/public-api/startTransaction/basic_usage/test.ts +++ b/dev-packages/browser-integration-tests/suites/public-api/startTransaction/basic_usage/test.ts @@ -39,5 +39,6 @@ sentryTest('should report finished spans as children of the root transaction', a const span_5 = transaction.spans?.[2]; expect(span_5?.op).toBe('span_5'); + // eslint-disable-next-line deprecation/deprecation expect(span_5?.parentSpanId).toEqual(span_3?.spanId); }); diff --git a/dev-packages/node-integration-tests/suites/express/sentry-trace/baggage-header-out/server.ts b/dev-packages/node-integration-tests/suites/express/sentry-trace/baggage-header-out/server.ts index 31e5580fe56a..4f4fc91b670a 100644 --- a/dev-packages/node-integration-tests/suites/express/sentry-trace/baggage-header-out/server.ts +++ b/dev-packages/node-integration-tests/suites/express/sentry-trace/baggage-header-out/server.ts @@ -28,6 +28,7 @@ app.use(cors()); app.get('/test/express', (_req, res) => { const transaction = Sentry.getCurrentHub().getScope().getTransaction(); if (transaction) { + // eslint-disable-next-line deprecation/deprecation transaction.traceId = '86f39e84263a4de99c326acab3bfe3bd'; } const headers = http.get('http://somewhere.not.sentry/').getHeaders(); diff --git a/dev-packages/node-integration-tests/suites/express/sentry-trace/baggage-transaction-name/server.ts b/dev-packages/node-integration-tests/suites/express/sentry-trace/baggage-transaction-name/server.ts index 061a7815ca75..24c3541d8529 100644 --- a/dev-packages/node-integration-tests/suites/express/sentry-trace/baggage-transaction-name/server.ts +++ b/dev-packages/node-integration-tests/suites/express/sentry-trace/baggage-transaction-name/server.ts @@ -31,6 +31,7 @@ app.use(cors()); app.get('/test/express', (_req, res) => { const transaction = Sentry.getCurrentHub().getScope().getTransaction(); if (transaction) { + // eslint-disable-next-line deprecation/deprecation transaction.traceId = '86f39e84263a4de99c326acab3bfe3bd'; transaction.setMetadata({ source: 'route' }); } diff --git a/packages/astro/test/server/meta.test.ts b/packages/astro/test/server/meta.test.ts index 0586507b570c..1aa9e9405be5 100644 --- a/packages/astro/test/server/meta.test.ts +++ b/packages/astro/test/server/meta.test.ts @@ -3,10 +3,17 @@ import { vi } from 'vitest'; import { getTracingMetaTags, isValidBaggageString } from '../../src/server/meta'; +const TRACE_FLAG_SAMPLED = 0x1; + const mockedSpan = { isRecording: () => true, - traceId: '12345678901234567890123456789012', - spanId: '1234567890123456', + spanContext: () => { + return { + traceId: '12345678901234567890123456789012', + spanId: '1234567890123456', + traceFlags: TRACE_FLAG_SAMPLED, + }; + }, transaction: { getDynamicSamplingContext: () => ({ environment: 'production', @@ -71,8 +78,13 @@ describe('getTracingMetaTags', () => { // @ts-expect-error - only passing a partial span object { isRecording: () => true, - traceId: '12345678901234567890123456789012', - spanId: '1234567890123456', + spanContext: () => { + return { + traceId: '12345678901234567890123456789012', + spanId: '1234567890123456', + traceFlags: TRACE_FLAG_SAMPLED, + }; + }, transaction: undefined, }, mockedScope, @@ -84,7 +96,7 @@ describe('getTracingMetaTags', () => { }); }); - it('returns only the `sentry-trace` tag if no DSC is available', () => { + it('returns only the `sentry-trace` tag if no DSC is available without a client', () => { vi.spyOn(SentryCore, 'getDynamicSamplingContextFromClient').mockReturnValueOnce({ trace_id: '', public_key: undefined, @@ -94,8 +106,13 @@ describe('getTracingMetaTags', () => { // @ts-expect-error - only passing a partial span object { isRecording: () => true, - traceId: '12345678901234567890123456789012', - spanId: '1234567890123456', + spanContext: () => { + return { + traceId: '12345678901234567890123456789012', + spanId: '1234567890123456', + traceFlags: TRACE_FLAG_SAMPLED, + }; + }, transaction: undefined, }, mockedScope, diff --git a/packages/browser/test/unit/profiling/hubextensions.test.ts b/packages/browser/test/unit/profiling/hubextensions.test.ts index d0f9d488ec91..30b7769e836d 100644 --- a/packages/browser/test/unit/profiling/hubextensions.test.ts +++ b/packages/browser/test/unit/profiling/hubextensions.test.ts @@ -10,6 +10,9 @@ import { JSDOM } from 'jsdom'; import { onProfilingStartRouteTransaction } from '../../../src'; +// eslint-disable-next-line no-bitwise +const TraceFlagSampled = 0x1 << 0; + // @ts-expect-error store a reference so we can reset it later const globalDocument = global.document; // @ts-expect-error store a reference so we can reset it later @@ -67,9 +70,17 @@ describe('BrowserProfilingIntegration', () => { // @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 = { isRecording: () => true } as Transaction; + const mockTransaction = { + isRecording: () => true, + spanContext: () => ({ + traceId: '12345678901234567890123456789012', + spanId: '1234567890123456', + traceFlags: TraceFlagSampled, + }), + } as Transaction; expect(() => onProfilingStartRouteTransaction(mockTransaction)).not.toThrow(); }); + it('does not throw if constructor throws', () => { const spy = jest.fn(); @@ -80,8 +91,15 @@ describe('BrowserProfilingIntegration', () => { } } - // set isRecording to true so that profiling does not early return - const mockTransaction = { isRecording: () => true } as Transaction; + // set sampled to true so that profiling does not early return + const mockTransaction = { + isRecording: () => true, + spanContext: () => ({ + traceId: '12345678901234567890123456789012', + spanId: '1234567890123456', + traceFlags: TraceFlagSampled, + }), + } as Transaction; // @ts-expect-error override with our own constructor global.window.Profiler = Profiler; diff --git a/packages/bun/test/integrations/bunserver.test.ts b/packages/bun/test/integrations/bunserver.test.ts index 4fe00180377c..a6f811ba3679 100644 --- a/packages/bun/test/integrations/bunserver.test.ts +++ b/packages/bun/test/integrations/bunserver.test.ts @@ -78,7 +78,7 @@ describe('Bun Serve Integration', () => { const SENTRY_BAGGAGE_HEADER = 'sentry-version=1.0,sentry-environment=production'; client.on('finishTransaction', transaction => { - expect(transaction.traceId).toBe(TRACE_ID); + expect(transaction.spanContext().traceId).toBe(TRACE_ID); expect(transaction.parentSpanId).toBe(PARENT_SPAN_ID); expect(transaction.isRecording()).toBe(true); diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 20b6a600a593..82cbe5dadf6e 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -78,6 +78,7 @@ export { handleCallbackErrors } from './utils/handleCallbackErrors'; export { spanToTraceHeader, spanToJSON, + spanIsSampled, } from './utils/spanUtils'; export { DEFAULT_ENVIRONMENT } from './constants'; export { ModuleMetadata } from './integrations/metadata'; diff --git a/packages/core/src/tracing/idletransaction.ts b/packages/core/src/tracing/idletransaction.ts index fdc1813e5b50..16e5fbcc61e8 100644 --- a/packages/core/src/tracing/idletransaction.ts +++ b/packages/core/src/tracing/idletransaction.ts @@ -45,18 +45,18 @@ export class IdleTransactionSpanRecorder extends SpanRecorder { public add(span: Span): void { // We should make sure we do not push and pop activities for // the transaction that this span recorder belongs to. - if (span.spanId !== this.transactionSpanId) { + if (span.spanContext().spanId !== this.transactionSpanId) { // We patch span.end() to pop an activity after setting an endTimestamp. // eslint-disable-next-line @typescript-eslint/unbound-method const originalEnd = span.end; span.end = (...rest: unknown[]) => { - this._popActivity(span.spanId); + this._popActivity(span.spanContext().spanId); return originalEnd.apply(span, rest); }; // We should only push new activities if the span does not have an end timestamp. if (span.endTimestamp === undefined) { - this._pushActivity(span.spanId); + this._pushActivity(span.spanContext().spanId); } } @@ -123,7 +123,7 @@ export class IdleTransaction extends Transaction { if (_onScope) { // We set the transaction here on the scope so error events pick up the trace // context and attach it to the error. - DEBUG_BUILD && logger.log(`Setting idle transaction on scope. Span ID: ${this.spanId}`); + DEBUG_BUILD && logger.log(`Setting idle transaction on scope. Span ID: ${this.spanContext().spanId}`); _idleHub.getScope().setSpan(this); } @@ -158,7 +158,7 @@ export class IdleTransaction extends Transaction { this.spanRecorder.spans = this.spanRecorder.spans.filter((span: Span) => { // If we are dealing with the transaction itself, we just return it - if (span.spanId === this.spanId) { + if (span.spanContext().spanId === this.spanContext().spanId) { return true; } @@ -233,7 +233,7 @@ export class IdleTransaction extends Transaction { this._popActivity(id); }; - this.spanRecorder = new IdleTransactionSpanRecorder(pushActivity, popActivity, this.spanId, maxlen); + this.spanRecorder = new IdleTransactionSpanRecorder(pushActivity, popActivity, this.spanContext().spanId, maxlen); // Start heartbeat so that transactions do not run forever. DEBUG_BUILD && logger.log('Starting heartbeat'); diff --git a/packages/core/src/tracing/span.ts b/packages/core/src/tracing/span.ts index a5405ebe0fbf..e225d6c7d17f 100644 --- a/packages/core/src/tracing/span.ts +++ b/packages/core/src/tracing/span.ts @@ -6,6 +6,7 @@ import type { SpanAttributeValue, SpanAttributes, SpanContext, + SpanContextData, SpanJSON, SpanOrigin, SpanTimeInput, @@ -15,7 +16,13 @@ import type { import { dropUndefinedKeys, logger, timestampInSeconds, uuid4 } from '@sentry/utils'; import { DEBUG_BUILD } from '../debug-build'; -import { spanTimeInputToSeconds, spanToTraceContext, spanToTraceHeader } from '../utils/spanUtils'; +import { + TRACE_FLAG_NONE, + TRACE_FLAG_SAMPLED, + spanTimeInputToSeconds, + spanToTraceContext, + spanToTraceHeader, +} from '../utils/spanUtils'; /** * Keeps track of finished spans for a given transaction @@ -52,16 +59,6 @@ export class SpanRecorder { * Span contains all data about a span */ export class Span implements SpanInterface { - /** - * @inheritDoc - */ - public traceId: string; - - /** - * @inheritDoc - */ - public spanId: string; - /** * @inheritDoc */ @@ -72,11 +69,6 @@ export class Span implements SpanInterface { */ public status?: SpanStatusType | string; - /** - * @inheritDoc - */ - public sampled?: boolean; - /** * Timestamp in seconds when the span was created. */ @@ -133,6 +125,10 @@ export class Span implements SpanInterface { */ public origin?: SpanOrigin; + protected _traceId: string; + protected _spanId: string; + protected _sampled: boolean | undefined; + /** * You should never call the constructor manually, always use `Sentry.startTransaction()` * or call `startChild()` on an existing span. @@ -141,8 +137,8 @@ export class Span implements SpanInterface { * @hidden */ public constructor(spanContext: SpanContext = {}) { - this.traceId = spanContext.traceId || uuid4(); - this.spanId = spanContext.spanId || uuid4().substring(16); + this._traceId = spanContext.traceId || uuid4(); + this._spanId = spanContext.spanId || uuid4().substring(16); this.startTimestamp = spanContext.startTimestamp || timestampInSeconds(); this.tags = spanContext.tags || {}; this.data = spanContext.data || {}; @@ -155,8 +151,7 @@ export class Span implements SpanInterface { } // We want to include booleans as well here if ('sampled' in spanContext) { - // eslint-disable-next-line deprecation/deprecation - this.sampled = spanContext.sampled; + this._sampled = spanContext.sampled; } if (spanContext.op) { this.op = spanContext.op; @@ -175,6 +170,9 @@ export class Span implements SpanInterface { } } + // This conflicts with another eslint rule :( + /* eslint-disable @typescript-eslint/member-ordering */ + /** An alias for `description` of the Span. */ public get name(): string { return this.description || ''; @@ -186,6 +184,66 @@ export class Span implements SpanInterface { this.updateName(name); } + /** + * The ID of the trace. + * @deprecated Use `spanContext().traceId` instead. + */ + public get traceId(): string { + return this._traceId; + } + + /** + * The ID of the trace. + * @deprecated You cannot update the traceId of a span after span creation. + */ + public set traceId(traceId: string) { + this._traceId = traceId; + } + + /** + * The ID of the span. + * @deprecated Use `spanContext().spanId` instead. + */ + public get spanId(): string { + return this._spanId; + } + + /** + * The ID of the span. + * @deprecated You cannot update the spanId of a span after span creation. + */ + public set spanId(spanId: string) { + this._spanId = spanId; + } + + /** + * Was this span chosen to be sent as part of the sample? + * @deprecated Use `isRecording()` instead. + */ + public get sampled(): boolean | undefined { + return this._sampled; + } + + /** + * Was this span chosen to be sent as part of the sample? + * @deprecated You cannot update the sampling decision of a span after span creation. + */ + public set sampled(sampled: boolean | undefined) { + this._sampled = sampled; + } + + /* eslint-enable @typescript-eslint/member-ordering */ + + /** @inheritdoc */ + public spanContext(): SpanContextData { + const { _spanId: spanId, _traceId: traceId, _sampled: sampled } = this; + return { + spanId, + traceId, + traceFlags: sampled ? TRACE_FLAG_SAMPLED : TRACE_FLAG_NONE, + }; + } + /** * Creates a new `Span` while setting the current `Span.id` as `parentSpanId`. * Also the `sampled` decision will be inherited. @@ -197,10 +255,9 @@ export class Span implements SpanInterface { ): Span { const childSpan = new Span({ ...spanContext, - parentSpanId: this.spanId, - // eslint-disable-next-line deprecation/deprecation - sampled: this.sampled, - traceId: this.traceId, + parentSpanId: this._spanId, + sampled: this._sampled, + traceId: this._traceId, }); childSpan.spanRecorder = this.spanRecorder; @@ -213,10 +270,10 @@ export class Span implements SpanInterface { if (DEBUG_BUILD && childSpan.transaction) { const opStr = (spanContext && spanContext.op) || '< unknown op >'; const nameStr = childSpan.transaction.name || '< unknown name >'; - const idStr = childSpan.transaction.spanId; + const idStr = childSpan.transaction.spanContext().spanId; const logMessage = `[Tracing] Starting '${opStr}' span on transaction '${nameStr}' (${idStr}).`; - childSpan.transaction.metadata.spanMetadata[childSpan.spanId] = { logMessage }; + childSpan.transaction.metadata.spanMetadata[childSpan.spanContext().spanId] = { logMessage }; logger.log(logMessage); } @@ -311,9 +368,9 @@ export class Span implements SpanInterface { DEBUG_BUILD && // Don't call this for transactions this.transaction && - this.transaction.spanId !== this.spanId + this.transaction.spanContext().spanId !== this._spanId ) { - const { logMessage } = this.transaction.metadata.spanMetadata[this.spanId]; + const { logMessage } = this.transaction.metadata.spanMetadata[this._spanId]; if (logMessage) { logger.log((logMessage as string).replace('Starting', 'Finishing')); } @@ -339,12 +396,12 @@ export class Span implements SpanInterface { endTimestamp: this.endTimestamp, op: this.op, parentSpanId: this.parentSpanId, - sampled: this.sampled, - spanId: this.spanId, + sampled: this._sampled, + spanId: this._spanId, startTimestamp: this.startTimestamp, status: this.status, tags: this.tags, - traceId: this.traceId, + traceId: this._traceId, }); } @@ -357,13 +414,12 @@ export class Span implements SpanInterface { this.endTimestamp = spanContext.endTimestamp; this.op = spanContext.op; this.parentSpanId = spanContext.parentSpanId; - // eslint-disable-next-line deprecation/deprecation - this.sampled = spanContext.sampled; - this.spanId = spanContext.spanId || this.spanId; + this._sampled = spanContext.sampled; + this._spanId = spanContext.spanId || this._spanId; this.startTimestamp = spanContext.startTimestamp || this.startTimestamp; this.status = spanContext.status; this.tags = spanContext.tags || {}; - this.traceId = spanContext.traceId || this.traceId; + this._traceId = spanContext.traceId || this._traceId; return this; } @@ -384,19 +440,19 @@ export class Span implements SpanInterface { description: this.description, op: this.op, parent_span_id: this.parentSpanId, - span_id: this.spanId, + span_id: this._spanId, start_timestamp: this.startTimestamp, status: this.status, tags: Object.keys(this.tags).length > 0 ? this.tags : undefined, timestamp: this.endTimestamp, - trace_id: this.traceId, + trace_id: this._traceId, origin: this.origin, }); } /** @inheritdoc */ public isRecording(): boolean { - return !this.endTimestamp && !!this.sampled; + return !this.endTimestamp && !!this._sampled; } /** diff --git a/packages/core/src/tracing/transaction.ts b/packages/core/src/tracing/transaction.ts index 60ac3eae074b..9747d48fade6 100644 --- a/packages/core/src/tracing/transaction.ts +++ b/packages/core/src/tracing/transaction.ts @@ -200,8 +200,10 @@ export class Transaction extends SpanClass implements TransactionInterface { if (!client) return {}; + const { _traceId: traceId, _sampled: sampled } = this; + const scope = hub.getScope(); - const dsc = getDynamicSamplingContextFromClient(this.traceId, client, scope); + const dsc = getDynamicSamplingContextFromClient(traceId, client, scope); const maybeSampleRate = this.metadata.sampleRate; if (maybeSampleRate !== undefined) { @@ -214,8 +216,8 @@ export class Transaction extends SpanClass implements TransactionInterface { dsc.transaction = this.name; } - if (this.sampled !== undefined) { - dsc.sampled = String(this.sampled); + if (sampled !== undefined) { + dsc.sampled = String(sampled); } // Uncomment if we want to make DSC immutable @@ -256,7 +258,7 @@ export class Transaction extends SpanClass implements TransactionInterface { client.emit('finishTransaction', this); } - if (this.sampled !== true) { + if (this._sampled !== true) { // At this point if `sampled !== true` we want to discard the transaction. DEBUG_BUILD && logger.log('[Tracing] Discarding transaction because its trace was not chosen to be sampled.'); diff --git a/packages/core/src/utils/spanUtils.ts b/packages/core/src/utils/spanUtils.ts index 9ecb98a6c990..125610f04bde 100644 --- a/packages/core/src/utils/spanUtils.ts +++ b/packages/core/src/utils/spanUtils.ts @@ -2,11 +2,15 @@ import type { Span, SpanJSON, SpanTimeInput, TraceContext } from '@sentry/types' import { dropUndefinedKeys, generateSentryTraceHeader, timestampInSeconds } from '@sentry/utils'; import type { Span as SpanClass } from '../tracing/span'; +// These are aligned with OpenTelemetry trace flags +export const TRACE_FLAG_NONE = 0x0; +export const TRACE_FLAG_SAMPLED = 0x1; + /** * Convert a span to a trace context, which can be sent as the `trace` context in an event. */ export function spanToTraceContext(span: Span): TraceContext { - const { spanId: span_id, traceId: trace_id } = span; + const { spanId: span_id, traceId: trace_id } = span.spanContext(); const { data, description, op, parent_span_id, status, tags, origin } = spanToJSON(span); return dropUndefinedKeys({ @@ -26,7 +30,9 @@ export function spanToTraceContext(span: Span): TraceContext { * Convert a Span to a Sentry trace header. */ export function spanToTraceHeader(span: Span): string { - return generateSentryTraceHeader(span.traceId, span.spanId, span.isRecording()); + const { traceId, spanId } = span.spanContext(); + const sampled = spanIsSampled(span); + return generateSentryTraceHeader(traceId, spanId, sampled); } /** @@ -88,3 +94,17 @@ export function spanToJSON(span: Span): Partial { function spanIsSpanClass(span: Span): span is SpanClass { return typeof (span as SpanClass).getSpanJSON === 'function'; } + +/** + * Returns true if a span is sampled. + * In most cases, you should just use `span.isRecording()` instead. + * However, this has a slightly different semantic, as it also returns false if the span is finished. + * So in the case where this distinction is important, use this method. + */ +export function spanIsSampled(span: Span): boolean { + // We align our trace flags with the ones OpenTelemetry use + // So we also check for sampled the same way they do. + const { traceFlags } = span.spanContext(); + // eslint-disable-next-line no-bitwise + return Boolean(traceFlags & TRACE_FLAG_SAMPLED); +} diff --git a/packages/core/test/lib/tracing/span.test.ts b/packages/core/test/lib/tracing/span.test.ts index 1d36b237175a..6626077956e3 100644 --- a/packages/core/test/lib/tracing/span.test.ts +++ b/packages/core/test/lib/tracing/span.test.ts @@ -1,5 +1,6 @@ import { timestampInSeconds } from '@sentry/utils'; import { Span } from '../../../src'; +import { TRACE_FLAG_NONE, TRACE_FLAG_SAMPLED } from '../../../src/utils/spanUtils'; describe('span', () => { it('works with name', () => { @@ -226,6 +227,35 @@ describe('span', () => { }); }); + describe('spanContext', () => { + it('works with default span', () => { + const span = new Span(); + expect(span.spanContext()).toEqual({ + spanId: span['_spanId'], + traceId: span['_traceId'], + traceFlags: TRACE_FLAG_NONE, + }); + }); + + it('works sampled span', () => { + const span = new Span({ sampled: true }); + expect(span.spanContext()).toEqual({ + spanId: span['_spanId'], + traceId: span['_traceId'], + traceFlags: TRACE_FLAG_SAMPLED, + }); + }); + + it('works unsampled span', () => { + const span = new Span({ sampled: false }); + expect(span.spanContext()).toEqual({ + spanId: span['_spanId'], + traceId: span['_traceId'], + traceFlags: TRACE_FLAG_NONE, + }); + }); + }); + // Ensure that attributes & data are merged together describe('_getData', () => { it('works without data & attributes', () => { diff --git a/packages/core/test/lib/utils/spanUtils.test.ts b/packages/core/test/lib/utils/spanUtils.test.ts index d4b0afc7a7a6..e521df7c2dc9 100644 --- a/packages/core/test/lib/utils/spanUtils.test.ts +++ b/packages/core/test/lib/utils/spanUtils.test.ts @@ -1,6 +1,6 @@ import { TRACEPARENT_REGEXP, timestampInSeconds } from '@sentry/utils'; import { Span, spanToTraceHeader } from '../../../src'; -import { spanTimeInputToSeconds, spanToJSON } from '../../../src/utils/spanUtils'; +import { spanIsSampled, spanTimeInputToSeconds, spanToJSON } from '../../../src/utils/spanUtils'; describe('spanToTraceHeader', () => { test('simple', () => { @@ -51,8 +51,8 @@ describe('spanToJSON', () => { it('works with a simple span', () => { const span = new Span(); expect(spanToJSON(span)).toEqual({ - span_id: span.spanId, - trace_id: span.traceId, + span_id: span.spanContext().spanId, + trace_id: span.spanContext().traceId, origin: 'manual', start_timestamp: span.startTimestamp, }); @@ -115,3 +115,15 @@ describe('spanToJSON', () => { expect(spanToJSON(span as unknown as Span)).toEqual({}); }); }); + +describe('spanIsSampled', () => { + test('sampled', () => { + const span = new Span({ sampled: true }); + expect(spanIsSampled(span)).toBe(true); + }); + + test('not sampled', () => { + const span = new Span({ sampled: false }); + expect(spanIsSampled(span)).toBe(false); + }); +}); diff --git a/packages/hub/test/scope.test.ts b/packages/hub/test/scope.test.ts index 4cb8694b9cca..505a523c1942 100644 --- a/packages/hub/test/scope.test.ts +++ b/packages/hub/test/scope.test.ts @@ -361,6 +361,7 @@ describe('Scope', () => { const scope = new Scope(); const span = { fake: 'span', + spanContext: () => ({}), toJSON: () => ({ origin: 'manual' }), } as any; scope.setSpan(span); @@ -374,6 +375,7 @@ describe('Scope', () => { const scope = new Scope(); const span = { fake: 'span', + spanContext: () => ({}), toJSON: () => ({ a: 'b' }), } as any; scope.setSpan(span); @@ -392,6 +394,7 @@ describe('Scope', () => { const scope = new Scope(); const transaction = { fake: 'span', + spanContext: () => ({}), toJSON: () => ({ a: 'b' }), name: 'fake transaction', getDynamicSamplingContext: () => ({}), @@ -407,9 +410,15 @@ describe('Scope', () => { test('adds `transaction` tag when span on scope', async () => { expect.assertions(1); const scope = new Scope(); - const transaction = { name: 'fake transaction', getDynamicSamplingContext: () => ({}) }; + const transaction = { + name: 'fake transaction', + spanContext: () => ({}), + toJSON: () => ({ description: 'fake transaction' }), + getDynamicSamplingContext: () => ({}), + }; const span = { fake: 'span', + spanContext: () => ({}), toJSON: () => ({ a: 'b' }), transaction, } as any; diff --git a/packages/node/test/handlers.test.ts b/packages/node/test/handlers.test.ts index ed5592cef5e0..0eb1237053ad 100644 --- a/packages/node/test/handlers.test.ts +++ b/packages/node/test/handlers.test.ts @@ -449,7 +449,7 @@ describe('tracingHandler', () => { expect(finishTransaction).toHaveBeenCalled(); expect(span.endTimestamp).toBeLessThanOrEqual(transaction.endTimestamp!); expect(sentEvent.spans?.length).toEqual(1); - expect(sentEvent.spans?.[0].spanId).toEqual(span.spanId); + expect(sentEvent.spans?.[0].spanContext().spanId).toEqual(span.spanContext().spanId); done(); }); }); diff --git a/packages/node/test/integrations/undici.test.ts b/packages/node/test/integrations/undici.test.ts index 1d7f847b2503..ad2c9a55ded1 100644 --- a/packages/node/test/integrations/undici.test.ts +++ b/packages/node/test/integrations/undici.test.ts @@ -221,7 +221,9 @@ conditionalTest({ min: 16 })('Undici integration', () => { expect(requestHeaders['sentry-trace']).toEqual(spanToTraceHeader(span!)); expect(requestHeaders['baggage']).toEqual( - `sentry-environment=production,sentry-public_key=0,sentry-trace_id=${span.traceId},sentry-sample_rate=1,sentry-transaction=test-transaction`, + `sentry-environment=production,sentry-public_key=0,sentry-trace_id=${ + span.spanContext().traceId + },sentry-sample_rate=1,sentry-transaction=test-transaction`, ); }); }); diff --git a/packages/opentelemetry-node/src/utils/spanMap.ts b/packages/opentelemetry-node/src/utils/spanMap.ts index 8fe43222e93a..eee8e923ccbf 100644 --- a/packages/opentelemetry-node/src/utils/spanMap.ts +++ b/packages/opentelemetry-node/src/utils/spanMap.ts @@ -31,7 +31,7 @@ export function getSentrySpan(spanId: string): SentrySpan | undefined { export function setSentrySpan(spanId: string, sentrySpan: SentrySpan): void { let ref: SpanRefType = SPAN_REF_ROOT; - const rootSpanId = sentrySpan.transaction?.spanId; + const rootSpanId = sentrySpan.transaction?.spanContext().spanId; if (rootSpanId && rootSpanId !== spanId) { const root = SPAN_MAP.get(rootSpanId); diff --git a/packages/opentelemetry-node/test/propagator.test.ts b/packages/opentelemetry-node/test/propagator.test.ts index 24e30b1e2bc0..6b2ce11fb69e 100644 --- a/packages/opentelemetry-node/test/propagator.test.ts +++ b/packages/opentelemetry-node/test/propagator.test.ts @@ -61,13 +61,13 @@ describe('SentryPropagator', () => { function createTransactionAndMaybeSpan(type: PerfType, transactionContext: TransactionContext) { const transaction = new Transaction(transactionContext, hub); - setSentrySpan(transaction.spanId, transaction); + setSentrySpan(transaction.spanContext().spanId, transaction); if (type === PerfType.Span) { // eslint-disable-next-line @typescript-eslint/no-unused-vars const { spanId, ...ctx } = transactionContext; // eslint-disable-next-line deprecation/deprecation const span = transaction.startChild({ ...ctx, description: transaction.name }); - setSentrySpan(span.spanId, span); + setSentrySpan(span.spanContext().spanId, span); } } diff --git a/packages/opentelemetry-node/test/spanprocessor.test.ts b/packages/opentelemetry-node/test/spanprocessor.test.ts index e9eb7b76f4db..fd35899f5bbc 100644 --- a/packages/opentelemetry-node/test/spanprocessor.test.ts +++ b/packages/opentelemetry-node/test/spanprocessor.test.ts @@ -83,9 +83,9 @@ describe('SentrySpanProcessor', () => { expect(sentrySpanTransaction?.name).toBe('GET /users'); expect(sentrySpanTransaction?.startTimestamp).toEqual(startTimestampMs / 1000); - expect(sentrySpanTransaction?.traceId).toEqual(otelSpan.spanContext().traceId); + expect(sentrySpanTransaction?.spanContext().traceId).toEqual(otelSpan.spanContext().traceId); expect(sentrySpanTransaction?.parentSpanId).toEqual(otelSpan.parentSpanId); - expect(sentrySpanTransaction?.spanId).toEqual(otelSpan.spanContext().spanId); + expect(sentrySpanTransaction?.spanContext().spanId).toEqual(otelSpan.spanContext().spanId); otelSpan.end(endTime); @@ -111,8 +111,8 @@ describe('SentrySpanProcessor', () => { expect(sentrySpan).toBeInstanceOf(SentrySpan); expect(sentrySpan?.description).toBe('SELECT * FROM users;'); expect(sentrySpan?.startTimestamp).toEqual(startTimestampMs / 1000); - expect(sentrySpan?.spanId).toEqual(childOtelSpan.spanContext().spanId); - expect(sentrySpan?.parentSpanId).toEqual(sentrySpanTransaction?.spanId); + expect(sentrySpan?.spanContext().spanId).toEqual(childOtelSpan.spanContext().spanId); + expect(sentrySpan?.parentSpanId).toEqual(sentrySpanTransaction?.spanContext().spanId); expect(hub.getScope().getSpan()).toBeUndefined(); @@ -151,7 +151,7 @@ describe('SentrySpanProcessor', () => { 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?.spanContext().spanId).toEqual(childOtelSpan.spanContext().spanId); expect(sentrySpan?.parentSpanId).toEqual(parentOtelSpan.spanContext().spanId); expect(hub.getScope().getSpan()).toBeUndefined(); @@ -183,9 +183,9 @@ describe('SentrySpanProcessor', () => { const sentrySpan2 = getSpanForOtelSpan(span2); const sentrySpan3 = getSpanForOtelSpan(span3); - expect(sentrySpan1?.parentSpanId).toEqual(sentrySpanTransaction?.spanId); - expect(sentrySpan2?.parentSpanId).toEqual(sentrySpanTransaction?.spanId); - expect(sentrySpan3?.parentSpanId).toEqual(sentrySpanTransaction?.spanId); + expect(sentrySpan1?.parentSpanId).toEqual(sentrySpanTransaction?.spanContext().spanId); + expect(sentrySpan2?.parentSpanId).toEqual(sentrySpanTransaction?.spanContext().spanId); + expect(sentrySpan3?.parentSpanId).toEqual(sentrySpanTransaction?.spanContext().spanId); expect(sentrySpan1?.description).toEqual('SELECT * FROM users;'); expect(sentrySpan2?.description).toEqual('SELECT * FROM companies;'); @@ -245,7 +245,7 @@ describe('SentrySpanProcessor', () => { expect(parentSpan?.endTimestamp).toBeDefined(); expect(childSpan?.endTimestamp).toBeDefined(); expect(parentSpan?.parentSpanId).toBeUndefined(); - expect(childSpan?.parentSpanId).toEqual(parentSpan?.spanId); + expect(childSpan?.parentSpanId).toEqual(parentSpan?.spanContext().spanId); }); }); }); diff --git a/packages/opentelemetry/test/custom/transaction.test.ts b/packages/opentelemetry/test/custom/transaction.test.ts index 5377371b3077..a088a082b41f 100644 --- a/packages/opentelemetry/test/custom/transaction.test.ts +++ b/packages/opentelemetry/test/custom/transaction.test.ts @@ -17,8 +17,7 @@ describe('NodeExperimentalTransaction', () => { const hub = getCurrentHub(); hub.bindClient(client); - const transaction = new OpenTelemetryTransaction({ name: 'test' }, hub); - transaction.sampled = true; + const transaction = new OpenTelemetryTransaction({ name: 'test', sampled: true }, hub); const res = transaction.finishWithScope(); @@ -64,8 +63,7 @@ describe('NodeExperimentalTransaction', () => { const hub = getCurrentHub(); hub.bindClient(client); - const transaction = new OpenTelemetryTransaction({ name: 'test', startTimestamp: 123456 }, hub); - transaction.sampled = true; + const transaction = new OpenTelemetryTransaction({ name: 'test', startTimestamp: 123456, sampled: true }, hub); const res = transaction.finishWithScope(1234567); @@ -89,8 +87,7 @@ describe('NodeExperimentalTransaction', () => { const hub = getCurrentHub(); hub.bindClient(client); - const transaction = new OpenTelemetryTransaction({ name: 'test', startTimestamp: 123456 }, hub); - transaction.sampled = true; + const transaction = new OpenTelemetryTransaction({ name: 'test', startTimestamp: 123456, sampled: true }, hub); const scope = new OpenTelemetryScope(); scope.setTags({ @@ -149,8 +146,7 @@ describe('startTranscation', () => { const transaction = startTransaction(hub, { name: 'test' }); expect(transaction).toBeInstanceOf(OpenTelemetryTransaction); - - expect(transaction.sampled).toBe(undefined); + expect(transaction['_sampled']).toBe(undefined); expect(transaction.spanRecorder).toBeDefined(); expect(transaction.spanRecorder?.spans).toHaveLength(1); expect(transaction.metadata).toEqual({ diff --git a/packages/tracing-internal/src/browser/browsertracing.ts b/packages/tracing-internal/src/browser/browsertracing.ts index d52d0e306379..09209e2655c1 100644 --- a/packages/tracing-internal/src/browser/browsertracing.ts +++ b/packages/tracing-internal/src/browser/browsertracing.ts @@ -1,6 +1,12 @@ /* eslint-disable max-lines */ import type { Hub, IdleTransaction } from '@sentry/core'; -import { TRACING_DEFAULTS, addTracingExtensions, getActiveTransaction, startIdleTransaction } from '@sentry/core'; +import { + TRACING_DEFAULTS, + addTracingExtensions, + getActiveTransaction, + spanIsSampled, + startIdleTransaction, +} from '@sentry/core'; import type { EventProcessor, Integration, Transaction, TransactionContext, TransactionSource } from '@sentry/types'; import { getDomElement, logger, tracingContextFromHeaders } from '@sentry/utils'; @@ -362,10 +368,10 @@ export class BrowserTracing implements Integration { // Navigation transactions should set a new propagation context based on the // created idle transaction. scope.setPropagationContext({ - traceId: idleTransaction.traceId, - spanId: idleTransaction.spanId, + traceId: idleTransaction.spanContext().traceId, + spanId: idleTransaction.spanContext().spanId, parentSpanId: idleTransaction.parentSpanId, - sampled: idleTransaction.sampled, + sampled: spanIsSampled(idleTransaction), }); } diff --git a/packages/tracing-internal/src/browser/request.ts b/packages/tracing-internal/src/browser/request.ts index 3adbda65cbb4..655a6e803306 100644 --- a/packages/tracing-internal/src/browser/request.ts +++ b/packages/tracing-internal/src/browser/request.ts @@ -289,7 +289,7 @@ export function xhrCallback( : undefined; if (span) { - xhr.__sentry_xhr_span_id__ = span.spanId; + xhr.__sentry_xhr_span_id__ = span.spanContext().spanId; spans[xhr.__sentry_xhr_span_id__] = span; } diff --git a/packages/tracing-internal/src/common/fetch.ts b/packages/tracing-internal/src/common/fetch.ts index bb7ff90ea21c..47cf443f2cb5 100644 --- a/packages/tracing-internal/src/common/fetch.ts +++ b/packages/tracing-internal/src/common/fetch.ts @@ -93,8 +93,8 @@ export function instrumentFetchRequest( : undefined; if (span) { - handlerData.fetchData.__span = span.spanId; - spans[span.spanId] = span; + handlerData.fetchData.__span = span.spanContext().spanId; + spans[span.spanContext().spanId] = span; } if (shouldAttachHeaders(handlerData.fetchData.url) && client) { diff --git a/packages/tracing/test/idletransaction.test.ts b/packages/tracing/test/idletransaction.test.ts index 9d82eb474af3..729b63074700 100644 --- a/packages/tracing/test/idletransaction.test.ts +++ b/packages/tracing/test/idletransaction.test.ts @@ -115,7 +115,7 @@ describe('IdleTransaction', () => { getCurrentScope().setSpan(transaction); const span = startInactiveSpan({ name: 'inner' })!; - expect(transaction.activities).toMatchObject({ [span.spanId]: true }); + expect(transaction.activities).toMatchObject({ [span.spanContext().spanId]: true }); expect(mockFinish).toHaveBeenCalledTimes(0); @@ -145,12 +145,15 @@ describe('IdleTransaction', () => { startSpanManual({ name: 'inner1' }, span => { const childSpan = startInactiveSpan({ name: 'inner2' })!; - expect(transaction.activities).toMatchObject({ [span!.spanId]: true, [childSpan.spanId]: true }); + expect(transaction.activities).toMatchObject({ + [span!.spanContext().spanId]: true, + [childSpan.spanContext().spanId]: true, + }); span?.end(); jest.advanceTimersByTime(TRACING_DEFAULTS.idleTimeout + 1); expect(mockFinish).toHaveBeenCalledTimes(0); - expect(transaction.activities).toMatchObject({ [childSpan.spanId]: true }); + expect(transaction.activities).toMatchObject({ [childSpan.spanContext().spanId]: true }); }); }); @@ -196,14 +199,14 @@ describe('IdleTransaction', () => { if (transaction.spanRecorder) { const spans = transaction.spanRecorder.spans; expect(spans).toHaveLength(3); - expect(spans[0].spanId).toBe(transaction.spanId); + expect(spans[0].spanContext().spanId).toBe(transaction.spanContext().spanId); // Regular Span - should not modified - expect(spans[1].spanId).toBe(regularSpan.spanId); + expect(spans[1].spanContext().spanId).toBe(regularSpan.spanContext().spanId); expect(spans[1].endTimestamp).not.toBe(transaction.endTimestamp); // Cancelled Span - has endtimestamp of transaction - expect(spans[2].spanId).toBe(cancelledSpan.spanId); + expect(spans[2].spanContext().spanId).toBe(cancelledSpan.spanContext().spanId); expect(spans[2].status).toBe('cancelled'); expect(spans[2].endTimestamp).toBe(transaction.endTimestamp); } @@ -482,13 +485,13 @@ describe('IdleTransactionSpanRecorder', () => { expect(spanRecorder.spans).toHaveLength(1); expect(mockPushActivity).toHaveBeenCalledTimes(1); - expect(mockPushActivity).toHaveBeenLastCalledWith(span.spanId); + expect(mockPushActivity).toHaveBeenLastCalledWith(span.spanContext().spanId); expect(mockPopActivity).toHaveBeenCalledTimes(0); span.end(); expect(mockPushActivity).toHaveBeenCalledTimes(1); expect(mockPopActivity).toHaveBeenCalledTimes(1); - expect(mockPushActivity).toHaveBeenLastCalledWith(span.spanId); + expect(mockPushActivity).toHaveBeenLastCalledWith(span.spanContext().spanId); }); it('does not push activities if a span has a timestamp', () => { @@ -507,7 +510,12 @@ describe('IdleTransactionSpanRecorder', () => { const mockPopActivity = jest.fn(); const transaction = new IdleTransaction({ name: 'foo' }, hub); - const spanRecorder = new IdleTransactionSpanRecorder(mockPushActivity, mockPopActivity, transaction.spanId, 10); + const spanRecorder = new IdleTransactionSpanRecorder( + mockPushActivity, + mockPopActivity, + transaction.spanContext().spanId, + 10, + ); spanRecorder.add(transaction); expect(mockPushActivity).toHaveBeenCalledTimes(0); diff --git a/packages/tracing/test/span.test.ts b/packages/tracing/test/span.test.ts index 011af5ba10d5..8413b975de03 100644 --- a/packages/tracing/test/span.test.ts +++ b/packages/tracing/test/span.test.ts @@ -506,8 +506,8 @@ describe('Span', () => { sampled: true, }); - expect(span.traceId).toBe('c'); - expect(span.spanId).toBe('d'); + expect(span.spanContext().traceId).toBe('c'); + expect(span.spanContext().spanId).toBe('d'); expect(span.sampled).toBe(true); expect(span.description).toBe(undefined); expect(span.op).toBe(undefined); @@ -541,8 +541,8 @@ describe('Span', () => { span.updateWithContext(newContext); - expect(span.traceId).toBe('a'); - expect(span.spanId).toBe('b'); + expect(span.spanContext().traceId).toBe('a'); + expect(span.spanContext().spanId).toBe('b'); expect(span.description).toBe('new'); expect(span.endTimestamp).toBe(1); expect(span.op).toBe('new-op'); diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index d47e8407e30c..34f668275c94 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -97,6 +97,8 @@ export type { SpanAttributes, SpanTimeInput, SpanJSON, + SpanContextData, + TraceFlag, } from './span'; export type { StackFrame } from './stackframe'; export type { Stacktrace, StackParser, StackLineParser, StackLineParserFn } from './stacktrace'; diff --git a/packages/types/src/span.ts b/packages/types/src/span.ts index 86acb7d82b3a..d9562d69b4e6 100644 --- a/packages/types/src/span.ts +++ b/packages/types/src/span.ts @@ -43,6 +43,45 @@ export interface SpanJSON { origin?: SpanOrigin; } +// These are aligned with OpenTelemetry trace flags +type TraceFlagNone = 0x0; +type TraceFlagSampled = 0x1; +export type TraceFlag = TraceFlagNone | TraceFlagSampled; + +export interface SpanContextData { + /** + * The ID of the trace that this span belongs to. It is worldwide unique + * with practically sufficient probability by being made as 16 randomly + * generated bytes, encoded as a 32 lowercase hex characters corresponding to + * 128 bits. + */ + traceId: string; + + /** + * The ID of the Span. It is globally unique with practically sufficient + * probability by being made as 8 randomly generated bytes, encoded as a 16 + * lowercase hex characters corresponding to 64 bits. + */ + spanId: string; + + /** + * Only true if the SpanContext was propagated from a remote parent. + */ + isRemote?: boolean; + + /** + * Trace flags to propagate. + * + * It is represented as 1 byte (bitmap). Bit to represent whether trace is + * sampled or not. When set, the least significant bit documents that the + * caller may have recorded trace data. A caller who does not record trace + * data out-of-band leaves this flag unset. + */ + traceFlags: TraceFlag; + + // Note: we do not have traceState here, but this is optional in OpenTelemetry anyhow +} + /** Interface holding all properties that can be set on a Span on creation. */ export interface SpanContext { /** @@ -73,8 +112,6 @@ export interface SpanContext { /** * Was this span chosen to be sent as part of the sample? - * - * @deprecated Use `isRecording()` instead. */ sampled?: boolean; @@ -132,15 +169,23 @@ export interface Span extends SpanContext { name: string; /** - * @inheritDoc + * The ID of the span. + * @deprecated Use `spanContext().spanId` instead. */ spanId: string; /** - * @inheritDoc + * The ID of the trace. + * @deprecated Use `spanContext().traceId` instead. */ traceId: string; + /** + * Was this span chosen to be sent as part of the sample? + * @deprecated Use `isRecording()` instead. + */ + sampled?: boolean; + /** * @inheritDoc */ @@ -171,6 +216,12 @@ export interface Span extends SpanContext { */ instrumenter: Instrumenter; + /** + * Get context data for this span. + * This includes the spanId & the traceId. + */ + spanContext(): SpanContextData; + /** * Sets the finish timestamp on the current span. * @param endTimestamp Takes an endTimestamp if the end should not be the time when you call this function. diff --git a/packages/types/src/transaction.ts b/packages/types/src/transaction.ts index a4bee40983a5..6a5ea28e2eb2 100644 --- a/packages/types/src/transaction.ts +++ b/packages/types/src/transaction.ts @@ -44,15 +44,23 @@ export type TraceparentData = Pick { /** - * @inheritDoc + * The ID of the transaction. + * @deprecated Use `spanContext().spanId` instead. */ spanId: string; /** - * @inheritDoc + * The ID of the trace. + * @deprecated Use `spanContext().traceId` instead. */ traceId: string; + /** + * Was this transaction chosen to be sent as part of the sample? + * @deprecated Use `spanIsSampled(transaction)` instead. + */ + sampled?: boolean; + /** * @inheritDoc */ From e7552e3b0b9fd962d8abef5054e93e6d5db437e5 Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Mon, 8 Jan 2024 15:21:05 +0100 Subject: [PATCH 22/43] feat(core): Deprecate span `name` and `description` (#10056) Instead, users should use `spanToJSON(span).description`. In reality, users should mostly not actually need this - most usages are internal things, or tests, right now. While at this, I also updated the `TraceContext` type to not have a `description`. This started failing in a bunch of tests, because while in theory the trace context has a `description` field, in praxis we always used to set `description` to `undefined` for transactions, leading to this never being set. The work in this PR unifies the name/description handling between span and transaction, which lead to `description` not being undefined for transactions anymore, leading to this actually showing up in the trace context. --- MIGRATION.md | 2 + .../suites/public-api/startSpan/basic/test.ts | 1 + .../tracing/metrics/web-vitals-fid/test.ts | 1 + .../tracing/metrics/web-vitals-fp-fcp/test.ts | 2 + .../browser/src/profiling/hubextensions.ts | 15 ++-- .../bun/test/integrations/bunserver.test.ts | 6 +- packages/core/src/integrations/requestdata.ts | 7 +- packages/core/src/metrics/exports.ts | 3 +- packages/core/src/tracing/sampling.ts | 4 +- packages/core/src/tracing/span.ts | 52 +++++++----- packages/core/src/tracing/trace.ts | 13 +-- packages/core/src/tracing/transaction.ts | 27 +++--- .../core/src/utils/applyScopeDataToEvent.ts | 4 +- packages/core/src/utils/spanUtils.ts | 3 +- packages/core/test/lib/tracing/span.test.ts | 82 ++++++++++--------- .../core/test/lib/tracing/transaction.test.ts | 62 +++++++------- packages/ember/package.json | 1 + packages/ember/tests/helpers/utils.ts | 3 +- packages/hub/test/scope.test.ts | 4 +- .../nextjs/src/common/utils/wrapperUtils.ts | 2 +- packages/node/src/integrations/http.ts | 21 +++-- packages/node/test/integrations/http.test.ts | 10 +-- .../opentelemetry-node/src/spanprocessor.ts | 2 +- .../test/propagator.test.ts | 2 +- .../test/spanprocessor.test.ts | 54 +++++++----- packages/remix/src/utils/instrumentServer.ts | 6 +- packages/replay/src/replay.ts | 4 +- .../test/browser/metrics/utils.test.ts | 3 +- packages/types/src/context.ts | 1 - packages/types/src/span.ts | 3 + packages/types/src/transaction.ts | 6 ++ 31 files changed, 232 insertions(+), 174 deletions(-) diff --git a/MIGRATION.md b/MIGRATION.md index e29480fb83c5..14ab08ee8d8f 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -71,6 +71,8 @@ In v8, the Span class is heavily reworked. The following properties & methods ar * `span.sampled`: Use `span.isRecording()` instead. * `span.spanId`: Use `span.spanContext().spanId` instead. * `span.traceId`: Use `span.spanContext().traceId` instead. +* `span.name`: Use `spanToJSON(span).description` instead. +* `span.description`: Use `spanToJSON(span).description` instead. ## Deprecate `pushScope` & `popScope` in favor of `withScope` diff --git a/dev-packages/browser-integration-tests/suites/public-api/startSpan/basic/test.ts b/dev-packages/browser-integration-tests/suites/public-api/startSpan/basic/test.ts index a4b5d8b9bd02..95d09927e463 100644 --- a/dev-packages/browser-integration-tests/suites/public-api/startSpan/basic/test.ts +++ b/dev-packages/browser-integration-tests/suites/public-api/startSpan/basic/test.ts @@ -29,6 +29,7 @@ sentryTest('should report finished spans as children of the root transaction', a expect(transaction.spans).toHaveLength(1); const span_1 = transaction.spans?.[0]; + // eslint-disable-next-line deprecation/deprecation expect(span_1?.description).toBe('child_span'); expect(span_1?.parentSpanId).toEqual(rootSpanId); }); diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-fid/test.ts b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-fid/test.ts index 55e0b4d0e833..aaab7059320c 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-fid/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-fid/test.ts @@ -21,6 +21,7 @@ sentryTest('should capture a FID vital.', async ({ browserName, getLocalTestPath expect(eventData.measurements).toBeDefined(); expect(eventData.measurements?.fid?.value).toBeDefined(); + // eslint-disable-next-line deprecation/deprecation const fidSpan = eventData.spans?.filter(({ description }) => description === 'first input delay')[0]; expect(fidSpan).toBeDefined(); diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-fp-fcp/test.ts b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-fp-fcp/test.ts index 3a97c62d7f68..4914c0b45779 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-fp-fcp/test.ts +++ b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-fp-fcp/test.ts @@ -16,6 +16,7 @@ sentryTest('should capture FP vital.', async ({ browserName, getLocalTestPath, p expect(eventData.measurements).toBeDefined(); expect(eventData.measurements?.fp?.value).toBeDefined(); + // eslint-disable-next-line deprecation/deprecation const fpSpan = eventData.spans?.filter(({ description }) => description === 'first-paint')[0]; expect(fpSpan).toBeDefined(); @@ -34,6 +35,7 @@ sentryTest('should capture FCP vital.', async ({ getLocalTestPath, page }) => { expect(eventData.measurements).toBeDefined(); expect(eventData.measurements?.fcp?.value).toBeDefined(); + // eslint-disable-next-line deprecation/deprecation const fcpSpan = eventData.spans?.filter(({ description }) => description === 'first-contentful-paint')[0]; expect(fcpSpan).toBeDefined(); diff --git a/packages/browser/src/profiling/hubextensions.ts b/packages/browser/src/profiling/hubextensions.ts index 462929fff04b..01a3bfc2bfac 100644 --- a/packages/browser/src/profiling/hubextensions.ts +++ b/packages/browser/src/profiling/hubextensions.ts @@ -1,4 +1,5 @@ /* eslint-disable complexity */ +import { spanToJSON } from '@sentry/core'; import type { Transaction } from '@sentry/types'; import { logger, timestampInSeconds, uuid4 } from '@sentry/utils'; @@ -56,7 +57,7 @@ export function startProfileForTransaction(transaction: Transaction): Transactio } if (DEBUG_BUILD) { - logger.log(`[Profiling] started profiling transaction: ${transaction.name || transaction.description}`); + logger.log(`[Profiling] started profiling transaction: ${spanToJSON(transaction).description}`); } // We create "unique" transaction names to avoid concurrent transactions with same names @@ -87,11 +88,7 @@ export function startProfileForTransaction(transaction: Transaction): Transactio } if (processedProfile) { if (DEBUG_BUILD) { - logger.log( - '[Profiling] profile for:', - transaction.name || transaction.description, - 'already exists, returning early', - ); + logger.log('[Profiling] profile for:', spanToJSON(transaction).description, 'already exists, returning early'); } return null; } @@ -105,14 +102,14 @@ export function startProfileForTransaction(transaction: Transaction): Transactio } if (DEBUG_BUILD) { - logger.log(`[Profiling] stopped profiling of transaction: ${transaction.name || transaction.description}`); + logger.log(`[Profiling] stopped profiling of transaction: ${spanToJSON(transaction).description}`); } // In case of an overlapping transaction, stopProfiling may return null and silently ignore the overlapping profile. if (!profile) { if (DEBUG_BUILD) { logger.log( - `[Profiling] profiler returned null profile for: ${transaction.name || transaction.description}`, + `[Profiling] profiler returned null profile for: ${spanToJSON(transaction).description}`, 'this may indicate an overlapping transaction or a call to stopProfiling with a profile title that was never started', ); } @@ -135,7 +132,7 @@ export function startProfileForTransaction(transaction: Transaction): Transactio if (DEBUG_BUILD) { logger.log( '[Profiling] max profile duration elapsed, stopping profiling for:', - transaction.name || transaction.description, + spanToJSON(transaction).description, ); } // If the timeout exceeds, we want to stop profiling, but not finish the transaction diff --git a/packages/bun/test/integrations/bunserver.test.ts b/packages/bun/test/integrations/bunserver.test.ts index a6f811ba3679..09b3ae45aee4 100644 --- a/packages/bun/test/integrations/bunserver.test.ts +++ b/packages/bun/test/integrations/bunserver.test.ts @@ -1,5 +1,5 @@ import { beforeAll, beforeEach, describe, expect, test } from 'bun:test'; -import { Hub, makeMain } from '@sentry/core'; +import { Hub, makeMain, spanToJSON } from '@sentry/core'; import { BunClient } from '../../src/client'; import { instrumentBunServe } from '../../src/integrations/bunserver'; @@ -30,7 +30,7 @@ describe('Bun Serve Integration', () => { 'http.status_code': '200', }); expect(transaction.op).toEqual('http.server'); - expect(transaction.name).toEqual('GET /'); + expect(spanToJSON(transaction).description).toEqual('GET /'); }); const server = Bun.serve({ @@ -52,7 +52,7 @@ describe('Bun Serve Integration', () => { 'http.status_code': '200', }); expect(transaction.op).toEqual('http.server'); - expect(transaction.name).toEqual('POST /'); + expect(spanToJSON(transaction).description).toEqual('POST /'); }); const server = Bun.serve({ diff --git a/packages/core/src/integrations/requestdata.ts b/packages/core/src/integrations/requestdata.ts index fcf70ccced1a..1deab79df241 100644 --- a/packages/core/src/integrations/requestdata.ts +++ b/packages/core/src/integrations/requestdata.ts @@ -2,6 +2,7 @@ import type { Client, IntegrationFn, Transaction } from '@sentry/types'; import type { AddRequestDataToEventOptions, TransactionNamingScheme } from '@sentry/utils'; import { addRequestDataToEvent, extractPathForTransaction } from '@sentry/utils'; import { convertIntegrationFnToClass } from '../integration'; +import { spanToJSON } from '../utils/spanUtils'; export type RequestDataIntegrationOptions = { /** @@ -105,18 +106,20 @@ const requestDataIntegration: IntegrationFn = (options: RequestDataIntegrationOp const reqWithTransaction = req as { _sentryTransaction?: Transaction }; const transaction = reqWithTransaction._sentryTransaction; if (transaction) { + const name = spanToJSON(transaction).description || ''; + // TODO (v8): Remove the nextjs check and just base it on `transactionNamingScheme` for all SDKs. (We have to // keep it the way it is for the moment, because changing the names of transactions in Sentry has the potential // to break things like alert rules.) const shouldIncludeMethodInTransactionName = getSDKName(client) === 'sentry.javascript.nextjs' - ? transaction.name.startsWith('/api') + ? name.startsWith('/api') : transactionNamingScheme !== 'path'; const [transactionValue] = extractPathForTransaction(req, { path: true, method: shouldIncludeMethodInTransactionName, - customRoute: transaction.name, + customRoute: name, }); processedEvent.transaction = transactionValue; diff --git a/packages/core/src/metrics/exports.ts b/packages/core/src/metrics/exports.ts index 66074a7e846c..45759982bc14 100644 --- a/packages/core/src/metrics/exports.ts +++ b/packages/core/src/metrics/exports.ts @@ -3,6 +3,7 @@ import { logger } from '@sentry/utils'; import type { BaseClient } from '../baseclient'; import { DEBUG_BUILD } from '../debug-build'; import { getClient, getCurrentScope } from '../exports'; +import { spanToJSON } from '../utils/spanUtils'; import { COUNTER_METRIC_TYPE, DISTRIBUTION_METRIC_TYPE, GAUGE_METRIC_TYPE, SET_METRIC_TYPE } from './constants'; import { MetricsAggregator } from './integration'; import type { MetricType } from './types'; @@ -38,7 +39,7 @@ function addToMetricsAggregator( metricTags.environment = environment; } if (transaction) { - metricTags.transaction = transaction.name; + metricTags.transaction = spanToJSON(transaction).description || ''; } DEBUG_BUILD && logger.log(`Adding value of ${value} to ${metricType} metric ${name}`); diff --git a/packages/core/src/tracing/sampling.ts b/packages/core/src/tracing/sampling.ts index 739303e1fe8c..e870813c88b2 100644 --- a/packages/core/src/tracing/sampling.ts +++ b/packages/core/src/tracing/sampling.ts @@ -3,6 +3,7 @@ import { isNaN, logger } from '@sentry/utils'; import { DEBUG_BUILD } from '../debug-build'; import { hasTracingEnabled } from '../utils/hasTracingEnabled'; +import { spanToJSON } from '../utils/spanUtils'; import type { Transaction } from './transaction'; /** @@ -100,7 +101,8 @@ export function sampleTransaction( return transaction; } - DEBUG_BUILD && logger.log(`[Tracing] starting ${transaction.op} transaction - ${transaction.name}`); + DEBUG_BUILD && + logger.log(`[Tracing] starting ${transaction.op} transaction - ${spanToJSON(transaction).description}`); return transaction; } diff --git a/packages/core/src/tracing/span.ts b/packages/core/src/tracing/span.ts index e225d6c7d17f..ec92ce23f646 100644 --- a/packages/core/src/tracing/span.ts +++ b/packages/core/src/tracing/span.ts @@ -20,6 +20,7 @@ import { TRACE_FLAG_NONE, TRACE_FLAG_SAMPLED, spanTimeInputToSeconds, + spanToJSON, spanToTraceContext, spanToTraceHeader, } from '../utils/spanUtils'; @@ -84,11 +85,6 @@ export class Span implements SpanInterface { */ public op?: string; - /** - * @inheritDoc - */ - public description?: string; - /** * @inheritDoc */ @@ -128,6 +124,7 @@ export class Span implements SpanInterface { protected _traceId: string; protected _spanId: string; protected _sampled: boolean | undefined; + protected _name?: string; /** * You should never call the constructor manually, always use `Sentry.startTransaction()` @@ -145,6 +142,8 @@ export class Span implements SpanInterface { this.attributes = spanContext.attributes || {}; this.instrumenter = spanContext.instrumenter || 'sentry'; this.origin = spanContext.origin || 'manual'; + // eslint-disable-next-line deprecation/deprecation + this._name = spanContext.name || spanContext.description; if (spanContext.parentSpanId) { this.parentSpanId = spanContext.parentSpanId; @@ -156,12 +155,6 @@ export class Span implements SpanInterface { if (spanContext.op) { this.op = spanContext.op; } - if (spanContext.description) { - this.description = spanContext.description; - } - if (spanContext.name) { - this.description = spanContext.name; - } if (spanContext.status) { this.status = spanContext.status; } @@ -170,20 +163,40 @@ export class Span implements SpanInterface { } } - // This conflicts with another eslint rule :( + // This rule conflicts with another rule :( /* eslint-disable @typescript-eslint/member-ordering */ - /** An alias for `description` of the Span. */ + /** + * An alias for `description` of the Span. + * @deprecated Use `spanToJSON(span).description` instead. + */ public get name(): string { - return this.description || ''; + return this._name || ''; } /** * Update the name of the span. + * @deprecated Use `spanToJSON(span).description` instead. */ public set name(name: string) { this.updateName(name); } + /** + * Get the description of the Span. + * @deprecated Use `spanToJSON(span).description` instead. + */ + public get description(): string | undefined { + return this._name; + } + + /** + * Get the description of the Span. + * @deprecated Use `spanToJSON(span).description` instead. + */ + public set description(description: string | undefined) { + this._name = description; + } + /** * The ID of the trace. * @deprecated Use `spanContext().traceId` instead. @@ -269,7 +282,7 @@ export class Span implements SpanInterface { if (DEBUG_BUILD && childSpan.transaction) { const opStr = (spanContext && spanContext.op) || '< unknown op >'; - const nameStr = childSpan.transaction.name || '< unknown name >'; + const nameStr = spanToJSON(childSpan).description || '< unknown name >'; const idStr = childSpan.transaction.spanContext().spanId; const logMessage = `[Tracing] Starting '${opStr}' span on transaction '${nameStr}' (${idStr}).`; @@ -342,7 +355,7 @@ export class Span implements SpanInterface { * @inheritDoc */ public updateName(name: string): this { - this.description = name; + this._name = name; return this; } @@ -392,7 +405,7 @@ export class Span implements SpanInterface { public toContext(): SpanContext { return dropUndefinedKeys({ data: this._getData(), - description: this.description, + description: this._name, endTimestamp: this.endTimestamp, op: this.op, parentSpanId: this.parentSpanId, @@ -410,7 +423,8 @@ export class Span implements SpanInterface { */ public updateWithContext(spanContext: SpanContext): this { this.data = spanContext.data || {}; - this.description = spanContext.description; + // eslint-disable-next-line deprecation/deprecation + this._name = spanContext.name || spanContext.description; this.endTimestamp = spanContext.endTimestamp; this.op = spanContext.op; this.parentSpanId = spanContext.parentSpanId; @@ -437,7 +451,7 @@ export class Span implements SpanInterface { public getSpanJSON(): SpanJSON { return dropUndefinedKeys({ data: this._getData(), - description: this.description, + description: this._name, op: this.op, parent_span_id: this.parentSpanId, span_id: this._spanId, diff --git a/packages/core/src/tracing/trace.ts b/packages/core/src/tracing/trace.ts index bbb1a9dbf131..faa03e470020 100644 --- a/packages/core/src/tracing/trace.ts +++ b/packages/core/src/tracing/trace.ts @@ -39,12 +39,11 @@ export function trace( // eslint-disable-next-line @typescript-eslint/no-empty-function afterFinish: () => void = () => {}, ): T { - const ctx = normalizeContext(context); - const hub = getCurrentHub(); const scope = getCurrentScope(); const parentSpan = scope.getSpan(); + const ctx = normalizeContext(context); const activeSpan = createChildSpanOrTransaction(hub, parentSpan, ctx); scope.setSpan(activeSpan); @@ -259,16 +258,12 @@ function createChildSpanOrTransaction( * Eventually the StartSpanOptions will be more aligned with OpenTelemetry. */ function normalizeContext(context: StartSpanOptions): 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; - } - if (context.startTime) { + const ctx = { ...context }; ctx.startTimestamp = spanTimeInputToSeconds(context.startTime); delete ctx.startTime; + return ctx; } - return ctx; + return context; } diff --git a/packages/core/src/tracing/transaction.ts b/packages/core/src/tracing/transaction.ts index 9747d48fade6..c531dff3ba7e 100644 --- a/packages/core/src/tracing/transaction.ts +++ b/packages/core/src/tracing/transaction.ts @@ -28,7 +28,7 @@ export class Transaction extends SpanClass implements TransactionInterface { */ public _hub: Hub; - private _name: string; + protected _name: string; private _measurements: Measurements; @@ -47,10 +47,6 @@ export class Transaction extends SpanClass implements TransactionInterface { */ public constructor(transactionContext: TransactionContext, hub?: Hub) { super(transactionContext); - // We need to delete description since it's set by the Span class constructor - // but not needed for transactions. - delete this.description; - this._measurements = {}; this._contexts = {}; @@ -78,13 +74,17 @@ export class Transaction extends SpanClass implements TransactionInterface { } } - /** Getter for `name` property */ + /** + * Getter for `name` property. + * @deprecated Use `spanToJSON(span).description` instead. + */ public get name(): string { return this._name; } /** * Setter for `name` property, which also sets `source` as custom. + * @deprecated Use `updateName()` and `setMetadata()` instead. */ public set name(newName: string) { // eslint-disable-next-line deprecation/deprecation @@ -166,7 +166,7 @@ export class Transaction extends SpanClass implements TransactionInterface { return dropUndefinedKeys({ ...spanContext, - name: this.name, + name: this._name, trimEnd: this._trimEnd, }); } @@ -178,8 +178,7 @@ export class Transaction extends SpanClass implements TransactionInterface { // eslint-disable-next-line deprecation/deprecation super.updateWithContext(transactionContext); - this.name = transactionContext.name || ''; - + this._name = transactionContext.name || ''; this._trimEnd = transactionContext.trimEnd; return this; @@ -213,7 +212,7 @@ export class Transaction extends SpanClass implements TransactionInterface { // We don't want to have a transaction name in the DSC if the source is "url" because URLs might contain PII const source = this.metadata.source; if (source && source !== 'url') { - dsc.transaction = this.name; + dsc.transaction = this._name; } if (sampled !== undefined) { @@ -245,9 +244,9 @@ export class Transaction extends SpanClass implements TransactionInterface { return undefined; } - if (!this.name) { + if (!this._name) { DEBUG_BUILD && logger.warn('Transaction has no name, falling back to ``.'); - this.name = ''; + this._name = ''; } // just sets the end timestamp @@ -293,7 +292,7 @@ export class Transaction extends SpanClass implements TransactionInterface { start_timestamp: this.startTimestamp, tags: this.tags, timestamp: this.endTimestamp, - transaction: this.name, + transaction: this._name, type: 'transaction', sdkProcessingMetadata: { ...metadata, @@ -317,7 +316,7 @@ export class Transaction extends SpanClass implements TransactionInterface { transaction.measurements = this._measurements; } - DEBUG_BUILD && logger.log(`[Tracing] Finishing ${this.op} transaction: ${this.name}.`); + DEBUG_BUILD && logger.log(`[Tracing] Finishing ${this.op} transaction: ${this._name}.`); return transaction; } diff --git a/packages/core/src/utils/applyScopeDataToEvent.ts b/packages/core/src/utils/applyScopeDataToEvent.ts index 96a85740ef64..d16b0b04806f 100644 --- a/packages/core/src/utils/applyScopeDataToEvent.ts +++ b/packages/core/src/utils/applyScopeDataToEvent.ts @@ -1,6 +1,6 @@ import type { Breadcrumb, Event, PropagationContext, ScopeData, Span } from '@sentry/types'; import { arrayify } from '@sentry/utils'; -import { spanToTraceContext } from './spanUtils'; +import { spanToJSON, spanToTraceContext } from './spanUtils'; /** * Applies data from the scope to the event and runs all event processors on it. @@ -169,7 +169,7 @@ function applySpanToEvent(event: Event, span: Span): void { dynamicSamplingContext: transaction.getDynamicSamplingContext(), ...event.sdkProcessingMetadata, }; - const transactionName = transaction.name; + const transactionName = spanToJSON(transaction).description; if (transactionName) { event.tags = { transaction: transactionName, ...event.tags }; } diff --git a/packages/core/src/utils/spanUtils.ts b/packages/core/src/utils/spanUtils.ts index 125610f04bde..cbf8ce6d7f5b 100644 --- a/packages/core/src/utils/spanUtils.ts +++ b/packages/core/src/utils/spanUtils.ts @@ -11,11 +11,10 @@ export const TRACE_FLAG_SAMPLED = 0x1; */ export function spanToTraceContext(span: Span): TraceContext { const { spanId: span_id, traceId: trace_id } = span.spanContext(); - const { data, description, op, parent_span_id, status, tags, origin } = spanToJSON(span); + const { data, op, parent_span_id, status, tags, origin } = spanToJSON(span); return dropUndefinedKeys({ data, - description, op, parent_span_id, span_id, diff --git a/packages/core/test/lib/tracing/span.test.ts b/packages/core/test/lib/tracing/span.test.ts index 6626077956e3..3b48cb3d8640 100644 --- a/packages/core/test/lib/tracing/span.test.ts +++ b/packages/core/test/lib/tracing/span.test.ts @@ -3,57 +3,61 @@ import { Span } from '../../../src'; import { TRACE_FLAG_NONE, TRACE_FLAG_SAMPLED } from '../../../src/utils/spanUtils'; describe('span', () => { - it('works with name', () => { - const span = new Span({ name: 'span name' }); - expect(span.name).toEqual('span name'); - expect(span.description).toEqual('span name'); - }); + describe('name', () => { + /* eslint-disable deprecation/deprecation */ + it('works with name', () => { + const span = new Span({ name: 'span name' }); + expect(span.name).toEqual('span name'); + expect(span.description).toEqual('span name'); + }); - it('works with description', () => { - const span = new Span({ description: 'span name' }); - expect(span.name).toEqual('span name'); - expect(span.description).toEqual('span name'); - }); + it('works with description', () => { + const span = new Span({ description: 'span name' }); + expect(span.name).toEqual('span name'); + expect(span.description).toEqual('span name'); + }); - it('works without name', () => { - const span = new Span({}); - expect(span.name).toEqual(''); - expect(span.description).toEqual(undefined); - }); + it('works without name', () => { + const span = new Span({}); + expect(span.name).toEqual(''); + expect(span.description).toEqual(undefined); + }); - it('allows to update the name via setter', () => { - const span = new Span({ name: 'span name' }); - expect(span.name).toEqual('span name'); - expect(span.description).toEqual('span name'); + it('allows to update the name via setter', () => { + const span = new Span({ name: 'span name' }); + expect(span.name).toEqual('span name'); + expect(span.description).toEqual('span name'); - span.name = 'new name'; + span.name = 'new name'; - expect(span.name).toEqual('new name'); - expect(span.description).toEqual('new name'); - }); + expect(span.name).toEqual('new name'); + expect(span.description).toEqual('new name'); + }); - it('allows to update the name via setName', () => { - const span = new Span({ name: 'span name' }); - expect(span.name).toEqual('span name'); - expect(span.description).toEqual('span name'); + it('allows to update the name via setName', () => { + const span = new Span({ name: 'span name' }); + expect(span.name).toEqual('span name'); + expect(span.description).toEqual('span name'); - // eslint-disable-next-line deprecation/deprecation - span.setName('new name'); + // eslint-disable-next-line deprecation/deprecation + span.setName('new name'); - expect(span.name).toEqual('new name'); - expect(span.description).toEqual('new name'); - }); + expect(span.name).toEqual('new name'); + expect(span.description).toEqual('new name'); + }); - it('allows to update the name via updateName', () => { - const span = new Span({ name: 'span name' }); - expect(span.name).toEqual('span name'); - expect(span.description).toEqual('span name'); + it('allows to update the name via updateName', () => { + const span = new Span({ name: 'span name' }); + expect(span.name).toEqual('span name'); + expect(span.description).toEqual('span name'); - span.updateName('new name'); + span.updateName('new name'); - expect(span.name).toEqual('new name'); - expect(span.description).toEqual('new name'); + expect(span.name).toEqual('new name'); + expect(span.description).toEqual('new name'); + }); }); + /* eslint-enable deprecation/deprecation */ describe('setAttribute', () => { it('allows to set attributes', () => { diff --git a/packages/core/test/lib/tracing/transaction.test.ts b/packages/core/test/lib/tracing/transaction.test.ts index 3be3d7dccfcc..d0fded3c0f04 100644 --- a/packages/core/test/lib/tracing/transaction.test.ts +++ b/packages/core/test/lib/tracing/transaction.test.ts @@ -1,44 +1,48 @@ import { Transaction } from '../../../src'; describe('transaction', () => { - it('works with name', () => { - const transaction = new Transaction({ name: 'span name' }); - expect(transaction.name).toEqual('span name'); - }); + describe('name', () => { + /* eslint-disable deprecation/deprecation */ + it('works with name', () => { + const transaction = new Transaction({ name: 'span name' }); + expect(transaction.name).toEqual('span name'); + }); - it('allows to update the name via setter', () => { - const transaction = new Transaction({ name: 'span name' }); - transaction.setMetadata({ source: 'route' }); - expect(transaction.name).toEqual('span name'); + it('allows to update the name via setter', () => { + const transaction = new Transaction({ name: 'span name' }); + transaction.setMetadata({ source: 'route' }); + expect(transaction.name).toEqual('span name'); - transaction.name = 'new name'; + transaction.name = 'new name'; - expect(transaction.name).toEqual('new name'); - expect(transaction.metadata.source).toEqual('custom'); - }); + expect(transaction.name).toEqual('new name'); + expect(transaction.metadata.source).toEqual('custom'); + }); - it('allows to update the name via setName', () => { - const transaction = new Transaction({ name: 'span name' }); - transaction.setMetadata({ source: 'route' }); - expect(transaction.name).toEqual('span name'); + it('allows to update the name via setName', () => { + const transaction = new Transaction({ name: 'span name' }); + transaction.setMetadata({ source: 'route' }); + expect(transaction.name).toEqual('span name'); - transaction.setMetadata({ source: 'route' }); + transaction.setMetadata({ source: 'route' }); - // eslint-disable-next-line deprecation/deprecation - transaction.setName('new name'); + // eslint-disable-next-line deprecation/deprecation + transaction.setName('new name'); - expect(transaction.name).toEqual('new name'); - expect(transaction.metadata.source).toEqual('custom'); - }); + expect(transaction.name).toEqual('new name'); + expect(transaction.metadata.source).toEqual('custom'); + }); - it('allows to update the name via updateName', () => { - const transaction = new Transaction({ name: 'span name' }); - transaction.setMetadata({ source: 'route' }); - expect(transaction.name).toEqual('span name'); + it('allows to update the name via updateName', () => { + const transaction = new Transaction({ name: 'span name' }); + transaction.setMetadata({ source: 'route' }); + expect(transaction.name).toEqual('span name'); - transaction.updateName('new name'); + transaction.updateName('new name'); - expect(transaction.name).toEqual('new name'); - expect(transaction.metadata.source).toEqual('route'); + expect(transaction.name).toEqual('new name'); + expect(transaction.metadata.source).toEqual('route'); + }); + /* eslint-enable deprecation/deprecation */ }); }); diff --git a/packages/ember/package.json b/packages/ember/package.json index d99c0a176270..77b4bcd89e2e 100644 --- a/packages/ember/package.json +++ b/packages/ember/package.json @@ -33,6 +33,7 @@ "dependencies": { "@embroider/macros": "^1.9.0", "@sentry/browser": "7.92.0", + "@sentry/core": "7.92.0", "@sentry/types": "7.92.0", "@sentry/utils": "7.92.0", "ember-auto-import": "^1.12.1 || ^2.4.3", diff --git a/packages/ember/tests/helpers/utils.ts b/packages/ember/tests/helpers/utils.ts index 3ec336cfa59c..99109074219e 100644 --- a/packages/ember/tests/helpers/utils.ts +++ b/packages/ember/tests/helpers/utils.ts @@ -1,3 +1,4 @@ +import { spanToJSON } from '@sentry/core'; import type { Event } from '@sentry/types'; const defaultAssertOptions = { @@ -67,7 +68,7 @@ export function assertSentryTransactions( const filteredSpans = spans .filter(span => !span.op?.startsWith('ui.ember.runloop.')) .map(s => { - return `${s.op} | ${s.description}`; + return `${s.op} | ${spanToJSON(s).description}`; }); assert.true( diff --git a/packages/hub/test/scope.test.ts b/packages/hub/test/scope.test.ts index 505a523c1942..b8cfaf1914e0 100644 --- a/packages/hub/test/scope.test.ts +++ b/packages/hub/test/scope.test.ts @@ -395,8 +395,7 @@ describe('Scope', () => { const transaction = { fake: 'span', spanContext: () => ({}), - toJSON: () => ({ a: 'b' }), - name: 'fake transaction', + toJSON: () => ({ a: 'b', description: 'fake transaction' }), getDynamicSamplingContext: () => ({}), } as any; transaction.transaction = transaction; // because this is a transaction, its `transaction` pointer points to itself @@ -411,7 +410,6 @@ describe('Scope', () => { expect.assertions(1); const scope = new Scope(); const transaction = { - name: 'fake transaction', spanContext: () => ({}), toJSON: () => ({ description: 'fake transaction' }), getDynamicSamplingContext: () => ({}), diff --git a/packages/nextjs/src/common/utils/wrapperUtils.ts b/packages/nextjs/src/common/utils/wrapperUtils.ts index 0731ab9b326a..d5013675486d 100644 --- a/packages/nextjs/src/common/utils/wrapperUtils.ts +++ b/packages/nextjs/src/common/utils/wrapperUtils.ts @@ -205,7 +205,7 @@ export async function callDataFetcherTraced Promis // right here so making that check will probabably not even be necessary. // Logic will be: If there is no active transaction, start one with correct name and source. If there is an active // transaction, create a child span with correct name and source. - transaction.name = parameterizedRoute; + transaction.updateName(parameterizedRoute); transaction.metadata.source = 'route'; // Capture the route, since pre-loading, revalidation, etc might mean that this span may happen during another diff --git a/packages/node/src/integrations/http.ts b/packages/node/src/integrations/http.ts index f083142261dc..d69eb86bb833 100644 --- a/packages/node/src/integrations/http.ts +++ b/packages/node/src/integrations/http.ts @@ -1,9 +1,16 @@ import type * as http from 'http'; import type * as https from 'https'; import type { Hub } from '@sentry/core'; -import { spanToTraceHeader } from '@sentry/core'; -import { addBreadcrumb, getClient, getCurrentScope } from '@sentry/core'; -import { getCurrentHub, getDynamicSamplingContextFromClient, isSentryRequestUrl } from '@sentry/core'; +import { + addBreadcrumb, + getClient, + getCurrentHub, + getCurrentScope, + getDynamicSamplingContextFromClient, + isSentryRequestUrl, + spanToJSON, + spanToTraceHeader, +} from '@sentry/core'; import type { DynamicSamplingContext, EventProcessor, @@ -293,7 +300,9 @@ function _createWrappedRequestMethodFactory( if (res.statusCode) { requestSpan.setHttpStatus(res.statusCode); } - requestSpan.description = cleanSpanDescription(requestSpan.description, requestOptions, req); + requestSpan.updateName( + cleanSpanDescription(spanToJSON(requestSpan).description || '', requestOptions, req) || '', + ); requestSpan.end(); } }) @@ -306,7 +315,9 @@ function _createWrappedRequestMethodFactory( } if (requestSpan) { requestSpan.setHttpStatus(500); - requestSpan.description = cleanSpanDescription(requestSpan.description, requestOptions, req); + requestSpan.updateName( + cleanSpanDescription(spanToJSON(requestSpan).description || '', requestOptions, req) || '', + ); requestSpan.end(); } }); diff --git a/packages/node/test/integrations/http.test.ts b/packages/node/test/integrations/http.test.ts index 91d5a6c0e20d..ead5e2469494 100644 --- a/packages/node/test/integrations/http.test.ts +++ b/packages/node/test/integrations/http.test.ts @@ -90,7 +90,7 @@ describe('tracing', () => { expect(spans.length).toEqual(2); // our span is at index 1 because the transaction itself is at index 0 - expect(spans[1].description).toEqual('GET http://dogs.are.great/'); + expect(sentryCore.spanToJSON(spans[1]).description).toEqual('GET http://dogs.are.great/'); expect(spans[1].op).toEqual('http.client'); }); @@ -104,7 +104,7 @@ describe('tracing', () => { // only the transaction itself should be there expect(spans.length).toEqual(1); - expect((spans[0] as Transaction).name).toEqual('dogpark'); + expect(sentryCore.spanToJSON(spans[0]).description).toEqual('dogpark'); }); it('attaches the sentry-trace header to outgoing non-sentry requests', async () => { @@ -292,7 +292,7 @@ describe('tracing', () => { expect(spans.length).toEqual(2); // our span is at index 1 because the transaction itself is at index 0 - expect(spans[1].description).toEqual('GET http://dogs.are.great/spaniel'); + expect(sentryCore.spanToJSON(spans[1]).description).toEqual('GET http://dogs.are.great/spaniel'); expect(spans[1].op).toEqual('http.client'); expect(spans[1].data['http.method']).toEqual('GET'); expect(spans[1].data.url).toEqual('http://dogs.are.great/spaniel'); @@ -311,7 +311,7 @@ describe('tracing', () => { expect(spans.length).toEqual(2); // our span is at index 1 because the transaction itself is at index 0 - expect(spans[1].description).toEqual('GET http://dogs.are.great/spaniel'); + expect(sentryCore.spanToJSON(spans[1]).description).toEqual('GET http://dogs.are.great/spaniel'); expect(spans[1].op).toEqual('http.client'); expect(spans[1].data['http.method']).toEqual('GET'); expect(spans[1].data.url).toEqual('http://dogs.are.great/spaniel'); @@ -336,7 +336,7 @@ describe('tracing', () => { expect(spans.length).toEqual(2); // our span is at index 1 because the transaction itself is at index 0 - expect(spans[1].description).toEqual(`GET http://${redactedAuth}dogs.are.great/`); + expect(sentryCore.spanToJSON(spans[1]).description).toEqual(`GET http://${redactedAuth}dogs.are.great/`); }); describe('Tracing options', () => { diff --git a/packages/opentelemetry-node/src/spanprocessor.ts b/packages/opentelemetry-node/src/spanprocessor.ts index 22a3a01b7671..cb80f342c3e4 100644 --- a/packages/opentelemetry-node/src/spanprocessor.ts +++ b/packages/opentelemetry-node/src/spanprocessor.ts @@ -197,7 +197,7 @@ function updateSpanWithOtelData(sentrySpan: SentrySpan, otelSpan: OtelSpan): voi }); sentrySpan.op = op; - sentrySpan.description = description; + sentrySpan.updateName(description); } function updateTransactionWithOtelData(transaction: Transaction, otelSpan: OtelSpan): void { diff --git a/packages/opentelemetry-node/test/propagator.test.ts b/packages/opentelemetry-node/test/propagator.test.ts index 6b2ce11fb69e..8e24d7564992 100644 --- a/packages/opentelemetry-node/test/propagator.test.ts +++ b/packages/opentelemetry-node/test/propagator.test.ts @@ -66,7 +66,7 @@ describe('SentryPropagator', () => { // eslint-disable-next-line @typescript-eslint/no-unused-vars const { spanId, ...ctx } = transactionContext; // eslint-disable-next-line deprecation/deprecation - const span = transaction.startChild({ ...ctx, description: transaction.name }); + const span = transaction.startChild({ ...ctx, name: transactionContext.name }); setSentrySpan(span.spanContext().spanId, span); } } diff --git a/packages/opentelemetry-node/test/spanprocessor.test.ts b/packages/opentelemetry-node/test/spanprocessor.test.ts index fd35899f5bbc..f8a9d47951e1 100644 --- a/packages/opentelemetry-node/test/spanprocessor.test.ts +++ b/packages/opentelemetry-node/test/spanprocessor.test.ts @@ -5,7 +5,15 @@ import type { Span as OtelSpan } from '@opentelemetry/sdk-trace-base'; import { NodeTracerProvider } from '@opentelemetry/sdk-trace-node'; import { SemanticAttributes, SemanticResourceAttributes } from '@opentelemetry/semantic-conventions'; import type { SpanStatusType } from '@sentry/core'; -import { Hub, Span as SentrySpan, Transaction, addTracingExtensions, createTransport, makeMain } from '@sentry/core'; +import { + Hub, + Span as SentrySpan, + Transaction, + addTracingExtensions, + createTransport, + makeMain, + spanToJSON, +} from '@sentry/core'; import { NodeClient } from '@sentry/node'; import { resolvedSyncPromise } from '@sentry/utils'; @@ -81,7 +89,7 @@ describe('SentrySpanProcessor', () => { const sentrySpanTransaction = getSpanForOtelSpan(otelSpan) as Transaction | undefined; expect(sentrySpanTransaction).toBeInstanceOf(Transaction); - expect(sentrySpanTransaction?.name).toBe('GET /users'); + expect(spanToJSON(sentrySpanTransaction!).description).toBe('GET /users'); expect(sentrySpanTransaction?.startTimestamp).toEqual(startTimestampMs / 1000); expect(sentrySpanTransaction?.spanContext().traceId).toEqual(otelSpan.spanContext().traceId); expect(sentrySpanTransaction?.parentSpanId).toEqual(otelSpan.parentSpanId); @@ -109,7 +117,7 @@ describe('SentrySpanProcessor', () => { const sentrySpan = getSpanForOtelSpan(childOtelSpan); expect(sentrySpan).toBeInstanceOf(SentrySpan); - expect(sentrySpan?.description).toBe('SELECT * FROM users;'); + expect(sentrySpan ? spanToJSON(sentrySpan).description : undefined).toBe('SELECT * FROM users;'); expect(sentrySpan?.startTimestamp).toEqual(startTimestampMs / 1000); expect(sentrySpan?.spanContext().spanId).toEqual(childOtelSpan.spanContext().spanId); expect(sentrySpan?.parentSpanId).toEqual(sentrySpanTransaction?.spanContext().spanId); @@ -149,7 +157,7 @@ describe('SentrySpanProcessor', () => { const sentrySpan = getSpanForOtelSpan(childOtelSpan); expect(sentrySpan).toBeInstanceOf(SentrySpan); expect(sentrySpan).toBeInstanceOf(Transaction); - expect(sentrySpan?.name).toBe('SELECT * FROM users;'); + expect(spanToJSON(sentrySpan!).description).toBe('SELECT * FROM users;'); expect(sentrySpan?.startTimestamp).toEqual(startTimestampMs / 1000); expect(sentrySpan?.spanContext().spanId).toEqual(childOtelSpan.spanContext().spanId); expect(sentrySpan?.parentSpanId).toEqual(parentOtelSpan.spanContext().spanId); @@ -172,7 +180,7 @@ describe('SentrySpanProcessor', () => { const sentrySpanTransaction = getSpanForOtelSpan(parentOtelSpan) as Transaction | undefined; expect(sentrySpanTransaction).toBeInstanceOf(SentrySpan); - expect(sentrySpanTransaction?.name).toBe('GET /users'); + expect(spanToJSON(sentrySpanTransaction!).description).toBe('GET /users'); // Create some parallel, independent spans const span1 = tracer.startSpan('SELECT * FROM users;') as OtelSpan; @@ -187,9 +195,9 @@ describe('SentrySpanProcessor', () => { expect(sentrySpan2?.parentSpanId).toEqual(sentrySpanTransaction?.spanContext().spanId); expect(sentrySpan3?.parentSpanId).toEqual(sentrySpanTransaction?.spanContext().spanId); - expect(sentrySpan1?.description).toEqual('SELECT * FROM users;'); - expect(sentrySpan2?.description).toEqual('SELECT * FROM companies;'); - expect(sentrySpan3?.description).toEqual('SELECT * FROM locations;'); + expect(spanToJSON(sentrySpan1!).description).toEqual('SELECT * FROM users;'); + expect(spanToJSON(sentrySpan2!).description).toEqual('SELECT * FROM companies;'); + expect(spanToJSON(sentrySpan3!).description).toEqual('SELECT * FROM locations;'); span1.end(); span2.end(); @@ -449,12 +457,12 @@ describe('SentrySpanProcessor', () => { child.updateName('new name'); expect(sentrySpan?.op).toBe(undefined); - expect(sentrySpan?.description).toBe('SELECT * FROM users;'); + expect(sentrySpan ? spanToJSON(sentrySpan).description : undefined).toBe('SELECT * FROM users;'); child.end(); expect(sentrySpan?.op).toBe(undefined); - expect(sentrySpan?.description).toBe('new name'); + expect(sentrySpan ? spanToJSON(sentrySpan).description : undefined).toBe('new name'); parentOtelSpan.end(); }); @@ -508,7 +516,7 @@ describe('SentrySpanProcessor', () => { child.end(); - expect(sentrySpan?.description).toBe('HTTP GET'); + expect(sentrySpan ? spanToJSON(sentrySpan).description : undefined).toBe('HTTP GET'); parentOtelSpan.end(); }); @@ -529,7 +537,7 @@ describe('SentrySpanProcessor', () => { child.end(); - expect(sentrySpan?.description).toBe('GET /my/route/{id}'); + expect(sentrySpan ? spanToJSON(sentrySpan).description : undefined).toBe('GET /my/route/{id}'); expect(sentrySpan?.data).toEqual({ 'http.method': 'GET', 'http.route': '/my/route/{id}', @@ -557,7 +565,9 @@ describe('SentrySpanProcessor', () => { child.end(); - expect(sentrySpan?.description).toBe('GET http://example.com/my/route/123'); + expect(sentrySpan ? spanToJSON(sentrySpan).description : undefined).toBe( + 'GET http://example.com/my/route/123', + ); expect(sentrySpan?.data).toEqual({ 'http.method': 'GET', 'http.target': '/my/route/123', @@ -584,7 +594,9 @@ describe('SentrySpanProcessor', () => { child.end(); - expect(sentrySpan?.description).toBe('GET http://example.com/my/route/123'); + expect(sentrySpan ? spanToJSON(sentrySpan).description : undefined).toBe( + 'GET http://example.com/my/route/123', + ); expect(sentrySpan?.data).toEqual({ 'http.method': 'GET', 'http.target': '/my/route/123', @@ -658,7 +670,7 @@ describe('SentrySpanProcessor', () => { child.end(); expect(sentrySpan?.op).toBe('db'); - expect(sentrySpan?.description).toBe('SELECT * FROM users'); + expect(sentrySpan ? spanToJSON(sentrySpan).description : undefined).toBe('SELECT * FROM users'); parentOtelSpan.end(); }); @@ -677,7 +689,7 @@ describe('SentrySpanProcessor', () => { child.end(); expect(sentrySpan?.op).toBe('db'); - expect(sentrySpan?.description).toBe('fetch users from DB'); + expect(sentrySpan ? spanToJSON(sentrySpan).description : undefined).toBe('fetch users from DB'); parentOtelSpan.end(); }); @@ -696,7 +708,7 @@ describe('SentrySpanProcessor', () => { child.end(); expect(sentrySpan?.op).toBe('rpc'); - expect(sentrySpan?.description).toBe('test operation'); + expect(sentrySpan ? spanToJSON(sentrySpan).description : undefined).toBe('test operation'); parentOtelSpan.end(); }); @@ -715,7 +727,7 @@ describe('SentrySpanProcessor', () => { child.end(); expect(sentrySpan?.op).toBe('message'); - expect(sentrySpan?.description).toBe('test operation'); + expect(sentrySpan ? spanToJSON(sentrySpan).description : undefined).toBe('test operation'); parentOtelSpan.end(); }); @@ -734,7 +746,7 @@ describe('SentrySpanProcessor', () => { child.end(); expect(sentrySpan?.op).toBe('test faas trigger'); - expect(sentrySpan?.description).toBe('test operation'); + expect(sentrySpan ? spanToJSON(sentrySpan).description : undefined).toBe('test operation'); parentOtelSpan.end(); }); @@ -750,8 +762,8 @@ describe('SentrySpanProcessor', () => { parentOtelSpan.setAttribute(SemanticAttributes.FAAS_TRIGGER, 'test faas trigger'); parentOtelSpan.end(); - expect(transaction?.op).toBe('test faas trigger'); - expect(transaction?.name).toBe('test operation'); + expect(transaction.op).toBe('test faas trigger'); + expect(spanToJSON(transaction).description).toBe('test operation'); }); }); }); diff --git a/packages/remix/src/utils/instrumentServer.ts b/packages/remix/src/utils/instrumentServer.ts index 4da8826b131e..fc8e8405fd84 100644 --- a/packages/remix/src/utils/instrumentServer.ts +++ b/packages/remix/src/utils/instrumentServer.ts @@ -5,6 +5,7 @@ import { getCurrentScope, hasTracingEnabled, runWithAsyncContext, + spanToJSON, spanToTraceHeader, } from '@sentry/core'; import type { Hub } from '@sentry/node'; @@ -140,7 +141,8 @@ export async function captureRemixServerException(err: unknown, name: string, re const objectifiedErr = objectify(err); captureException(isResponse(objectifiedErr) ? await extractResponseError(objectifiedErr) : objectifiedErr, scope => { - const activeTransactionName = getActiveTransaction()?.name; + const transaction = getActiveTransaction(); + const activeTransactionName = transaction ? spanToJSON(transaction) : undefined; scope.setSDKProcessingMetadata({ request: { @@ -188,7 +190,7 @@ function makeWrappedDocumentRequestFunction(remixVersion?: number) { const span = activeTransaction?.startChild({ op: 'function.remix.document_request', origin: 'auto.function.remix', - description: activeTransaction.name, + description: spanToJSON(activeTransaction).description, tags: { method: request.method, url: request.url, diff --git a/packages/replay/src/replay.ts b/packages/replay/src/replay.ts index 75ec17f3627e..b3a1f48d162f 100644 --- a/packages/replay/src/replay.ts +++ b/packages/replay/src/replay.ts @@ -1,6 +1,6 @@ /* eslint-disable max-lines */ // TODO: We might want to split this file up import { EventType, record } from '@sentry-internal/rrweb'; -import { captureException, getClient, getCurrentScope } from '@sentry/core'; +import { captureException, getClient, getCurrentScope, spanToJSON } from '@sentry/core'; import type { ReplayRecordingMode, Transaction } from '@sentry/types'; import { logger } from '@sentry/utils'; @@ -704,7 +704,7 @@ export class ReplayContainer implements ReplayContainerInterface { return undefined; } - return lastTransaction.name; + return spanToJSON(lastTransaction).description; } /** diff --git a/packages/tracing-internal/test/browser/metrics/utils.test.ts b/packages/tracing-internal/test/browser/metrics/utils.test.ts index 25f03c11af0b..120eb6cf8076 100644 --- a/packages/tracing-internal/test/browser/metrics/utils.test.ts +++ b/packages/tracing-internal/test/browser/metrics/utils.test.ts @@ -1,3 +1,4 @@ +import { spanToJSON } from '@sentry/core'; import { Span, Transaction } from '../../../src'; import { _startChild } from '../../../src/browser/metrics/utils'; @@ -10,7 +11,7 @@ describe('_startChild()', () => { }); expect(span).toBeInstanceOf(Span); - expect(span.description).toBe('evaluation'); + expect(spanToJSON(span).description).toBe('evaluation'); expect(span.op).toBe('script'); }); diff --git a/packages/types/src/context.ts b/packages/types/src/context.ts index 8dadb959a97d..4f92ff4c1c6a 100644 --- a/packages/types/src/context.ts +++ b/packages/types/src/context.ts @@ -95,7 +95,6 @@ export interface ResponseContext extends Record { export interface TraceContext extends Record { data?: { [key: string]: any }; - description?: string; op?: string; parent_span_id?: string; span_id: string; diff --git a/packages/types/src/span.ts b/packages/types/src/span.ts index d9562d69b4e6..1538fc343e52 100644 --- a/packages/types/src/span.ts +++ b/packages/types/src/span.ts @@ -86,6 +86,8 @@ export interface SpanContextData { export interface SpanContext { /** * Description of the Span. + * + * @deprecated Use `name` instead. */ description?: string; @@ -165,6 +167,7 @@ export interface SpanContext { export interface Span extends SpanContext { /** * Human-readable identifier for the span. Identical to span.description. + * @deprecated Use `spanToJSON(span).description` instead. */ name: string; diff --git a/packages/types/src/transaction.ts b/packages/types/src/transaction.ts index 6a5ea28e2eb2..c144c5a1281d 100644 --- a/packages/types/src/transaction.ts +++ b/packages/types/src/transaction.ts @@ -43,6 +43,12 @@ export type TraceparentData = Pick { + /** + * Human-readable identifier for the transaction. + * @deprecated Use `spanToJSON(span).description` instead. + */ + name: string; + /** * The ID of the transaction. * @deprecated Use `spanContext().spanId` instead. From 31604195cf9cf0db711e3d86e90eb2da5349411a Mon Sep 17 00:00:00 2001 From: Tim Fish Date: Mon, 8 Jan 2024 15:29:59 +0100 Subject: [PATCH 23/43] test(node): Add memory leak test for `LocalVariables` (#10080) This PR adds a test for the `LocalVariable` integration that causes 20k caught exceptions and checks that the memory usage does not go over 100MB. --- .../local-variables-memory-test.js | 44 +++++++++++++++++++ .../suites/public-api/LocalVariables/test.ts | 25 +++++++++++ 2 files changed, 69 insertions(+) create mode 100644 dev-packages/node-integration-tests/suites/public-api/LocalVariables/local-variables-memory-test.js diff --git a/dev-packages/node-integration-tests/suites/public-api/LocalVariables/local-variables-memory-test.js b/dev-packages/node-integration-tests/suites/public-api/LocalVariables/local-variables-memory-test.js new file mode 100644 index 000000000000..7b227d4d08de --- /dev/null +++ b/dev-packages/node-integration-tests/suites/public-api/LocalVariables/local-variables-memory-test.js @@ -0,0 +1,44 @@ +/* eslint-disable no-unused-vars */ +const Sentry = require('@sentry/node'); + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + includeLocalVariables: true, + beforeSend: _ => { + return null; + }, + // Stop the rate limiting from kicking in + integrations: [new Sentry.Integrations.LocalVariables({ maxExceptionsPerSecond: 10000000 })], +}); + +class Some { + two(name) { + throw new Error('Enough!'); + } +} + +function one(name) { + const arr = [1, '2', null]; + const obj = { + name, + num: 5, + }; + + const ty = new Some(); + + ty.two(name); +} + +// Every millisecond cause a caught exception +setInterval(() => { + try { + one('some name'); + } catch (e) { + // + } +}, 1); + +// Every second send a memory usage update to parent process +setInterval(() => { + process.send({ memUsage: process.memoryUsage() }); +}, 1000); diff --git a/dev-packages/node-integration-tests/suites/public-api/LocalVariables/test.ts b/dev-packages/node-integration-tests/suites/public-api/LocalVariables/test.ts index 0b542c19c629..1c58fd802122 100644 --- a/dev-packages/node-integration-tests/suites/public-api/LocalVariables/test.ts +++ b/dev-packages/node-integration-tests/suites/public-api/LocalVariables/test.ts @@ -109,4 +109,29 @@ conditionalTest({ min: 18 })('LocalVariables integration', () => { done(); }); }); + + test('Should not leak memory', done => { + const testScriptPath = path.resolve(__dirname, 'local-variables-memory-test.js'); + + const child = childProcess.spawn('node', [testScriptPath], { + stdio: ['inherit', 'inherit', 'inherit', 'ipc'], + }); + + let reportedCount = 0; + + child.on('message', msg => { + reportedCount++; + const rssMb = msg.memUsage.rss / 1024 / 1024; + // We shouldn't use more than 100MB of memory + expect(rssMb).toBeLessThan(100); + }); + + // Wait for 20 seconds + setTimeout(() => { + // Ensure we've had memory usage reported at least 15 times + expect(reportedCount).toBeGreaterThan(15); + child.kill(); + done(); + }, 20000); + }); }); From b0496ea0046e1d0d54f2d21f97768a806b11a02b Mon Sep 17 00:00:00 2001 From: Yagiz Nizipli Date: Mon, 8 Jan 2024 09:38:32 -0500 Subject: [PATCH 24/43] fix(astro): use correct package name for CF (#10099) Hopefully this fixes https://github.com/getsentry/sentry-javascript/issues/9777 --- packages/astro/src/integration/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/astro/src/integration/index.ts b/packages/astro/src/integration/index.ts index 142a0b0b3019..4e68053fea6d 100644 --- a/packages/astro/src/integration/index.ts +++ b/packages/astro/src/integration/index.ts @@ -84,7 +84,7 @@ export const sentryAstro = (options: SentryOptions = {}): AstroIntegration => { // Prevent Sentry from being externalized for SSR. // Cloudflare like environments have Node.js APIs are available under `node:` prefix. // Ref: https://developers.cloudflare.com/workers/runtime-apis/nodejs/ - if (config?.adapter?.name.startsWith('@astro/cloudflare')) { + if (config?.adapter?.name.startsWith('@astrojs/cloudflare')) { updateConfig({ vite: { ssr: { From 0cbdf67c080183fef095e6781bd88a6c61041e57 Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Mon, 8 Jan 2024 16:17:36 +0100 Subject: [PATCH 25/43] feat(core): Deprecate arguments for `startSpan()` (#10101) In v8, there will be a reduced API surface. --- MIGRATION.md | 12 ++++ packages/core/src/tracing/trace.ts | 106 ++++++++++++++++++++++++++++- 2 files changed, 116 insertions(+), 2 deletions(-) diff --git a/MIGRATION.md b/MIGRATION.md index 14ab08ee8d8f..a91ce9ad8e01 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -8,6 +8,18 @@ npx @sentry/migr8@latest This will let you select which updates to run, and automatically update your code. Make sure to still review all code changes! +## Deprecate arguments for `startSpan()` APIs + +In v8, the API to start a new span will be reduced from the currently available options. +Going forward, only these argument will be passable to `startSpan()`, `startSpanManual()` and `startInactiveSpan()`: + +* `name` +* `attributes` +* `origin` +* `op` +* `startTime` +* `scope` + ## Deprecate `startTransaction()` & `span.startChild()` In v8, the old performance API `startTransaction()` (and `hub.startTransaction()`), as well as `span.startChild()`, will be removed. diff --git a/packages/core/src/tracing/trace.ts b/packages/core/src/tracing/trace.ts index faa03e470020..b244d54254f0 100644 --- a/packages/core/src/tracing/trace.ts +++ b/packages/core/src/tracing/trace.ts @@ -1,4 +1,15 @@ -import type { Scope, Span, SpanTimeInput, TransactionContext } from '@sentry/types'; +import type { + Instrumenter, + Primitive, + Scope, + Span, + SpanTimeInput, + TransactionContext, + TransactionMetadata, +} from '@sentry/types'; +import type { SpanAttributes } from '@sentry/types'; +import type { SpanOrigin } from '@sentry/types'; +import type { TransactionSource } from '@sentry/types'; import { dropUndefinedKeys, logger, tracingContextFromHeaders } from '@sentry/utils'; import { DEBUG_BUILD } from '../debug-build'; @@ -15,6 +26,97 @@ interface StartSpanOptions extends TransactionContext { /** If defined, start this span off this scope instead off the current scope. */ scope?: Scope; + + /** The name of the span. */ + name: string; + + /** An op for the span. This is a categorization for spans. */ + op?: string; + + /** The origin of the span - if it comes from auto instrumenation or manual instrumentation. */ + origin?: SpanOrigin; + + /** Attributes for the span. */ + attributes?: SpanAttributes; + + // All remaining fields are deprecated + + /** + * @deprecated Manually set the end timestamp instead. + */ + trimEnd?: boolean; + + /** + * @deprecated This cannot be set manually anymore. + */ + parentSampled?: boolean; + + /** + * @deprecated Use attributes or set data on scopes instead. + */ + metadata?: Partial; + + /** + * The name thingy. + * @deprecated Use `name` instead. + */ + description?: string; + + /** + * @deprecated Use `span.setStatus()` instead. + */ + status?: string; + + /** + * @deprecated Use `scope` instead. + */ + parentSpanId?: string; + + /** + * @deprecated You cannot manually set the span to sampled anymore. + */ + sampled?: boolean; + + /** + * @deprecated You cannot manually set the spanId anymore. + */ + spanId?: string; + + /** + * @deprecated You cannot manually set the traceId anymore. + */ + traceId?: string; + + /** + * @deprecated Use an attribute instead. + */ + source?: TransactionSource; + + /** + * @deprecated Use attributes or set tags on the scope instead. + */ + tags?: { [key: string]: Primitive }; + + /** + * @deprecated Use attributes instead. + */ + // eslint-disable-next-line @typescript-eslint/no-explicit-any + data?: { [key: string]: any }; + + /** + * @deprecated Use `startTime` instead. + */ + startTimestamp?: number; + + /** + * @deprecated Use `span.end()` instead. + */ + endTimestamp?: number; + + /** + * @deprecated You cannot set the instrumenter manually anymore. + */ + instrumenter?: Instrumenter; } /** @@ -259,7 +361,7 @@ function createChildSpanOrTransaction( */ function normalizeContext(context: StartSpanOptions): TransactionContext { if (context.startTime) { - const ctx = { ...context }; + const ctx: TransactionContext & { startTime?: SpanTimeInput } = { ...context }; ctx.startTimestamp = spanTimeInputToSeconds(context.startTime); delete ctx.startTime; return ctx; From 474545471938cd66be3840e2097d48b86540edbe Mon Sep 17 00:00:00 2001 From: Tim Fish Date: Mon, 8 Jan 2024 16:48:13 +0100 Subject: [PATCH 26/43] feat(node): Instrumentation for `node-schedule` library (#10086) This PR adds auto instrumented check-ins for the `node-schedule` library. It's not shown in the readme, but `scheduleJob` can be passed a job name as the first parameter: https://github.com/node-schedule/node-schedule/blob/c5a4d9a0dbcd5bda4996e089817e5669b5acd95f/lib/schedule.js#L28 ```ts import * as Sentry from '@sentry/node'; import * as schedule from 'node-schedule'; const scheduleWithCheckIn = Sentry.cron.instrumentNodeSchedule(schedule); const job = scheduleWithCheckIn.scheduleJob('my-cron-job', '* * * * *', () => { console.log('You will see this message every minute'); }); ``` This PR also adds a check to the `cron` instrumentation that ensures that you can't create multiple schedules with the same monitor slug. --- packages/node/src/cron/cron.ts | 14 ++++ packages/node/src/cron/node-schedule.ts | 60 +++++++++++++++++ packages/node/src/index.ts | 2 + packages/node/test/cron.test.ts | 85 +++++++++++++++++++++++++ 4 files changed, 161 insertions(+) create mode 100644 packages/node/src/cron/node-schedule.ts diff --git a/packages/node/src/cron/cron.ts b/packages/node/src/cron/cron.ts index a8b42ec0fed7..88a3e9e58eb5 100644 --- a/packages/node/src/cron/cron.ts +++ b/packages/node/src/cron/cron.ts @@ -56,6 +56,8 @@ const ERROR_TEXT = 'Automatic instrumentation of CronJob only supports crontab s * ``` */ export function instrumentCron(lib: T & CronJobConstructor, monitorSlug: string): T { + let jobScheduled = false; + return new Proxy(lib, { construct(target, args: ConstructorParameters) { const [cronTime, onTick, onComplete, start, timeZone, ...rest] = args; @@ -64,6 +66,12 @@ export function instrumentCron(lib: T & CronJobConstructor, monitorSlug: stri throw new Error(ERROR_TEXT); } + if (jobScheduled) { + throw new Error(`A job named '${monitorSlug}' has already been scheduled`); + } + + jobScheduled = true; + const cronString = replaceCronNames(cronTime); function monitoredTick(context: unknown, onComplete?: unknown): void | Promise { @@ -90,6 +98,12 @@ export function instrumentCron(lib: T & CronJobConstructor, monitorSlug: stri throw new Error(ERROR_TEXT); } + if (jobScheduled) { + throw new Error(`A job named '${monitorSlug}' has already been scheduled`); + } + + jobScheduled = true; + const cronString = replaceCronNames(cronTime); param.onTick = (context: unknown, onComplete?: unknown) => { diff --git a/packages/node/src/cron/node-schedule.ts b/packages/node/src/cron/node-schedule.ts new file mode 100644 index 000000000000..79ae44a06e52 --- /dev/null +++ b/packages/node/src/cron/node-schedule.ts @@ -0,0 +1,60 @@ +import { withMonitor } from '@sentry/core'; +import { replaceCronNames } from './common'; + +export interface NodeSchedule { + scheduleJob( + nameOrExpression: string | Date | object, + expressionOrCallback: string | Date | object | (() => void), + callback?: () => void, + ): unknown; +} + +/** + * Instruments the `node-schedule` library to send a check-in event to Sentry for each job execution. + * + * ```ts + * import * as Sentry from '@sentry/node'; + * import * as schedule from 'node-schedule'; + * + * const scheduleWithCheckIn = Sentry.cron.instrumentNodeSchedule(schedule); + * + * const job = scheduleWithCheckIn.scheduleJob('my-cron-job', '* * * * *', () => { + * console.log('You will see this message every minute'); + * }); + * ``` + */ +export function instrumentNodeSchedule(lib: T & NodeSchedule): T { + return new Proxy(lib, { + get(target, prop: keyof NodeSchedule) { + if (prop === 'scheduleJob') { + // eslint-disable-next-line @typescript-eslint/unbound-method + return new Proxy(target.scheduleJob, { + apply(target, thisArg, argArray: Parameters) { + const [nameOrExpression, expressionOrCallback] = argArray; + + if (typeof nameOrExpression !== 'string' || typeof expressionOrCallback !== 'string') { + throw new Error( + "Automatic instrumentation of 'node-schedule' requires the first parameter of 'scheduleJob' to be a job name string and the second parameter to be a crontab string", + ); + } + + const monitorSlug = nameOrExpression; + const expression = expressionOrCallback; + + return withMonitor( + monitorSlug, + () => { + return target.apply(thisArg, argArray); + }, + { + schedule: { type: 'crontab', value: replaceCronNames(expression) }, + }, + ); + }, + }); + } + + return target[prop]; + }, + }); +} diff --git a/packages/node/src/index.ts b/packages/node/src/index.ts index 47206462b937..a157e2bc2028 100644 --- a/packages/node/src/index.ts +++ b/packages/node/src/index.ts @@ -124,9 +124,11 @@ export { hapiErrorPlugin } from './integrations/hapi'; import { instrumentCron } from './cron/cron'; import { instrumentNodeCron } from './cron/node-cron'; +import { instrumentNodeSchedule } from './cron/node-schedule'; /** Methods to instrument cron libraries for Sentry check-ins */ export const cron = { instrumentCron, instrumentNodeCron, + instrumentNodeSchedule, }; diff --git a/packages/node/test/cron.test.ts b/packages/node/test/cron.test.ts index 8f479e7a16d4..eee6d4a66711 100644 --- a/packages/node/test/cron.test.ts +++ b/packages/node/test/cron.test.ts @@ -78,6 +78,26 @@ describe('cron check-ins', () => { }, }); }); + + test('throws with multiple jobs same name', () => { + const CronJobWithCheckIn = cron.instrumentCron(CronJobMock, 'my-cron-job'); + + CronJobWithCheckIn.from({ + cronTime: '* * * Jan,Sep Sun', + onTick: () => { + // + }, + }); + + expect(() => { + CronJobWithCheckIn.from({ + cronTime: '* * * Jan,Sep Sun', + onTick: () => { + // + }, + }); + }).toThrowError("A job named 'my-cron-job' has already been scheduled"); + }); }); describe('node-cron', () => { @@ -125,4 +145,69 @@ describe('cron check-ins', () => { }).toThrowError('Missing "name" for scheduled job. A name is required for Sentry check-in monitoring.'); }); }); + + describe('node-schedule', () => { + test('calls withMonitor', done => { + expect.assertions(5); + + class NodeScheduleMock { + scheduleJob( + nameOrExpression: string | Date | object, + expressionOrCallback: string | Date | object | (() => void), + callback: () => void, + ): unknown { + expect(nameOrExpression).toBe('my-cron-job'); + expect(expressionOrCallback).toBe('* * * Jan,Sep Sun'); + expect(callback).toBeInstanceOf(Function); + return callback(); + } + } + + const scheduleWithCheckIn = cron.instrumentNodeSchedule(new NodeScheduleMock()); + + scheduleWithCheckIn.scheduleJob('my-cron-job', '* * * Jan,Sep Sun', () => { + expect(withMonitorSpy).toHaveBeenCalledTimes(1); + expect(withMonitorSpy).toHaveBeenLastCalledWith('my-cron-job', expect.anything(), { + schedule: { type: 'crontab', value: '* * * 1,9 0' }, + }); + done(); + }); + }); + + test('throws without crontab string', () => { + class NodeScheduleMock { + scheduleJob(_: string, __: string | Date, ___: () => void): unknown { + return undefined; + } + } + + const scheduleWithCheckIn = cron.instrumentNodeSchedule(new NodeScheduleMock()); + + expect(() => { + scheduleWithCheckIn.scheduleJob('my-cron-job', new Date(), () => { + // + }); + }).toThrowError( + "Automatic instrumentation of 'node-schedule' requires the first parameter of 'scheduleJob' to be a job name string and the second parameter to be a crontab string", + ); + }); + + test('throws without job name', () => { + class NodeScheduleMock { + scheduleJob(_: string, __: () => void): unknown { + return undefined; + } + } + + const scheduleWithCheckIn = cron.instrumentNodeSchedule(new NodeScheduleMock()); + + expect(() => { + scheduleWithCheckIn.scheduleJob('* * * * *', () => { + // + }); + }).toThrowError( + "Automatic instrumentation of 'node-schedule' requires the first parameter of 'scheduleJob' to be a job name string and the second parameter to be a crontab string", + ); + }); + }); }); From a1b1929605b5cb18ec2773255409c11c182e8ff5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 8 Jan 2024 16:03:01 +0000 Subject: [PATCH 27/43] feat(deps): bump @sentry/cli from 2.23.2 to 2.24.1 (#10103) --- yarn.lock | 88 +++++++++++++++++++++++++++---------------------------- 1 file changed, 44 insertions(+), 44 deletions(-) diff --git a/yarn.lock b/yarn.lock index 48857bd5a97d..0887fc534495 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5292,40 +5292,40 @@ magic-string "0.27.0" unplugin "1.0.1" -"@sentry/cli-darwin@2.23.2": - version "2.23.2" - resolved "https://registry.yarnpkg.com/@sentry/cli-darwin/-/cli-darwin-2.23.2.tgz#d1fed31063e19bfbdf5d5ab0bb9938f407eb9e33" - integrity sha512-7Jw1yEmJxiNan5WJyiAKXascxoe8uccKVaTvEo0JwzgWhPzS71j3eUlthuQuy0xv5Pqw4d89khAP79X/pzW/dw== - -"@sentry/cli-linux-arm64@2.23.2": - version "2.23.2" - resolved "https://registry.yarnpkg.com/@sentry/cli-linux-arm64/-/cli-linux-arm64-2.23.2.tgz#a4171da7de22fd31a359fdd5671b9e445316778c" - integrity sha512-Hs2PbK2++r6Lbss44HIDXJwBSIyw1naLdIpOBi9NVLBGZxO2VLt8sQYDhVDv2ZIUijw1aGc5sg8R7R0/6qqr8Q== - -"@sentry/cli-linux-arm@2.23.2": - version "2.23.2" - resolved "https://registry.yarnpkg.com/@sentry/cli-linux-arm/-/cli-linux-arm-2.23.2.tgz#3a101ffcc37128eeebd9abdbe033cf9fcbf093ad" - integrity sha512-fQZNHsGO6kRPT7nuv/GZ048rA2aEGHcrTZEN6UhgHoowPGGmfSpOqlpdXLME6WYWzWeSBt5Sy5RcxMvPzuDnRQ== - -"@sentry/cli-linux-i686@2.23.2": - version "2.23.2" - resolved "https://registry.yarnpkg.com/@sentry/cli-linux-i686/-/cli-linux-i686-2.23.2.tgz#fcbaa045aa8ab2d646d6c27274f88ccc29bb417c" - integrity sha512-emogfai7xCySsTAaixjnh0hgzcb2nhEqz7MRYxGA+rSI8IgP1ZMBfdWHA/4fUap0wLNA6vVgvbHlFcBVQcGchA== - -"@sentry/cli-linux-x64@2.23.2": - version "2.23.2" - resolved "https://registry.yarnpkg.com/@sentry/cli-linux-x64/-/cli-linux-x64-2.23.2.tgz#30404f32d8f32e33a24fed67f11f11ac84da0a6c" - integrity sha512-VewJmJRUFvKR3YiPp1pZOZJxrFGLgBHLGEP/9wBkkp3cY+rKrzQ3b7Dlh9v+YOkz1qjF1R1FsAzvsYd9/05dLg== - -"@sentry/cli-win32-i686@2.23.2": - version "2.23.2" - resolved "https://registry.yarnpkg.com/@sentry/cli-win32-i686/-/cli-win32-i686-2.23.2.tgz#6f77749aad856dceaa9b206c6bd51fbc8caca704" - integrity sha512-R8olErQICIV+AdjINxLQYKVGRi49PdSykjs94gfTvJBxb2hvqCpS+LIVS5SFu2UDvT3/9Elq6hXMKxEgYNy0pQ== - -"@sentry/cli-win32-x64@2.23.2": - version "2.23.2" - resolved "https://registry.yarnpkg.com/@sentry/cli-win32-x64/-/cli-win32-x64-2.23.2.tgz#d68ac046568ca951d5bfe7216ae5c52a07a65ecc" - integrity sha512-GK9xburDBnpBmjtbWrMK+9I7DRKbEhmjfWLdoTQK593xOHPOzy8lhDZ1u9Lp1mUKUcG1xba4BOFZgNppMYG2cA== +"@sentry/cli-darwin@2.24.1": + version "2.24.1" + resolved "https://registry.yarnpkg.com/@sentry/cli-darwin/-/cli-darwin-2.24.1.tgz#feae406b2bf9a6a736e5a6f31e5561aaae8ed902" + integrity sha512-L6puTcZn5AarTL9YCVCSSCJoMV7opMx5hwwl0+sQGbLO8BChuC2QZl+j4ftEb3WgnFcT5+OODBlu4ocREtG7sQ== + +"@sentry/cli-linux-arm64@2.24.1": + version "2.24.1" + resolved "https://registry.yarnpkg.com/@sentry/cli-linux-arm64/-/cli-linux-arm64-2.24.1.tgz#a3e5339904fcb89167736a4a3c6edcd697972c24" + integrity sha512-47fq/sOZnY8oSnuEurlplHKlcEhCf4Pd3JHmV6N8dYYwPEapoELb3V53BDPhjkj/rwdpJf8T90+LXCQkeF/o+w== + +"@sentry/cli-linux-arm@2.24.1": + version "2.24.1" + resolved "https://registry.yarnpkg.com/@sentry/cli-linux-arm/-/cli-linux-arm-2.24.1.tgz#3784604555de7baa17b0b7ae8b7912d37d52738e" + integrity sha512-wnOeIl0NzUdSvz7kJKTovksDUwx6TTyV1iBjM19gZGqi+hfNc1bUa1IDGSY0m/T+CpSZiuKL0M36sYet5euDUQ== + +"@sentry/cli-linux-i686@2.24.1": + version "2.24.1" + resolved "https://registry.yarnpkg.com/@sentry/cli-linux-i686/-/cli-linux-i686-2.24.1.tgz#9ba0c0eaa783bd955060ea105278c2856c42755f" + integrity sha512-OjpP1aRV0cwdtcics0hv8tZR6Bl5+1KIc+0habMeMxfTN7FvGmJb3ZTpuhi8OJLDglppQe6KlWzEFi8UgcE42Q== + +"@sentry/cli-linux-x64@2.24.1": + version "2.24.1" + resolved "https://registry.yarnpkg.com/@sentry/cli-linux-x64/-/cli-linux-x64-2.24.1.tgz#b7d157b66f76a5ac47a43b934eadeec50e9c5640" + integrity sha512-GfryILChjrgSGBrT90ln46qt6UTI1ebevcDPoWArftTQ0n+P4tPFcfA9bCMV16Jsnc59CtjMFlQknLOAWnezgg== + +"@sentry/cli-win32-i686@2.24.1": + version "2.24.1" + resolved "https://registry.yarnpkg.com/@sentry/cli-win32-i686/-/cli-win32-i686-2.24.1.tgz#b01158d6e633a2e6dfed59feaeb6979a32d7632c" + integrity sha512-COi7b/g3BbJHlJfF7GA0LAw/foyP3rMfDLQid/4fj7a0DqNjwAJRgazXvvtAY7/3XThHVE/sgLH0UmAgYaBBpA== + +"@sentry/cli-win32-x64@2.24.1": + version "2.24.1" + resolved "https://registry.yarnpkg.com/@sentry/cli-win32-x64/-/cli-win32-x64-2.24.1.tgz#8b8752606939d229207b1afe6d4a254d24acb47c" + integrity sha512-gJxQw9ppRgZecMZ4t7mi5zWTFssVFbO2V35Nq6qk9bwHACa/LjQbyRqSqOg6zil8QruGvH1oQYClFZHlW8EHuA== "@sentry/cli@^1.74.4", "@sentry/cli@^1.77.1": version "1.77.1" @@ -5340,9 +5340,9 @@ which "^2.0.2" "@sentry/cli@^2.17.0", "@sentry/cli@^2.21.2", "@sentry/cli@^2.23.0": - version "2.23.2" - resolved "https://registry.yarnpkg.com/@sentry/cli/-/cli-2.23.2.tgz#5b8edd4e6e8fdea05f5d6bb6c84b55d52897c250" - integrity sha512-coQoJnts6E/yN21uQyI7sqa89kixXQuIRodOPnIymQtYJZG3DAwqxcCBLMS3NZyVQ3HemeuhhDnE/KFd1mS53Q== + version "2.24.1" + resolved "https://registry.yarnpkg.com/@sentry/cli/-/cli-2.24.1.tgz#9b643ee44c7b2be7cf9b9435b7eea4b92bdfd6cd" + integrity sha512-eXqbKRzychtG8mMfGmqc0DRY677ngHRYa3aVS8f0VVKHK4PPV/ta08ORs0iS73IaasP563r8YEzpYjD74GtSZA== dependencies: https-proxy-agent "^5.0.0" node-fetch "^2.6.7" @@ -5350,13 +5350,13 @@ proxy-from-env "^1.1.0" which "^2.0.2" optionalDependencies: - "@sentry/cli-darwin" "2.23.2" - "@sentry/cli-linux-arm" "2.23.2" - "@sentry/cli-linux-arm64" "2.23.2" - "@sentry/cli-linux-i686" "2.23.2" - "@sentry/cli-linux-x64" "2.23.2" - "@sentry/cli-win32-i686" "2.23.2" - "@sentry/cli-win32-x64" "2.23.2" + "@sentry/cli-darwin" "2.24.1" + "@sentry/cli-linux-arm" "2.24.1" + "@sentry/cli-linux-arm64" "2.24.1" + "@sentry/cli-linux-i686" "2.24.1" + "@sentry/cli-linux-x64" "2.24.1" + "@sentry/cli-win32-i686" "2.24.1" + "@sentry/cli-win32-x64" "2.24.1" "@sentry/vite-plugin@^0.6.1": version "0.6.1" From d305c8d6a3722b176831e5da6d27b8a72b4b301c Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Mon, 8 Jan 2024 17:14:20 +0100 Subject: [PATCH 28/43] feat(core): Deprecate `getActiveTransaction()` & `scope.getTransaction()` (#10098) A lot to refactor for us... but for now, let's deprecate this. --- MIGRATION.md | 4 +++ .../sentry-trace/baggage-header-out/server.ts | 1 + .../baggage-transaction-name/server.ts | 1 + packages/angular-ivy/README.md | 31 ++++++------------ packages/angular/README.md | 31 ++++++------------ packages/angular/src/index.ts | 1 + packages/angular/src/tracing.ts | 10 +++++- packages/astro/src/index.server.ts | 1 + packages/browser/src/index.ts | 1 + packages/browser/src/profiling/integration.ts | 1 + .../test/unit/profiling/integration.test.ts | 1 + packages/bun/src/index.ts | 1 + packages/core/src/metrics/exports.ts | 1 + packages/core/src/scope.ts | 3 +- packages/core/src/tracing/errors.ts | 1 + packages/core/src/tracing/idletransaction.ts | 1 + packages/core/src/tracing/measurement.ts | 1 + packages/core/src/tracing/utils.ts | 7 +++- packages/deno/src/index.ts | 1 + packages/ember/addon/index.ts | 32 +++++++++---------- .../sentry-performance.ts | 4 +++ .../nextjs/src/common/utils/wrapperUtils.ts | 1 + .../wrapAppGetInitialPropsWithSentry.ts | 1 + .../wrapErrorGetInitialPropsWithSentry.ts | 1 + .../common/wrapGetInitialPropsWithSentry.ts | 1 + .../wrapGetServerSidePropsWithSentry.ts | 1 + packages/node/src/handlers.ts | 1 + packages/node/src/index.ts | 1 + packages/node/src/integrations/hapi/index.ts | 3 ++ packages/node/test/handlers.test.ts | 2 ++ packages/react/src/profiler.tsx | 1 + packages/remix/src/index.server.ts | 1 + packages/remix/src/utils/instrumentServer.ts | 4 +++ packages/replay/src/replay.ts | 1 + packages/serverless/src/awsservices.ts | 1 + packages/serverless/src/google-cloud-grpc.ts | 1 + packages/serverless/src/google-cloud-http.ts | 1 + packages/serverless/src/index.ts | 1 + packages/svelte/src/performance.ts | 1 + packages/sveltekit/src/client/router.ts | 1 + packages/sveltekit/src/server/handle.ts | 1 + packages/sveltekit/src/server/index.ts | 1 + .../src/browser/backgroundtab.ts | 1 + .../src/browser/browsertracing.ts | 1 + .../src/browser/metrics/index.ts | 2 ++ .../tracing-internal/src/exports/index.ts | 1 + packages/tracing/src/index.ts | 1 + packages/tracing/test/idletransaction.test.ts | 5 +++ packages/types/src/scope.ts | 3 +- packages/vercel-edge/src/index.ts | 1 + packages/vue/src/router.ts | 1 + packages/vue/src/tracing.ts | 9 +++++- 52 files changed, 124 insertions(+), 63 deletions(-) diff --git a/MIGRATION.md b/MIGRATION.md index a91ce9ad8e01..812ce2fa8dbe 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -8,6 +8,10 @@ npx @sentry/migr8@latest This will let you select which updates to run, and automatically update your code. Make sure to still review all code changes! +## Deprecate `scope.getTransaction()` and `getActiveTransaction()` + +Instead, you should not rely on the active transaction, but just use `startSpan()` APIs, which handle this for you. + ## Deprecate arguments for `startSpan()` APIs In v8, the API to start a new span will be reduced from the currently available options. diff --git a/dev-packages/node-integration-tests/suites/express/sentry-trace/baggage-header-out/server.ts b/dev-packages/node-integration-tests/suites/express/sentry-trace/baggage-header-out/server.ts index 4f4fc91b670a..11ccc4d732e0 100644 --- a/dev-packages/node-integration-tests/suites/express/sentry-trace/baggage-header-out/server.ts +++ b/dev-packages/node-integration-tests/suites/express/sentry-trace/baggage-header-out/server.ts @@ -26,6 +26,7 @@ app.use(Sentry.Handlers.tracingHandler()); app.use(cors()); app.get('/test/express', (_req, res) => { + // eslint-disable-next-line deprecation/deprecation const transaction = Sentry.getCurrentHub().getScope().getTransaction(); if (transaction) { // eslint-disable-next-line deprecation/deprecation diff --git a/dev-packages/node-integration-tests/suites/express/sentry-trace/baggage-transaction-name/server.ts b/dev-packages/node-integration-tests/suites/express/sentry-trace/baggage-transaction-name/server.ts index 24c3541d8529..3b1fee1bba18 100644 --- a/dev-packages/node-integration-tests/suites/express/sentry-trace/baggage-transaction-name/server.ts +++ b/dev-packages/node-integration-tests/suites/express/sentry-trace/baggage-transaction-name/server.ts @@ -29,6 +29,7 @@ app.use(Sentry.Handlers.tracingHandler()); app.use(cors()); app.get('/test/express', (_req, res) => { + // eslint-disable-next-line deprecation/deprecation const transaction = Sentry.getCurrentHub().getScope().getTransaction(); if (transaction) { // eslint-disable-next-line deprecation/deprecation diff --git a/packages/angular-ivy/README.md b/packages/angular-ivy/README.md index f487ffa22707..6967e7570a82 100644 --- a/packages/angular-ivy/README.md +++ b/packages/angular-ivy/README.md @@ -215,33 +215,22 @@ export class FooterComponent implements OnInit { } ``` -You can also add your own custom spans by attaching them to the current active transaction using `getActiveTransaction` -helper. For example, if you'd like to track the duration of Angular boostraping process, you can do it as follows: +You can also add your own custom spans via `startSpan()`. For example, if you'd like to track the duration of Angular boostraping process, you can do it as follows: ```javascript import { enableProdMode } from '@angular/core'; import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; -import { init, getActiveTransaction } from '@sentry/angular-ivy'; +import { init, startSpan } from '@sentry/angular'; import { AppModule } from './app/app.module'; // ... - -const activeTransaction = getActiveTransaction(); -const boostrapSpan = - activeTransaction && - activeTransaction.startChild({ - description: 'platform-browser-dynamic', - op: 'ui.angular.bootstrap', - }); - -platformBrowserDynamic() - .bootstrapModule(AppModule) - .then(() => console.log(`Bootstrap success`)) - .catch(err => console.error(err)); - .finally(() => { - if (bootstrapSpan) { - boostrapSpan.finish(); - } - }) +startSpan({ + name: 'platform-browser-dynamic', + op: 'ui.angular.bootstrap' + }, + async () => { + await platformBrowserDynamic().bootstrapModule(AppModule); + } +); ``` diff --git a/packages/angular/README.md b/packages/angular/README.md index aa18724839a4..302b060bdb39 100644 --- a/packages/angular/README.md +++ b/packages/angular/README.md @@ -215,33 +215,22 @@ export class FooterComponent implements OnInit { } ``` -You can also add your own custom spans by attaching them to the current active transaction using `getActiveTransaction` -helper. For example, if you'd like to track the duration of Angular boostraping process, you can do it as follows: +You can also add your own custom spans via `startSpan()`. For example, if you'd like to track the duration of Angular boostraping process, you can do it as follows: ```javascript import { enableProdMode } from '@angular/core'; import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; -import { init, getActiveTransaction } from '@sentry/angular'; +import { init, startSpan } from '@sentry/angular'; import { AppModule } from './app/app.module'; // ... - -const activeTransaction = getActiveTransaction(); -const boostrapSpan = - activeTransaction && - activeTransaction.startChild({ - description: 'platform-browser-dynamic', - op: 'ui.angular.bootstrap', - }); - -platformBrowserDynamic() - .bootstrapModule(AppModule) - .then(() => console.log(`Bootstrap success`)) - .catch(err => console.error(err)); - .finally(() => { - if (bootstrapSpan) { - boostrapSpan.finish(); - } - }) +startSpan({ + name: 'platform-browser-dynamic', + op: 'ui.angular.bootstrap' + }, + async () => { + await platformBrowserDynamic().bootstrapModule(AppModule); + } +); ``` diff --git a/packages/angular/src/index.ts b/packages/angular/src/index.ts index b6f188a35d14..f7f0536463a2 100644 --- a/packages/angular/src/index.ts +++ b/packages/angular/src/index.ts @@ -5,6 +5,7 @@ export * from '@sentry/browser'; export { init } from './sdk'; export { createErrorHandler, SentryErrorHandler } from './errorhandler'; export { + // eslint-disable-next-line deprecation/deprecation getActiveTransaction, // TODO `instrumentAngularRouting` is just an alias for `routingInstrumentation`; deprecate the latter at some point instrumentAngularRouting, // new name diff --git a/packages/angular/src/tracing.ts b/packages/angular/src/tracing.ts index c34fa822cb14..c75c10d0c60e 100644 --- a/packages/angular/src/tracing.ts +++ b/packages/angular/src/tracing.ts @@ -47,9 +47,12 @@ export function routingInstrumentation( export const instrumentAngularRouting = routingInstrumentation; /** - * Grabs active transaction off scope + * Grabs active transaction off scope. + * + * @deprecated You should not rely on the transaction, but just use `startSpan()` APIs instead. */ export function getActiveTransaction(): Transaction | undefined { + // eslint-disable-next-line deprecation/deprecation return getCurrentScope().getTransaction(); } @@ -69,6 +72,7 @@ export class TraceService implements OnDestroy { } const strippedUrl = stripUrlQueryAndFragment(navigationEvent.url); + // eslint-disable-next-line deprecation/deprecation let activeTransaction = getActiveTransaction(); if (!activeTransaction && stashedStartTransactionOnLocationChange) { @@ -116,6 +120,7 @@ export class TraceService implements OnDestroy { (event.state as unknown as RouterState & { root: ActivatedRouteSnapshot }).root, ); + // eslint-disable-next-line deprecation/deprecation const transaction = getActiveTransaction(); // TODO (v8 / #5416): revisit the source condition. Do we want to make the parameterized route the default? if (transaction && transaction.metadata.source === 'url') { @@ -182,6 +187,7 @@ export class TraceDirective implements OnInit, AfterViewInit { this.componentName = UNKNOWN_COMPONENT; } + // eslint-disable-next-line deprecation/deprecation const activeTransaction = getActiveTransaction(); if (activeTransaction) { // eslint-disable-next-line deprecation/deprecation @@ -225,6 +231,7 @@ export function TraceClassDecorator(): ClassDecorator { const originalOnInit = target.prototype.ngOnInit; // eslint-disable-next-line @typescript-eslint/no-explicit-any target.prototype.ngOnInit = function (...args: any[]): ReturnType { + // eslint-disable-next-line deprecation/deprecation const activeTransaction = getActiveTransaction(); if (activeTransaction) { // eslint-disable-next-line deprecation/deprecation @@ -263,6 +270,7 @@ export function TraceMethodDecorator(): MethodDecorator { // eslint-disable-next-line @typescript-eslint/no-explicit-any descriptor.value = function (...args: any[]): ReturnType { const now = timestampInSeconds(); + // eslint-disable-next-line deprecation/deprecation const activeTransaction = getActiveTransaction(); if (activeTransaction) { // eslint-disable-next-line deprecation/deprecation diff --git a/packages/astro/src/index.server.ts b/packages/astro/src/index.server.ts index b038e215496d..2eec2fc1e0e9 100644 --- a/packages/astro/src/index.server.ts +++ b/packages/astro/src/index.server.ts @@ -22,6 +22,7 @@ export { createTransport, // eslint-disable-next-line deprecation/deprecation extractTraceparentData, + // eslint-disable-next-line deprecation/deprecation getActiveTransaction, getHubFromCarrier, getCurrentHub, diff --git a/packages/browser/src/index.ts b/packages/browser/src/index.ts index d9afa470b2ba..97abefea8242 100644 --- a/packages/browser/src/index.ts +++ b/packages/browser/src/index.ts @@ -46,6 +46,7 @@ export { setMeasurement, // eslint-disable-next-line deprecation/deprecation extractTraceparentData, + // eslint-disable-next-line deprecation/deprecation getActiveTransaction, spanStatusfromHttpCode, // eslint-disable-next-line deprecation/deprecation diff --git a/packages/browser/src/profiling/integration.ts b/packages/browser/src/profiling/integration.ts index 3f823429c122..b50c60552a7f 100644 --- a/packages/browser/src/profiling/integration.ts +++ b/packages/browser/src/profiling/integration.ts @@ -24,6 +24,7 @@ const browserProfilingIntegration: IntegrationFn = () => { setup(client) { const scope = getCurrentScope(); + // eslint-disable-next-line deprecation/deprecation const transaction = scope.getTransaction(); if (transaction && isAutomatedPageLoadTransaction(transaction)) { diff --git a/packages/browser/test/unit/profiling/integration.test.ts b/packages/browser/test/unit/profiling/integration.test.ts index ae95927ac2cd..bfcdde5c33ea 100644 --- a/packages/browser/test/unit/profiling/integration.test.ts +++ b/packages/browser/test/unit/profiling/integration.test.ts @@ -50,6 +50,7 @@ describe('BrowserProfilingIntegration', () => { const client = Sentry.getClient(); + // eslint-disable-next-line deprecation/deprecation const currentTransaction = Sentry.getCurrentHub().getScope().getTransaction(); expect(currentTransaction?.op).toBe('pageload'); currentTransaction?.end(); diff --git a/packages/bun/src/index.ts b/packages/bun/src/index.ts index 749299badd74..bb033496da02 100644 --- a/packages/bun/src/index.ts +++ b/packages/bun/src/index.ts @@ -39,6 +39,7 @@ export { // eslint-disable-next-line deprecation/deprecation extractTraceparentData, flush, + // eslint-disable-next-line deprecation/deprecation getActiveTransaction, getHubFromCarrier, getCurrentHub, diff --git a/packages/core/src/metrics/exports.ts b/packages/core/src/metrics/exports.ts index 45759982bc14..03e81ed49f0e 100644 --- a/packages/core/src/metrics/exports.ts +++ b/packages/core/src/metrics/exports.ts @@ -30,6 +30,7 @@ function addToMetricsAggregator( } const { unit, tags, timestamp } = data; const { release, environment } = client.getOptions(); + // eslint-disable-next-line deprecation/deprecation const transaction = scope.getTransaction(); const metricTags: Record = {}; if (release) { diff --git a/packages/core/src/scope.ts b/packages/core/src/scope.ts index 8857329de3dd..54bb9fc52b40 100644 --- a/packages/core/src/scope.ts +++ b/packages/core/src/scope.ts @@ -321,7 +321,8 @@ export class Scope implements ScopeInterface { } /** - * @inheritDoc + * Returns the `Transaction` attached to the scope (if there is one). + * @deprecated You should not rely on the transaction, but just use `startSpan()` APIs instead. */ public getTransaction(): Transaction | undefined { // Often, this span (if it exists at all) will be a transaction, but it's not guaranteed to be. Regardless, it will diff --git a/packages/core/src/tracing/errors.ts b/packages/core/src/tracing/errors.ts index 9030f02efadc..5a885dd1f090 100644 --- a/packages/core/src/tracing/errors.ts +++ b/packages/core/src/tracing/errors.ts @@ -27,6 +27,7 @@ export function registerErrorInstrumentation(): void { * If an error or unhandled promise occurs, we mark the active transaction as failed */ function errorCallback(): void { + // eslint-disable-next-line deprecation/deprecation const activeTransaction = getActiveTransaction(); if (activeTransaction) { const status: SpanStatusType = 'internal_error'; diff --git a/packages/core/src/tracing/idletransaction.ts b/packages/core/src/tracing/idletransaction.ts index 16e5fbcc61e8..4643e6b0c97b 100644 --- a/packages/core/src/tracing/idletransaction.ts +++ b/packages/core/src/tracing/idletransaction.ts @@ -196,6 +196,7 @@ export class IdleTransaction extends Transaction { // if `this._onScope` is `true`, the transaction put itself on the scope when it started if (this._onScope) { const scope = this._idleHub.getScope(); + // eslint-disable-next-line deprecation/deprecation if (scope.getTransaction() === this) { scope.setSpan(undefined); } diff --git a/packages/core/src/tracing/measurement.ts b/packages/core/src/tracing/measurement.ts index b13bcb6b5a4a..7fff22688f20 100644 --- a/packages/core/src/tracing/measurement.ts +++ b/packages/core/src/tracing/measurement.ts @@ -6,6 +6,7 @@ import { getActiveTransaction } from './utils'; * Adds a measurement to the current active transaction. */ export function setMeasurement(name: string, value: number, unit: MeasurementUnit): void { + // eslint-disable-next-line deprecation/deprecation const transaction = getActiveTransaction(); if (transaction) { transaction.setMeasurement(name, value, unit); diff --git a/packages/core/src/tracing/utils.ts b/packages/core/src/tracing/utils.ts index f1b4c0f1ae06..bfea3d5d1e28 100644 --- a/packages/core/src/tracing/utils.ts +++ b/packages/core/src/tracing/utils.ts @@ -4,10 +4,15 @@ import { extractTraceparentData as _extractTraceparentData } from '@sentry/utils import type { Hub } from '../hub'; import { getCurrentHub } from '../hub'; -/** Grabs active transaction off scope, if any */ +/** + * Grabs active transaction off scope. + * + * @deprecated You should not rely on the transaction, but just use `startSpan()` APIs instead. + */ export function getActiveTransaction(maybeHub?: Hub): T | undefined { const hub = maybeHub || getCurrentHub(); const scope = hub.getScope(); + // eslint-disable-next-line deprecation/deprecation return scope.getTransaction() as T | undefined; } diff --git a/packages/deno/src/index.ts b/packages/deno/src/index.ts index f70d511ae8a3..2658d6f31e36 100644 --- a/packages/deno/src/index.ts +++ b/packages/deno/src/index.ts @@ -38,6 +38,7 @@ export { extractTraceparentData, continueTrace, flush, + // eslint-disable-next-line deprecation/deprecation getActiveTransaction, getHubFromCarrier, getCurrentHub, diff --git a/packages/ember/addon/index.ts b/packages/ember/addon/index.ts index 4f62a19b3ba3..4179df0d1dd2 100644 --- a/packages/ember/addon/index.ts +++ b/packages/ember/addon/index.ts @@ -2,11 +2,12 @@ import { assert, warn } from '@ember/debug'; import type Route from '@ember/routing/route'; import { next } from '@ember/runloop'; import { getOwnConfig, isDevelopingApp, macroCondition } from '@embroider/macros'; +import { startSpan } from '@sentry/browser'; import type { BrowserOptions } from '@sentry/browser'; import * as Sentry from '@sentry/browser'; import { SDK_VERSION } from '@sentry/browser'; import type { Transaction } from '@sentry/types'; -import { GLOBAL_OBJ, timestampInSeconds } from '@sentry/utils'; +import { GLOBAL_OBJ } from '@sentry/utils'; import Ember from 'ember'; import type { EmberSentryConfig, GlobalConfig, OwnConfig } from './types'; @@ -66,7 +67,13 @@ export function InitSentryForEmber(_runtimeConfig?: BrowserOptions): void { } } +/** + * Grabs active transaction off scope. + * + * @deprecated You should not rely on the transaction, but just use `startSpan()` APIs instead. + */ export const getActiveTransaction = (): Transaction | undefined => { + // eslint-disable-next-line deprecation/deprecation return Sentry.getCurrentHub().getScope().getTransaction(); }; @@ -80,23 +87,16 @@ export const instrumentRoutePerformance = (BaseRoute fn: X, args: Parameters, ): Promise> => { - const startTimestamp = timestampInSeconds(); - const result = await fn(...args); - - const currentTransaction = getActiveTransaction(); - if (!currentTransaction) { - return result; - } - currentTransaction - // eslint-disable-next-line deprecation/deprecation - .startChild({ + return startSpan( + { op, - description, + name: description, origin: 'auto.ui.ember', - startTimestamp, - }) - .end(); - return result; + }, + () => { + return fn(...args); + }, + ); }; const routeName = BaseRoute.name; diff --git a/packages/ember/addon/instance-initializers/sentry-performance.ts b/packages/ember/addon/instance-initializers/sentry-performance.ts index 1a502d052774..d61801fa6d17 100644 --- a/packages/ember/addon/instance-initializers/sentry-performance.ts +++ b/packages/ember/addon/instance-initializers/sentry-performance.ts @@ -196,6 +196,7 @@ function _instrumentEmberRunloop(config: EmberSentryConfig): void { if (previousInstance) { return; } + // eslint-disable-next-line deprecation/deprecation const activeTransaction = getActiveTransaction(); if (!activeTransaction) { return; @@ -227,6 +228,7 @@ function _instrumentEmberRunloop(config: EmberSentryConfig): void { // Setup for next queue + // eslint-disable-next-line deprecation/deprecation const stillActiveTransaction = getActiveTransaction(); if (!stillActiveTransaction) { return; @@ -288,6 +290,7 @@ function processComponentRenderAfter( const componentRenderDuration = now - begin.now; if (componentRenderDuration * 1000 >= minComponentDuration) { + // eslint-disable-next-line deprecation/deprecation const activeTransaction = getActiveTransaction(); // eslint-disable-next-line deprecation/deprecation activeTransaction?.startChild({ @@ -374,6 +377,7 @@ function _instrumentInitialLoad(config: EmberSentryConfig): void { const startTimestamp = (measure.startTime + browserPerformanceTimeOrigin) / 1000; const endTimestamp = startTimestamp + measure.duration / 1000; + // eslint-disable-next-line deprecation/deprecation const transaction = getActiveTransaction(); // eslint-disable-next-line deprecation/deprecation const span = transaction?.startChild({ diff --git a/packages/nextjs/src/common/utils/wrapperUtils.ts b/packages/nextjs/src/common/utils/wrapperUtils.ts index d5013675486d..be00cb4c7a2a 100644 --- a/packages/nextjs/src/common/utils/wrapperUtils.ts +++ b/packages/nextjs/src/common/utils/wrapperUtils.ts @@ -194,6 +194,7 @@ export async function callDataFetcherTraced Promis ): Promise> { const { parameterizedRoute, dataFetchingMethodName } = options; + // eslint-disable-next-line deprecation/deprecation const transaction = getActiveTransaction(); if (!transaction) { diff --git a/packages/nextjs/src/common/wrapAppGetInitialPropsWithSentry.ts b/packages/nextjs/src/common/wrapAppGetInitialPropsWithSentry.ts index cd6cc4934493..c965cacb3c32 100644 --- a/packages/nextjs/src/common/wrapAppGetInitialPropsWithSentry.ts +++ b/packages/nextjs/src/common/wrapAppGetInitialPropsWithSentry.ts @@ -52,6 +52,7 @@ export function wrapAppGetInitialPropsWithSentry(origAppGetInitialProps: AppGetI }; } = await tracedGetInitialProps.apply(thisArg, args); + // eslint-disable-next-line deprecation/deprecation const requestTransaction = getTransactionFromRequest(req) ?? getCurrentScope().getTransaction(); // Per definition, `pageProps` is not optional, however an increased amount of users doesn't seem to call diff --git a/packages/nextjs/src/common/wrapErrorGetInitialPropsWithSentry.ts b/packages/nextjs/src/common/wrapErrorGetInitialPropsWithSentry.ts index b26e4a2434c3..413c350ef14f 100644 --- a/packages/nextjs/src/common/wrapErrorGetInitialPropsWithSentry.ts +++ b/packages/nextjs/src/common/wrapErrorGetInitialPropsWithSentry.ts @@ -53,6 +53,7 @@ export function wrapErrorGetInitialPropsWithSentry( _sentryBaggage?: string; } = await tracedGetInitialProps.apply(thisArg, args); + // eslint-disable-next-line deprecation/deprecation const requestTransaction = getTransactionFromRequest(req) ?? getCurrentScope().getTransaction(); if (requestTransaction) { errorGetInitialProps._sentryTraceData = spanToTraceHeader(requestTransaction); diff --git a/packages/nextjs/src/common/wrapGetInitialPropsWithSentry.ts b/packages/nextjs/src/common/wrapGetInitialPropsWithSentry.ts index df4e3febfefc..8263f00c3dcb 100644 --- a/packages/nextjs/src/common/wrapGetInitialPropsWithSentry.ts +++ b/packages/nextjs/src/common/wrapGetInitialPropsWithSentry.ts @@ -49,6 +49,7 @@ export function wrapGetInitialPropsWithSentry(origGetInitialProps: GetInitialPro _sentryBaggage?: string; } = (await tracedGetInitialProps.apply(thisArg, args)) ?? {}; // Next.js allows undefined to be returned from a getInitialPropsFunction. + // eslint-disable-next-line deprecation/deprecation const requestTransaction = getTransactionFromRequest(req) ?? getCurrentScope().getTransaction(); if (requestTransaction) { initialProps._sentryTraceData = spanToTraceHeader(requestTransaction); diff --git a/packages/nextjs/src/common/wrapGetServerSidePropsWithSentry.ts b/packages/nextjs/src/common/wrapGetServerSidePropsWithSentry.ts index c74f9db7292b..fff92d31f49b 100644 --- a/packages/nextjs/src/common/wrapGetServerSidePropsWithSentry.ts +++ b/packages/nextjs/src/common/wrapGetServerSidePropsWithSentry.ts @@ -46,6 +46,7 @@ export function wrapGetServerSidePropsWithSentry( >); if (serverSideProps && 'props' in serverSideProps) { + // eslint-disable-next-line deprecation/deprecation const requestTransaction = getTransactionFromRequest(req) ?? getCurrentScope().getTransaction(); if (requestTransaction) { serverSideProps.props._sentryTraceData = spanToTraceHeader(requestTransaction); diff --git a/packages/node/src/handlers.ts b/packages/node/src/handlers.ts index 6d4c6f5a4494..cc4877125507 100644 --- a/packages/node/src/handlers.ts +++ b/packages/node/src/handlers.ts @@ -328,6 +328,7 @@ interface TrpcMiddlewareArguments { export function trpcMiddleware(options: SentryTrpcMiddlewareOptions = {}) { return function ({ path, type, next, rawInput }: TrpcMiddlewareArguments): T { const clientOptions = getClient()?.getOptions(); + // eslint-disable-next-line deprecation/deprecation const sentryTransaction = getCurrentScope().getTransaction(); if (sentryTransaction) { diff --git a/packages/node/src/index.ts b/packages/node/src/index.ts index a157e2bc2028..ba0f2333df6d 100644 --- a/packages/node/src/index.ts +++ b/packages/node/src/index.ts @@ -38,6 +38,7 @@ export { // eslint-disable-next-line deprecation/deprecation extractTraceparentData, flush, + // eslint-disable-next-line deprecation/deprecation getActiveTransaction, getHubFromCarrier, getCurrentHub, diff --git a/packages/node/src/integrations/hapi/index.ts b/packages/node/src/integrations/hapi/index.ts index 409470680a73..f63207b59c7a 100644 --- a/packages/node/src/integrations/hapi/index.ts +++ b/packages/node/src/integrations/hapi/index.ts @@ -45,6 +45,7 @@ export const hapiErrorPlugin = { const server = serverArg as unknown as Server; server.events.on('request', (request, event) => { + // eslint-disable-next-line deprecation/deprecation const transaction = getActiveTransaction(); if (request.response && isBoomObject(request.response)) { @@ -91,6 +92,7 @@ export const hapiTracingPlugin = { }); server.ext('onPreResponse', (request, h) => { + // eslint-disable-next-line deprecation/deprecation const transaction = getActiveTransaction(); if (request.response && isResponseObject(request.response) && transaction) { @@ -110,6 +112,7 @@ export const hapiTracingPlugin = { }); server.ext('onPostHandler', (request, h) => { + // eslint-disable-next-line deprecation/deprecation const transaction = getActiveTransaction(); if (request.response && isResponseObject(request.response) && transaction) { diff --git a/packages/node/test/handlers.test.ts b/packages/node/test/handlers.test.ts index 0eb1237053ad..d234994455bf 100644 --- a/packages/node/test/handlers.test.ts +++ b/packages/node/test/handlers.test.ts @@ -340,6 +340,7 @@ describe('tracingHandler', () => { sentryTracingMiddleware(req, res, next); + // eslint-disable-next-line deprecation/deprecation const transaction = sentryCore.getCurrentHub().getScope().getTransaction(); expect(transaction).toBeDefined(); @@ -463,6 +464,7 @@ describe('tracingHandler', () => { sentryTracingMiddleware(req, res, next); + // eslint-disable-next-line deprecation/deprecation const transaction = sentryCore.getCurrentScope().getTransaction(); expect(transaction?.metadata.request).toEqual(req); diff --git a/packages/react/src/profiler.tsx b/packages/react/src/profiler.tsx index 43a8f166d48b..d3bdd1ed06aa 100644 --- a/packages/react/src/profiler.tsx +++ b/packages/react/src/profiler.tsx @@ -227,6 +227,7 @@ export { withProfiler, Profiler, useProfiler }; export function getActiveTransaction(hub: Hub = getCurrentHub()): T | undefined { if (hub) { const scope = hub.getScope(); + // eslint-disable-next-line deprecation/deprecation return scope.getTransaction() as T | undefined; } diff --git a/packages/remix/src/index.server.ts b/packages/remix/src/index.server.ts index b28ce95caa61..f30b5311d0f6 100644 --- a/packages/remix/src/index.server.ts +++ b/packages/remix/src/index.server.ts @@ -25,6 +25,7 @@ export { createTransport, // eslint-disable-next-line deprecation/deprecation extractTraceparentData, + // eslint-disable-next-line deprecation/deprecation getActiveTransaction, getHubFromCarrier, getCurrentHub, diff --git a/packages/remix/src/utils/instrumentServer.ts b/packages/remix/src/utils/instrumentServer.ts index fc8e8405fd84..b1383725a94d 100644 --- a/packages/remix/src/utils/instrumentServer.ts +++ b/packages/remix/src/utils/instrumentServer.ts @@ -141,6 +141,7 @@ export async function captureRemixServerException(err: unknown, name: string, re const objectifiedErr = objectify(err); captureException(isResponse(objectifiedErr) ? await extractResponseError(objectifiedErr) : objectifiedErr, scope => { + // eslint-disable-next-line deprecation/deprecation const transaction = getActiveTransaction(); const activeTransactionName = transaction ? spanToJSON(transaction) : undefined; @@ -183,6 +184,7 @@ function makeWrappedDocumentRequestFunction(remixVersion?: number) { loadContext?: Record, ): Promise { let res: Response; + // eslint-disable-next-line deprecation/deprecation const activeTransaction = getActiveTransaction(); try { @@ -238,6 +240,7 @@ function makeWrappedDataFunction( } let res: Response | AppData; + // eslint-disable-next-line deprecation/deprecation const activeTransaction = getActiveTransaction(); const currentScope = getCurrentScope(); @@ -294,6 +297,7 @@ function getTraceAndBaggage(): { sentryTrace?: string; sentryBaggage?: string; } { + // eslint-disable-next-line deprecation/deprecation const transaction = getActiveTransaction(); const currentScope = getCurrentScope(); diff --git a/packages/replay/src/replay.ts b/packages/replay/src/replay.ts index b3a1f48d162f..20e59da7a902 100644 --- a/packages/replay/src/replay.ts +++ b/packages/replay/src/replay.ts @@ -699,6 +699,7 @@ export class ReplayContainer implements ReplayContainerInterface { * This is only available if performance is enabled, and if an instrumented router is used. */ public getCurrentRoute(): string | undefined { + // eslint-disable-next-line deprecation/deprecation const lastTransaction = this.lastTransaction || getCurrentScope().getTransaction(); if (!lastTransaction || !['route', 'custom'].includes(lastTransaction.metadata.source)) { return undefined; diff --git a/packages/serverless/src/awsservices.ts b/packages/serverless/src/awsservices.ts index 7dc3ef010abd..41b73f58464e 100644 --- a/packages/serverless/src/awsservices.ts +++ b/packages/serverless/src/awsservices.ts @@ -58,6 +58,7 @@ function wrapMakeRequest( return function (this: TService, operation: string, params?: GenericParams, callback?: MakeRequestCallback) { let span: Span | undefined; const scope = getCurrentScope(); + // eslint-disable-next-line deprecation/deprecation const transaction = scope.getTransaction(); const req = orig.call(this, operation, params); req.on('afterBuild', () => { diff --git a/packages/serverless/src/google-cloud-grpc.ts b/packages/serverless/src/google-cloud-grpc.ts index d3815422e4df..74a88f622333 100644 --- a/packages/serverless/src/google-cloud-grpc.ts +++ b/packages/serverless/src/google-cloud-grpc.ts @@ -109,6 +109,7 @@ function fillGrpcFunction(stub: Stub, serviceIdentifier: string, methodName: str } let span: Span | undefined; const scope = getCurrentScope(); + // eslint-disable-next-line deprecation/deprecation const transaction = scope.getTransaction(); if (transaction) { // eslint-disable-next-line deprecation/deprecation diff --git a/packages/serverless/src/google-cloud-http.ts b/packages/serverless/src/google-cloud-http.ts index 813d65add0e5..87687a52f82e 100644 --- a/packages/serverless/src/google-cloud-http.ts +++ b/packages/serverless/src/google-cloud-http.ts @@ -53,6 +53,7 @@ function wrapRequestFunction(orig: RequestFunction): RequestFunction { return function (this: common.Service, reqOpts: RequestOptions, callback: ResponseCallback): void { let span: Span | undefined; const scope = getCurrentScope(); + // eslint-disable-next-line deprecation/deprecation const transaction = scope.getTransaction(); if (transaction) { const httpMethod = reqOpts.method || 'GET'; diff --git a/packages/serverless/src/index.ts b/packages/serverless/src/index.ts index ce076283b635..749909e39272 100644 --- a/packages/serverless/src/index.ts +++ b/packages/serverless/src/index.ts @@ -27,6 +27,7 @@ export { // eslint-disable-next-line deprecation/deprecation configureScope, createTransport, + // eslint-disable-next-line deprecation/deprecation getActiveTransaction, getCurrentHub, getClient, diff --git a/packages/svelte/src/performance.ts b/packages/svelte/src/performance.ts index e3ba158e6f59..f0aa16d0c961 100644 --- a/packages/svelte/src/performance.ts +++ b/packages/svelte/src/performance.ts @@ -94,5 +94,6 @@ function recordUpdateSpans(componentName: string, initSpan?: Span): void { } function getActiveTransaction(): Transaction | undefined { + // eslint-disable-next-line deprecation/deprecation return getCurrentScope().getTransaction(); } diff --git a/packages/sveltekit/src/client/router.ts b/packages/sveltekit/src/client/router.ts index 2250943909ab..ab967a9d0f13 100644 --- a/packages/sveltekit/src/client/router.ts +++ b/packages/sveltekit/src/client/router.ts @@ -98,6 +98,7 @@ function instrumentNavigations(startTransactionFn: (context: TransactionContext) const parameterizedRouteOrigin = from && from.route.id; const parameterizedRouteDestination = to && to.route.id; + // eslint-disable-next-line deprecation/deprecation activeTransaction = getActiveTransaction(); if (!activeTransaction) { diff --git a/packages/sveltekit/src/server/handle.ts b/packages/sveltekit/src/server/handle.ts index 540de96c6de9..d6eb5555fefd 100644 --- a/packages/sveltekit/src/server/handle.ts +++ b/packages/sveltekit/src/server/handle.ts @@ -95,6 +95,7 @@ export function addSentryCodeToPage(options: SentryHandleOptions): NonNullable { + // eslint-disable-next-line deprecation/deprecation const transaction = getActiveTransaction(); if (transaction) { const traceparentData = spanToTraceHeader(transaction); diff --git a/packages/sveltekit/src/server/index.ts b/packages/sveltekit/src/server/index.ts index 61419c196736..e224df7421a2 100644 --- a/packages/sveltekit/src/server/index.ts +++ b/packages/sveltekit/src/server/index.ts @@ -19,6 +19,7 @@ export { createTransport, // eslint-disable-next-line deprecation/deprecation extractTraceparentData, + // eslint-disable-next-line deprecation/deprecation getActiveTransaction, getHubFromCarrier, getCurrentHub, diff --git a/packages/tracing-internal/src/browser/backgroundtab.ts b/packages/tracing-internal/src/browser/backgroundtab.ts index e13b997b16db..67385b665be4 100644 --- a/packages/tracing-internal/src/browser/backgroundtab.ts +++ b/packages/tracing-internal/src/browser/backgroundtab.ts @@ -12,6 +12,7 @@ import { WINDOW } from './types'; export function registerBackgroundTabDetection(): void { if (WINDOW && WINDOW.document) { WINDOW.document.addEventListener('visibilitychange', () => { + // eslint-disable-next-line deprecation/deprecation const activeTransaction = getActiveTransaction() as IdleTransaction; if (WINDOW.document.hidden && activeTransaction) { const statusType: SpanStatusType = 'cancelled'; diff --git a/packages/tracing-internal/src/browser/browsertracing.ts b/packages/tracing-internal/src/browser/browsertracing.ts index 09209e2655c1..687f5c057ded 100644 --- a/packages/tracing-internal/src/browser/browsertracing.ts +++ b/packages/tracing-internal/src/browser/browsertracing.ts @@ -390,6 +390,7 @@ export class BrowserTracing implements Integration { const { idleTimeout, finalTimeout, heartbeatInterval } = this.options; const op = 'ui.action.click'; + // eslint-disable-next-line deprecation/deprecation const currentTransaction = getActiveTransaction(); if (currentTransaction && currentTransaction.op && ['navigation', 'pageload'].includes(currentTransaction.op)) { DEBUG_BUILD && diff --git a/packages/tracing-internal/src/browser/metrics/index.ts b/packages/tracing-internal/src/browser/metrics/index.ts index beb98783d8fd..72cce4f7aad6 100644 --- a/packages/tracing-internal/src/browser/metrics/index.ts +++ b/packages/tracing-internal/src/browser/metrics/index.ts @@ -69,6 +69,7 @@ export function startTrackingWebVitals(): () => void { export function startTrackingLongTasks(): void { addPerformanceInstrumentationHandler('longtask', ({ entries }) => { for (const entry of entries) { + // eslint-disable-next-line deprecation/deprecation const transaction = getActiveTransaction() as IdleTransaction | undefined; if (!transaction) { return; @@ -94,6 +95,7 @@ export function startTrackingLongTasks(): void { export function startTrackingInteractions(): void { addPerformanceInstrumentationHandler('event', ({ entries }) => { for (const entry of entries) { + // eslint-disable-next-line deprecation/deprecation const transaction = getActiveTransaction() as IdleTransaction | undefined; if (!transaction) { return; diff --git a/packages/tracing-internal/src/exports/index.ts b/packages/tracing-internal/src/exports/index.ts index 7b7f81a9caa1..8c10b3165608 100644 --- a/packages/tracing-internal/src/exports/index.ts +++ b/packages/tracing-internal/src/exports/index.ts @@ -1,6 +1,7 @@ export { // eslint-disable-next-line deprecation/deprecation extractTraceparentData, + // eslint-disable-next-line deprecation/deprecation getActiveTransaction, hasTracingEnabled, IdleTransaction, diff --git a/packages/tracing/src/index.ts b/packages/tracing/src/index.ts index a515db240117..8559188884d7 100644 --- a/packages/tracing/src/index.ts +++ b/packages/tracing/src/index.ts @@ -62,6 +62,7 @@ export const addExtensionMethods = addExtensionMethodsT; * * `getActiveTransaction` can be imported from `@sentry/node`, `@sentry/browser`, or your framework SDK */ +// eslint-disable-next-line deprecation/deprecation export const getActiveTransaction = getActiveTransactionT; /** diff --git a/packages/tracing/test/idletransaction.test.ts b/packages/tracing/test/idletransaction.test.ts index 729b63074700..7e6b186ea82f 100644 --- a/packages/tracing/test/idletransaction.test.ts +++ b/packages/tracing/test/idletransaction.test.ts @@ -34,6 +34,7 @@ describe('IdleTransaction', () => { transaction.initSpanRecorder(10); const scope = hub.getScope(); + // eslint-disable-next-line deprecation/deprecation expect(scope.getTransaction()).toBe(transaction); }); @@ -42,6 +43,7 @@ describe('IdleTransaction', () => { transaction.initSpanRecorder(10); const scope = hub.getScope(); + // eslint-disable-next-line deprecation/deprecation expect(scope.getTransaction()).toBe(undefined); }); @@ -60,6 +62,7 @@ describe('IdleTransaction', () => { jest.runAllTimers(); const scope = hub.getScope(); + // eslint-disable-next-line deprecation/deprecation expect(scope.getTransaction()).toBe(undefined); }); @@ -77,6 +80,7 @@ describe('IdleTransaction', () => { jest.runAllTimers(); const scope = hub.getScope(); + // eslint-disable-next-line deprecation/deprecation expect(scope.getTransaction()).toBe(undefined); }); @@ -99,6 +103,7 @@ describe('IdleTransaction', () => { jest.runAllTimers(); const scope = hub.getScope(); + // eslint-disable-next-line deprecation/deprecation expect(scope.getTransaction()).toBe(otherTransaction); }); }); diff --git a/packages/types/src/scope.ts b/packages/types/src/scope.ts index 7cbe7ff264b5..8d6c1a9d26aa 100644 --- a/packages/types/src/scope.ts +++ b/packages/types/src/scope.ts @@ -147,7 +147,8 @@ export interface Scope { getSpan(): Span | undefined; /** - * Returns the `Transaction` attached to the scope (if there is one) + * Returns the `Transaction` attached to the scope (if there is one). + * @deprecated You should not rely on the transaction, but just use `startSpan()` APIs instead. */ getTransaction(): Transaction | undefined; diff --git a/packages/vercel-edge/src/index.ts b/packages/vercel-edge/src/index.ts index ff8d97fbf398..515b9c8691b7 100644 --- a/packages/vercel-edge/src/index.ts +++ b/packages/vercel-edge/src/index.ts @@ -38,6 +38,7 @@ export { // eslint-disable-next-line deprecation/deprecation extractTraceparentData, flush, + // eslint-disable-next-line deprecation/deprecation getActiveTransaction, getHubFromCarrier, getCurrentHub, diff --git a/packages/vue/src/router.ts b/packages/vue/src/router.ts index 70117e960fe2..5acc19bb825c 100644 --- a/packages/vue/src/router.ts +++ b/packages/vue/src/router.ts @@ -108,6 +108,7 @@ export function vueRouterInstrumentation( } if (startTransactionOnPageLoad && isPageLoadNavigation) { + // eslint-disable-next-line deprecation/deprecation const pageloadTransaction = getActiveTransaction(); if (pageloadTransaction) { if (pageloadTransaction.metadata.source !== 'custom') { diff --git a/packages/vue/src/tracing.ts b/packages/vue/src/tracing.ts index 2fede1c740c8..cff758c4b571 100644 --- a/packages/vue/src/tracing.ts +++ b/packages/vue/src/tracing.ts @@ -32,8 +32,13 @@ const HOOKS: { [key in Operation]: Hook[] } = { update: ['beforeUpdate', 'updated'], }; -/** Grabs active transaction off scope, if any */ +/** + * Grabs active transaction off scope. + * + * @deprecated You should not rely on the transaction, but just use `startSpan()` APIs instead. + */ export function getActiveTransaction(): Transaction | undefined { + // eslint-disable-next-line deprecation/deprecation return getCurrentScope().getTransaction(); } @@ -73,6 +78,7 @@ export const createTracingMixins = (options: TracingOptions): Mixins => { const isRoot = this.$root === this; if (isRoot) { + // eslint-disable-next-line deprecation/deprecation const activeTransaction = getActiveTransaction(); if (activeTransaction) { this.$_sentryRootSpan = @@ -102,6 +108,7 @@ export const createTracingMixins = (options: TracingOptions): Mixins => { // Start a new span if current hook is a 'before' hook. // Otherwise, retrieve the current span and finish it. if (internalHook == internalHooks[0]) { + // eslint-disable-next-line deprecation/deprecation const activeTransaction = (this.$root && this.$root.$_sentryRootSpan) || getActiveTransaction(); if (activeTransaction) { // Cancel old span for this hook operation in case it didn't get cleaned up. We're not actually sure if it From 4a920b53f0c84c5a1bce54ff1d71e264ee3ef44f Mon Sep 17 00:00:00 2001 From: Tim Fish Date: Mon, 8 Jan 2024 21:16:20 +0100 Subject: [PATCH 29/43] fix(node): Update ANR min node version to v16.17.0 (#10107) Upon testing the full range of Electron versions for release in CI, I found that starting a worker thread via a data uri requires at least node v16.17.0. This wasn't picked up by the tests in this repository because we only test the latest version of each major. --- packages/node/src/integrations/anr/index.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/node/src/integrations/anr/index.ts b/packages/node/src/integrations/anr/index.ts index 3aa71f0589f3..549e483b51d0 100644 --- a/packages/node/src/integrations/anr/index.ts +++ b/packages/node/src/integrations/anr/index.ts @@ -56,8 +56,8 @@ const anrIntegration = ((options: Partial = {}) => { return { name: INTEGRATION_NAME, setup(client: NodeClient) { - if (NODE_VERSION.major < 16) { - throw new Error('ANR detection requires Node 16 or later'); + if (NODE_VERSION.major < 16 || (NODE_VERSION.major === 16 && NODE_VERSION.minor < 17)) { + throw new Error('ANR detection requires Node 16.17.0 or later'); } // setImmediate is used to ensure that all other integrations have been setup @@ -68,6 +68,8 @@ const anrIntegration = ((options: Partial = {}) => { /** * Starts a thread to detect App Not Responding (ANR) events + * + * ANR detection requires Node 16.17.0 or later */ // eslint-disable-next-line deprecation/deprecation export const Anr = convertIntegrationFnToClass(INTEGRATION_NAME, anrIntegration); From 0276c0386117371c34c953b1bc889e6a3b5e2607 Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Tue, 9 Jan 2024 09:58:32 +0100 Subject: [PATCH 30/43] feat(core): Deprecate transaction metadata in favor of attributes (#10097) This deprecates any usage of `metadata` on transactions. The main usages we have are to set `sampleRate` and `source` in there. These I replaced with semantic attributes. For backwards compatibility, when creating the transaction event we still check the metadata there as well. Other usage of metadata (mostly around `request`) remains intact for now, we need to replace this in v8 - e.g. put this on the isolation scope, probably. This is the first usage of [Semantic Attributes](https://github.com/getsentry/rfcs/blob/main/text/0116-sentry-semantic-conventions.md) in the SDK! This replaces https://github.com/getsentry/sentry-javascript/pull/10041 --- MIGRATION.md | 2 + .../startTransaction/circular_data/subject.js | 6 +- .../startTransaction/circular_data/test.ts | 4 +- .../baggage-transaction-name/server.ts | 3 +- packages/angular-ivy/ng-package.json | 5 +- packages/angular-ivy/package.json | 1 + packages/angular/ng-package.json | 5 +- packages/angular/package.json | 1 + packages/angular/src/tracing.ts | 14 +++- packages/angular/test/tracing.test.ts | 32 +++++--- packages/astro/src/server/middleware.ts | 5 +- packages/astro/test/server/middleware.test.ts | 15 ++-- packages/bun/src/integrations/bunserver.ts | 4 +- .../bun/test/integrations/bunserver.test.ts | 1 + packages/core/src/index.ts | 1 + packages/core/src/semanticAttributes.ts | 11 +++ packages/core/src/tracing/sampling.ts | 19 ++--- packages/core/src/tracing/span.ts | 6 +- packages/core/src/tracing/transaction.ts | 68 ++++++++++++++--- .../core/test/lib/tracing/transaction.test.ts | 73 +++++++++++++++++-- .../src/common/utils/edgeWrapperUtils.ts | 12 ++- .../nextjs/src/common/utils/wrapperUtils.ts | 3 +- .../src/common/wrapApiHandlerWithSentry.ts | 6 +- .../wrapGenerationFunctionWithSentry.ts | 6 +- .../common/wrapServerComponentWithSentry.ts | 6 +- .../nextjs/test/config/withSentry.test.ts | 6 +- .../nextjs/test/edge/edgeWrapperUtils.test.ts | 8 +- .../nextjs/test/edge/withSentryAPI.test.ts | 13 +++- packages/node/src/handlers.ts | 8 +- packages/node/test/handlers.test.ts | 1 + .../opentelemetry-node/src/spanprocessor.ts | 11 ++- .../opentelemetry-node/src/utils/spanData.ts | 1 + .../test/spanprocessor.test.ts | 3 + .../test/custom/transaction.test.ts | 3 +- packages/react/package.json | 1 + packages/react/src/reactrouter.tsx | 3 +- packages/react/src/reactrouterv6.tsx | 3 +- packages/react/test/reactrouterv4.test.tsx | 19 ++--- packages/react/test/reactrouterv5.test.tsx | 17 +++-- packages/react/test/reactrouterv6.4.test.tsx | 13 ++-- packages/react/test/reactrouterv6.test.tsx | 17 +++-- packages/remix/src/client/performance.tsx | 3 +- packages/replay/src/replay.ts | 13 +++- packages/serverless/src/awslambda.ts | 6 +- .../src/gcpfunction/cloud_events.ts | 4 +- packages/serverless/src/gcpfunction/events.ts | 4 +- packages/serverless/src/gcpfunction/http.ts | 8 +- packages/serverless/test/awslambda.test.ts | 40 ++++++++-- packages/serverless/test/gcpfunction.test.ts | 63 ++++++++++++---- packages/sveltekit/src/client/router.ts | 10 +-- packages/sveltekit/test/client/router.test.ts | 17 ++--- .../src/browser/browsertracing.ts | 20 +++-- .../src/node/integrations/express.ts | 6 +- packages/tracing/test/hub.test.ts | 18 ++--- packages/tracing/test/span.test.ts | 6 +- packages/tracing/test/transaction.test.ts | 5 +- packages/types/src/transaction.ts | 21 ++++-- packages/utils/src/requestdata.ts | 3 + packages/vue/src/router.ts | 16 ++-- packages/vue/test/router.test.ts | 46 ++++++------ 60 files changed, 515 insertions(+), 230 deletions(-) create mode 100644 packages/core/src/semanticAttributes.ts diff --git a/MIGRATION.md b/MIGRATION.md index 812ce2fa8dbe..fbff1464c634 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -89,6 +89,8 @@ In v8, the Span class is heavily reworked. The following properties & methods ar * `span.traceId`: Use `span.spanContext().traceId` instead. * `span.name`: Use `spanToJSON(span).description` instead. * `span.description`: Use `spanToJSON(span).description` instead. +* `transaction.setMetadata()`: Use attributes instead, or set data on the scope. +* `transaction.metadata`: Use attributes instead, or set data on the scope. ## Deprecate `pushScope` & `popScope` in favor of `withScope` diff --git a/dev-packages/browser-integration-tests/suites/public-api/startTransaction/circular_data/subject.js b/dev-packages/browser-integration-tests/suites/public-api/startTransaction/circular_data/subject.js index d2ae465addf7..9d4da2e3027a 100644 --- a/dev-packages/browser-integration-tests/suites/public-api/startTransaction/circular_data/subject.js +++ b/dev-packages/browser-integration-tests/suites/public-api/startTransaction/circular_data/subject.js @@ -2,10 +2,8 @@ const chicken = {}; const egg = { contains: chicken }; chicken.lays = egg; -const circularObject = chicken; - -const transaction = Sentry.startTransaction({ name: 'circular_object_test_transaction', data: circularObject }); -const span = transaction.startChild({ op: 'circular_object_test_span', data: circularObject }); +const transaction = Sentry.startTransaction({ name: 'circular_object_test_transaction', data: { chicken } }); +const span = transaction.startChild({ op: 'circular_object_test_span', data: { chicken } }); span.end(); transaction.end(); diff --git a/dev-packages/browser-integration-tests/suites/public-api/startTransaction/circular_data/test.ts b/dev-packages/browser-integration-tests/suites/public-api/startTransaction/circular_data/test.ts index 1870f679b3da..b6e88e821cbb 100644 --- a/dev-packages/browser-integration-tests/suites/public-api/startTransaction/circular_data/test.ts +++ b/dev-packages/browser-integration-tests/suites/public-api/startTransaction/circular_data/test.ts @@ -17,12 +17,12 @@ sentryTest('should be able to handle circular data', async ({ getLocalTestPath, expect(eventData.contexts).toMatchObject({ trace: { - data: { lays: { contains: '[Circular ~]' } }, + data: { chicken: { lays: { contains: '[Circular ~]' } } }, }, }); expect(eventData?.spans?.[0]).toMatchObject({ - data: { lays: { contains: '[Circular ~]' } }, + data: { chicken: { lays: { contains: '[Circular ~]' } } }, op: 'circular_object_test_span', }); diff --git a/dev-packages/node-integration-tests/suites/express/sentry-trace/baggage-transaction-name/server.ts b/dev-packages/node-integration-tests/suites/express/sentry-trace/baggage-transaction-name/server.ts index 3b1fee1bba18..28a84f87cf52 100644 --- a/dev-packages/node-integration-tests/suites/express/sentry-trace/baggage-transaction-name/server.ts +++ b/dev-packages/node-integration-tests/suites/express/sentry-trace/baggage-transaction-name/server.ts @@ -1,4 +1,5 @@ import http from 'http'; +import { SEMANTIC_ATTRIBUTE_SENTRY_SOURCE } from '@sentry/core'; import * as Sentry from '@sentry/node'; import * as Tracing from '@sentry/tracing'; import cors from 'cors'; @@ -34,7 +35,7 @@ app.get('/test/express', (_req, res) => { if (transaction) { // eslint-disable-next-line deprecation/deprecation transaction.traceId = '86f39e84263a4de99c326acab3bfe3bd'; - transaction.setMetadata({ source: 'route' }); + transaction.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, 'route'); } const headers = http.get('http://somewhere.not.sentry/').getHeaders(); diff --git a/packages/angular-ivy/ng-package.json b/packages/angular-ivy/ng-package.json index b50faf694df3..38d9d7f5ac68 100644 --- a/packages/angular-ivy/ng-package.json +++ b/packages/angular-ivy/ng-package.json @@ -5,9 +5,10 @@ "entryFile": "src/index.ts", "umdModuleIds": { "@sentry/browser": "Sentry", - "@sentry/utils": "Sentry.util" + "@sentry/utils": "Sentry.util", + "@sentry/core": "Sentry.core" } }, - "allowedNonPeerDependencies": ["@sentry/browser", "@sentry/utils", "@sentry/types", "tslib"], + "allowedNonPeerDependencies": ["@sentry/browser", "@sentry/core", "@sentry/utils", "@sentry/types", "tslib"], "assets": ["README.md", "LICENSE"] } diff --git a/packages/angular-ivy/package.json b/packages/angular-ivy/package.json index 9d9ccd91e4db..14b10ae27954 100644 --- a/packages/angular-ivy/package.json +++ b/packages/angular-ivy/package.json @@ -22,6 +22,7 @@ }, "dependencies": { "@sentry/browser": "7.92.0", + "@sentry/core": "7.92.0", "@sentry/types": "7.92.0", "@sentry/utils": "7.92.0", "tslib": "^2.4.1" diff --git a/packages/angular/ng-package.json b/packages/angular/ng-package.json index 88df70c1c7bd..28794322dd0a 100644 --- a/packages/angular/ng-package.json +++ b/packages/angular/ng-package.json @@ -5,9 +5,10 @@ "entryFile": "src/index.ts", "umdModuleIds": { "@sentry/browser": "Sentry", - "@sentry/utils": "Sentry.util" + "@sentry/utils": "Sentry.util", + "@sentry/core": "Sentry.core" } }, - "whitelistedNonPeerDependencies": ["@sentry/browser", "@sentry/utils", "@sentry/types", "tslib"], + "whitelistedNonPeerDependencies": ["@sentry/browser", "@sentry/core", "@sentry/utils", "@sentry/types", "tslib"], "assets": ["README.md", "LICENSE"] } diff --git a/packages/angular/package.json b/packages/angular/package.json index c1cfc9765071..55998227fc7c 100644 --- a/packages/angular/package.json +++ b/packages/angular/package.json @@ -22,6 +22,7 @@ }, "dependencies": { "@sentry/browser": "7.92.0", + "@sentry/core": "7.92.0", "@sentry/types": "7.92.0", "@sentry/utils": "7.92.0", "tslib": "^2.4.1" diff --git a/packages/angular/src/tracing.ts b/packages/angular/src/tracing.ts index c75c10d0c60e..efd2c840420b 100644 --- a/packages/angular/src/tracing.ts +++ b/packages/angular/src/tracing.ts @@ -8,6 +8,7 @@ import type { ActivatedRouteSnapshot, Event, RouterState } from '@angular/router import { NavigationCancel, NavigationError, Router } from '@angular/router'; import { NavigationEnd, NavigationStart, ResolveEnd } from '@angular/router'; import { WINDOW, getCurrentScope } from '@sentry/browser'; +import { SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, spanToJSON } from '@sentry/core'; import type { Span, Transaction, TransactionContext } from '@sentry/types'; import { logger, stripUrlQueryAndFragment, timestampInSeconds } from '@sentry/utils'; import type { Observable } from 'rxjs'; @@ -39,7 +40,9 @@ export function routingInstrumentation( name: WINDOW.location.pathname, op: 'pageload', origin: 'auto.pageload.angular', - metadata: { source: 'url' }, + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'url', + }, }); } } @@ -80,7 +83,9 @@ export class TraceService implements OnDestroy { name: strippedUrl, op: 'navigation', origin: 'auto.navigation.angular', - metadata: { source: 'url' }, + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'url', + }, }); } @@ -123,9 +128,10 @@ export class TraceService implements OnDestroy { // eslint-disable-next-line deprecation/deprecation const transaction = getActiveTransaction(); // TODO (v8 / #5416): revisit the source condition. Do we want to make the parameterized route the default? - if (transaction && transaction.metadata.source === 'url') { + const attributes = (transaction && spanToJSON(transaction).data) || {}; + if (transaction && attributes[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE] === 'url') { transaction.updateName(route); - transaction.setMetadata({ source: 'route' }); + transaction.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, 'route'); } }), ); diff --git a/packages/angular/test/tracing.test.ts b/packages/angular/test/tracing.test.ts index 695e3d7af564..c2406f628128 100644 --- a/packages/angular/test/tracing.test.ts +++ b/packages/angular/test/tracing.test.ts @@ -1,5 +1,6 @@ import { Component } from '@angular/core'; import type { ActivatedRouteSnapshot, CanActivate, RouterStateSnapshot } from '@angular/router'; +import { SEMANTIC_ATTRIBUTE_SENTRY_SOURCE } from '@sentry/core'; import { TraceClassDecorator, TraceDirective, TraceMethodDecorator, instrumentAngularRouting } from '../src'; import { getParameterizedRouteFromSnapshot } from '../src/tracing'; @@ -11,7 +12,14 @@ const defaultStartTransaction = (ctx: any) => { transaction = { ...ctx, updateName: jest.fn(name => (transaction.name = name)), - setMetadata: jest.fn(), + setAttribute: jest.fn(), + toJSON: () => ({ + data: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'custom', + ...ctx.data, + ...ctx.attributes, + }, + }), }; return transaction; @@ -45,7 +53,7 @@ describe('Angular Tracing', () => { name: '/', op: 'pageload', origin: 'auto.pageload.angular', - metadata: { source: 'url' }, + attributes: { [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'url' }, }); }); }); @@ -107,11 +115,15 @@ describe('Angular Tracing', () => { const customStartTransaction = jest.fn((ctx: any) => { transaction = { ...ctx, - metadata: { - ...ctx.metadata, - source: 'custom', - }, + toJSON: () => ({ + data: { + ...ctx.data, + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'custom', + }, + }), + metadata: ctx.metadata, updateName: jest.fn(name => (transaction.name = name)), + setAttribute: jest.fn(), }; return transaction; @@ -135,12 +147,12 @@ describe('Angular Tracing', () => { name: url, op: 'pageload', origin: 'auto.pageload.angular', - metadata: { source: 'url' }, + attributes: { [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'url' }, }); expect(transaction.updateName).toHaveBeenCalledTimes(0); expect(transaction.name).toEqual(url); - expect(transaction.metadata.source).toBe('custom'); + expect(transaction.toJSON().data).toEqual({ [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'custom' }); env.destroy(); }); @@ -326,10 +338,10 @@ describe('Angular Tracing', () => { name: url, op: 'navigation', origin: 'auto.navigation.angular', - metadata: { source: 'url' }, + attributes: { [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'url' }, }); expect(transaction.updateName).toHaveBeenCalledWith(result); - expect(transaction.setMetadata).toHaveBeenCalledWith({ source: 'route' }); + expect(transaction.setAttribute).toHaveBeenCalledWith(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, 'route'); env.destroy(); }); diff --git a/packages/astro/src/server/middleware.ts b/packages/astro/src/server/middleware.ts index 907714d33874..79ac0b09a987 100644 --- a/packages/astro/src/server/middleware.ts +++ b/packages/astro/src/server/middleware.ts @@ -1,3 +1,4 @@ +import { SEMANTIC_ATTRIBUTE_SENTRY_SOURCE } from '@sentry/core'; import { captureException, continueTrace, @@ -111,6 +112,7 @@ async function instrumentRequest( try { const interpolatedRoute = interpolateRouteFromUrlAndParams(ctx.url.pathname, ctx.params); + const source = interpolatedRoute ? 'route' : 'url'; // storing res in a variable instead of directly returning is necessary to // invoke the catch block if next() throws const res = await startSpan( @@ -121,12 +123,13 @@ async function instrumentRequest( origin: 'auto.http.astro', status: 'ok', metadata: { + // eslint-disable-next-line deprecation/deprecation ...traceCtx?.metadata, - source: interpolatedRoute ? 'route' : 'url', }, data: { method, url: stripUrlQueryAndFragment(ctx.url.href), + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: source, ...(ctx.url.search && { 'http.query': ctx.url.search }), ...(ctx.url.hash && { 'http.fragment': ctx.url.hash }), ...(options.trackHeaders && { headers: allHeaders }), diff --git a/packages/astro/test/server/middleware.test.ts b/packages/astro/test/server/middleware.test.ts index 13508cebf057..03a4d2ee1dc4 100644 --- a/packages/astro/test/server/middleware.test.ts +++ b/packages/astro/test/server/middleware.test.ts @@ -1,3 +1,4 @@ +import { SEMANTIC_ATTRIBUTE_SENTRY_SOURCE } from '@sentry/core'; import * as SentryNode from '@sentry/node'; import type { Client } from '@sentry/types'; import { vi } from 'vitest'; @@ -57,10 +58,9 @@ describe('sentryMiddleware', () => { data: { method: 'GET', url: 'https://mydomain.io/users/123/details', + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route', }, - metadata: { - source: 'route', - }, + metadata: {}, name: 'GET /users/[id]/details', op: 'http.server', origin: 'auto.http.astro', @@ -94,10 +94,9 @@ describe('sentryMiddleware', () => { data: { method: 'GET', url: 'http://localhost:1234/a%xx', + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'url', }, - metadata: { - source: 'url', - }, + metadata: {}, name: 'GET a%xx', op: 'http.server', origin: 'auto.http.astro', @@ -159,8 +158,10 @@ describe('sentryMiddleware', () => { expect(startSpanSpy).toHaveBeenCalledWith( expect.objectContaining({ + data: expect.objectContaining({ + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route', + }), metadata: { - source: 'route', dynamicSamplingContext: { release: '1.0.0', }, diff --git a/packages/bun/src/integrations/bunserver.ts b/packages/bun/src/integrations/bunserver.ts index 89d245908400..695b26edc144 100644 --- a/packages/bun/src/integrations/bunserver.ts +++ b/packages/bun/src/integrations/bunserver.ts @@ -1,4 +1,5 @@ import { + SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, Transaction, captureException, continueTrace, @@ -54,6 +55,7 @@ function instrumentBunServeOptions(serveOptions: Parameters[0] const parsedUrl = parseUrl(request.url); const data: Record = { 'http.request.method': request.method || 'GET', + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'url', }; if (parsedUrl.search) { data['http.query'] = parsedUrl.search; @@ -72,8 +74,8 @@ function instrumentBunServeOptions(serveOptions: Parameters[0] ...ctx, data, metadata: { + // eslint-disable-next-line deprecation/deprecation ...ctx.metadata, - source: 'url', request: { url, method: request.method, diff --git a/packages/bun/test/integrations/bunserver.test.ts b/packages/bun/test/integrations/bunserver.test.ts index 09b3ae45aee4..59b242b5ea7c 100644 --- a/packages/bun/test/integrations/bunserver.test.ts +++ b/packages/bun/test/integrations/bunserver.test.ts @@ -82,6 +82,7 @@ describe('Bun Serve Integration', () => { expect(transaction.parentSpanId).toBe(PARENT_SPAN_ID); expect(transaction.isRecording()).toBe(true); + // eslint-disable-next-line deprecation/deprecation expect(transaction.metadata?.dynamicSamplingContext).toStrictEqual({ version: '1.0', environment: 'production' }); }); diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 82cbe5dadf6e..e277c01f2dbe 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -5,6 +5,7 @@ export type { ServerRuntimeClientOptions } from './server-runtime-client'; export type { RequestDataIntegrationOptions } from './integrations/requestdata'; export * from './tracing'; +export * from './semanticAttributes'; export { createEventEnvelope, createSessionEnvelope } from './envelope'; export { addBreadcrumb, diff --git a/packages/core/src/semanticAttributes.ts b/packages/core/src/semanticAttributes.ts new file mode 100644 index 000000000000..6239e6f6acf7 --- /dev/null +++ b/packages/core/src/semanticAttributes.ts @@ -0,0 +1,11 @@ +/** + * Use this attribute to represent the source of a span. + * Should be one of: custom, url, route, view, component, task, unknown + * + */ +export const SEMANTIC_ATTRIBUTE_SENTRY_SOURCE = 'sentry.source'; + +/** + * Use this attribute to represent the sample rate used for a span. + */ +export const SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE = 'sentry.sample_rate'; diff --git a/packages/core/src/tracing/sampling.ts b/packages/core/src/tracing/sampling.ts index e870813c88b2..f14aeb131db4 100644 --- a/packages/core/src/tracing/sampling.ts +++ b/packages/core/src/tracing/sampling.ts @@ -2,6 +2,7 @@ import type { Options, SamplingContext } from '@sentry/types'; import { isNaN, logger } from '@sentry/utils'; import { DEBUG_BUILD } from '../debug-build'; +import { SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE } from '../semanticAttributes'; import { hasTracingEnabled } from '../utils/hasTracingEnabled'; import { spanToJSON } from '../utils/spanUtils'; import type { Transaction } from './transaction'; @@ -30,10 +31,8 @@ export function sampleTransaction( // if the user has forced a sampling decision by passing a `sampled` value in their transaction context, go with that // eslint-disable-next-line deprecation/deprecation if (transaction.sampled !== undefined) { - transaction.setMetadata({ - // eslint-disable-next-line deprecation/deprecation - sampleRate: Number(transaction.sampled), - }); + // eslint-disable-next-line deprecation/deprecation + transaction.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE, Number(transaction.sampled)); return transaction; } @@ -42,22 +41,16 @@ export function sampleTransaction( let sampleRate; if (typeof options.tracesSampler === 'function') { sampleRate = options.tracesSampler(samplingContext); - transaction.setMetadata({ - sampleRate: Number(sampleRate), - }); + transaction.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE, Number(sampleRate)); } else if (samplingContext.parentSampled !== undefined) { sampleRate = samplingContext.parentSampled; } else if (typeof options.tracesSampleRate !== 'undefined') { sampleRate = options.tracesSampleRate; - transaction.setMetadata({ - sampleRate: Number(sampleRate), - }); + transaction.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE, Number(sampleRate)); } else { // When `enableTracing === true`, we use a sample rate of 100% sampleRate = 1; - transaction.setMetadata({ - sampleRate, - }); + transaction.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE, sampleRate); } // 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/core/src/tracing/span.ts b/packages/core/src/tracing/span.ts index ec92ce23f646..b436d0bd64e7 100644 --- a/packages/core/src/tracing/span.ts +++ b/packages/core/src/tracing/span.ts @@ -126,6 +126,8 @@ export class Span implements SpanInterface { protected _sampled: boolean | undefined; protected _name?: string; + private _logMessage?: string; + /** * You should never call the constructor manually, always use `Sentry.startTransaction()` * or call `startChild()` on an existing span. @@ -286,8 +288,8 @@ export class Span implements SpanInterface { const idStr = childSpan.transaction.spanContext().spanId; const logMessage = `[Tracing] Starting '${opStr}' span on transaction '${nameStr}' (${idStr}).`; - childSpan.transaction.metadata.spanMetadata[childSpan.spanContext().spanId] = { logMessage }; logger.log(logMessage); + this._logMessage = logMessage; } return childSpan; @@ -383,7 +385,7 @@ export class Span implements SpanInterface { this.transaction && this.transaction.spanContext().spanId !== this._spanId ) { - const { logMessage } = this.transaction.metadata.spanMetadata[this._spanId]; + const logMessage = this._logMessage; if (logMessage) { logger.log((logMessage as string).replace('Starting', 'Finishing')); } diff --git a/packages/core/src/tracing/transaction.ts b/packages/core/src/tracing/transaction.ts index c531dff3ba7e..7d13038de6e0 100644 --- a/packages/core/src/tracing/transaction.ts +++ b/packages/core/src/tracing/transaction.ts @@ -15,14 +15,13 @@ import { dropUndefinedKeys, logger } from '@sentry/utils'; import { DEBUG_BUILD } from '../debug-build'; import type { Hub } from '../hub'; import { getCurrentHub } from '../hub'; +import { SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE, SEMANTIC_ATTRIBUTE_SENTRY_SOURCE } from '../semanticAttributes'; import { spanTimeInputToSeconds, spanToTraceContext } from '../utils/spanUtils'; import { getDynamicSamplingContextFromClient } from './dynamicSamplingContext'; import { Span as SpanClass, SpanRecorder } from './span'; /** JSDoc */ export class Transaction extends SpanClass implements TransactionInterface { - public metadata: TransactionMetadata; - /** * The reference to the current hub. */ @@ -38,6 +37,8 @@ export class Transaction extends SpanClass implements TransactionInterface { private _frozenDynamicSamplingContext: Readonly> | undefined; + private _metadata: Partial; + /** * This constructor should never be called manually. Those instrumenting tracing should use * `Sentry.startTransaction()`, and internal methods should use `hub.startTransaction()`. @@ -54,10 +55,9 @@ export class Transaction extends SpanClass implements TransactionInterface { this._name = transactionContext.name || ''; - this.metadata = { - source: 'custom', + this._metadata = { + // eslint-disable-next-line deprecation/deprecation ...transactionContext.metadata, - spanMetadata: {}, }; this._trimEnd = transactionContext.trimEnd; @@ -67,13 +67,16 @@ export class Transaction extends SpanClass implements TransactionInterface { // If Dynamic Sampling Context is provided during the creation of the transaction, we freeze it as it usually means // there is incoming Dynamic Sampling Context. (Either through an incoming request, a baggage meta-tag, or other means) - const incomingDynamicSamplingContext = this.metadata.dynamicSamplingContext; + const incomingDynamicSamplingContext = this._metadata.dynamicSamplingContext; if (incomingDynamicSamplingContext) { // We shallow copy this in case anything writes to the original reference of the passed in `dynamicSamplingContext` this._frozenDynamicSamplingContext = { ...incomingDynamicSamplingContext }; } } + // This sadly conflicts with the getter/setter ordering :( + /* eslint-disable @typescript-eslint/member-ordering */ + /** * Getter for `name` property. * @deprecated Use `spanToJSON(span).description` instead. @@ -91,6 +94,41 @@ export class Transaction extends SpanClass implements TransactionInterface { this.setName(newName); } + /** + * Get the metadata for this transaction. + * @deprecated Use `spanGetMetadata(transaction)` instead. + */ + public get metadata(): TransactionMetadata { + // We merge attributes in for backwards compatibility + return { + // Defaults + // eslint-disable-next-line deprecation/deprecation + source: 'custom', + spanMetadata: {}, + + // Legacy metadata + ...this._metadata, + + // From attributes + ...(this.attributes[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE] && { + source: this.attributes[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE] as TransactionMetadata['source'], + }), + ...(this.attributes[SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE] && { + sampleRate: this.attributes[SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE] as TransactionMetadata['sampleRate'], + }), + }; + } + + /** + * Update the metadata for this transaction. + * @deprecated Use `spanGetMetadata(transaction)` instead. + */ + public set metadata(metadata: TransactionMetadata) { + this._metadata = metadata; + } + + /* eslint-enable @typescript-eslint/member-ordering */ + /** * Setter for `name` property, which also sets `source` on the metadata. * @@ -98,7 +136,7 @@ export class Transaction extends SpanClass implements TransactionInterface { */ public setName(name: string, source: TransactionMetadata['source'] = 'custom'): void { this._name = name; - this.metadata.source = source; + this.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, source); } /** @inheritdoc */ @@ -138,10 +176,11 @@ export class Transaction extends SpanClass implements TransactionInterface { } /** - * @inheritDoc + * Store metadata on this transaction. + * @deprecated Use attributes or store data on the scope instead. */ public setMetadata(newMetadata: Partial): void { - this.metadata = { ...this.metadata, ...newMetadata }; + this._metadata = { ...this._metadata, ...newMetadata }; } /** @@ -204,12 +243,14 @@ export class Transaction extends SpanClass implements TransactionInterface { const scope = hub.getScope(); const dsc = getDynamicSamplingContextFromClient(traceId, client, scope); + // eslint-disable-next-line deprecation/deprecation const maybeSampleRate = this.metadata.sampleRate; if (maybeSampleRate !== undefined) { dsc.sample_rate = `${maybeSampleRate}`; } // We don't want to have a transaction name in the DSC if the source is "url" because URLs might contain PII + // eslint-disable-next-line deprecation/deprecation const source = this.metadata.source; if (source && source !== 'url') { dsc.transaction = this._name; @@ -279,7 +320,10 @@ export class Transaction extends SpanClass implements TransactionInterface { }).endTimestamp; } - const metadata = this.metadata; + // eslint-disable-next-line deprecation/deprecation + const { metadata } = this; + // eslint-disable-next-line deprecation/deprecation + const { source } = metadata; const transaction: TransactionEvent = { contexts: { @@ -298,9 +342,9 @@ export class Transaction extends SpanClass implements TransactionInterface { ...metadata, dynamicSamplingContext: this.getDynamicSamplingContext(), }, - ...(metadata.source && { + ...(source && { transaction_info: { - source: metadata.source, + source, }, }), }; diff --git a/packages/core/test/lib/tracing/transaction.test.ts b/packages/core/test/lib/tracing/transaction.test.ts index d0fded3c0f04..415c0448f78e 100644 --- a/packages/core/test/lib/tracing/transaction.test.ts +++ b/packages/core/test/lib/tracing/transaction.test.ts @@ -1,4 +1,4 @@ -import { Transaction } from '../../../src'; +import { SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE, SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, Transaction } from '../../../src'; describe('transaction', () => { describe('name', () => { @@ -10,7 +10,7 @@ describe('transaction', () => { it('allows to update the name via setter', () => { const transaction = new Transaction({ name: 'span name' }); - transaction.setMetadata({ source: 'route' }); + transaction.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, 'route'); expect(transaction.name).toEqual('span name'); transaction.name = 'new name'; @@ -21,12 +21,9 @@ describe('transaction', () => { it('allows to update the name via setName', () => { const transaction = new Transaction({ name: 'span name' }); - transaction.setMetadata({ source: 'route' }); + transaction.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, 'route'); expect(transaction.name).toEqual('span name'); - transaction.setMetadata({ source: 'route' }); - - // eslint-disable-next-line deprecation/deprecation transaction.setName('new name'); expect(transaction.name).toEqual('new name'); @@ -35,7 +32,7 @@ describe('transaction', () => { it('allows to update the name via updateName', () => { const transaction = new Transaction({ name: 'span name' }); - transaction.setMetadata({ source: 'route' }); + transaction.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, 'route'); expect(transaction.name).toEqual('span name'); transaction.updateName('new name'); @@ -45,4 +42,66 @@ describe('transaction', () => { }); /* eslint-enable deprecation/deprecation */ }); + + describe('metadata', () => { + /* eslint-disable deprecation/deprecation */ + it('works with defaults', () => { + const transaction = new Transaction({ name: 'span name' }); + expect(transaction.metadata).toEqual({ + source: 'custom', + spanMetadata: {}, + }); + }); + + it('allows to set metadata in constructor', () => { + const transaction = new Transaction({ name: 'span name', metadata: { source: 'url', request: {} } }); + expect(transaction.metadata).toEqual({ + source: 'url', + spanMetadata: {}, + request: {}, + }); + }); + + it('allows to set source & sample rate data in constructor', () => { + const transaction = new Transaction({ + name: 'span name', + metadata: { request: {} }, + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'url', + [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: 0.5, + }, + }); + expect(transaction.metadata).toEqual({ + source: 'url', + sampleRate: 0.5, + spanMetadata: {}, + request: {}, + }); + }); + + it('allows to update metadata via setMetadata', () => { + const transaction = new Transaction({ name: 'span name', metadata: { source: 'url', request: {} } }); + + transaction.setMetadata({ source: 'route' }); + + expect(transaction.metadata).toEqual({ + source: 'route', + spanMetadata: {}, + request: {}, + }); + }); + + it('allows to update metadata via setAttribute', () => { + const transaction = new Transaction({ name: 'span name', metadata: { source: 'url', request: {} } }); + + transaction.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, 'route'); + + expect(transaction.metadata).toEqual({ + source: 'route', + spanMetadata: {}, + request: {}, + }); + }); + /* eslint-enable deprecation/deprecation */ + }); }); diff --git a/packages/nextjs/src/common/utils/edgeWrapperUtils.ts b/packages/nextjs/src/common/utils/edgeWrapperUtils.ts index 9c479a88ceeb..59114ddee709 100644 --- a/packages/nextjs/src/common/utils/edgeWrapperUtils.ts +++ b/packages/nextjs/src/common/utils/edgeWrapperUtils.ts @@ -1,4 +1,11 @@ -import { addTracingExtensions, captureException, continueTrace, handleCallbackErrors, startSpan } from '@sentry/core'; +import { + SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, + addTracingExtensions, + captureException, + continueTrace, + handleCallbackErrors, + startSpan, +} from '@sentry/core'; import { winterCGRequestToRequestData } from '@sentry/utils'; import type { EdgeRouteHandler } from '../../edge/types'; @@ -34,10 +41,11 @@ export function withEdgeWrapping( name: options.spanDescription, op: options.spanOp, origin: 'auto.function.nextjs.withEdgeWrapping', + attributes: { [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route' }, metadata: { + // eslint-disable-next-line deprecation/deprecation ...transactionContext.metadata, request: req instanceof Request ? winterCGRequestToRequestData(req) : undefined, - source: 'route', }, }, async span => { diff --git a/packages/nextjs/src/common/utils/wrapperUtils.ts b/packages/nextjs/src/common/utils/wrapperUtils.ts index be00cb4c7a2a..5911fdf79694 100644 --- a/packages/nextjs/src/common/utils/wrapperUtils.ts +++ b/packages/nextjs/src/common/utils/wrapperUtils.ts @@ -1,5 +1,6 @@ import type { IncomingMessage, ServerResponse } from 'http'; import { + SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, captureException, getActiveTransaction, getCurrentScope, @@ -207,7 +208,7 @@ export async function callDataFetcherTraced Promis // Logic will be: If there is no active transaction, start one with correct name and source. If there is an active // transaction, create a child span with correct name and source. transaction.updateName(parameterizedRoute); - transaction.metadata.source = 'route'; + transaction.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, 'route'); // Capture the route, since pre-loading, revalidation, etc might mean that this span may happen during another // route's transaction diff --git a/packages/nextjs/src/common/wrapApiHandlerWithSentry.ts b/packages/nextjs/src/common/wrapApiHandlerWithSentry.ts index fc8f602f524b..16228aa0cda8 100644 --- a/packages/nextjs/src/common/wrapApiHandlerWithSentry.ts +++ b/packages/nextjs/src/common/wrapApiHandlerWithSentry.ts @@ -1,4 +1,5 @@ import { + SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, addTracingExtensions, captureException, continueTrace, @@ -108,9 +109,12 @@ export function withSentry(apiHandler: NextApiHandler, parameterizedRoute?: stri name: `${reqMethod}${reqPath}`, op: 'http.server', origin: 'auto.http.nextjs', + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route', + }, metadata: { + // eslint-disable-next-line deprecation/deprecation ...transactionContext.metadata, - source: 'route', request: req, }, }, diff --git a/packages/nextjs/src/common/wrapGenerationFunctionWithSentry.ts b/packages/nextjs/src/common/wrapGenerationFunctionWithSentry.ts index fe90b6f6ca39..f2e829704dd6 100644 --- a/packages/nextjs/src/common/wrapGenerationFunctionWithSentry.ts +++ b/packages/nextjs/src/common/wrapGenerationFunctionWithSentry.ts @@ -1,4 +1,5 @@ import { + SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, addTracingExtensions, captureException, continueTrace, @@ -69,9 +70,12 @@ export function wrapGenerationFunctionWithSentry a origin: 'auto.function.nextjs', ...transactionContext, data, + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'url', + }, metadata: { + // eslint-disable-next-line deprecation/deprecation ...transactionContext.metadata, - source: 'url', request: { headers: headers ? winterCGHeadersToDict(headers) : undefined, }, diff --git a/packages/nextjs/src/common/wrapServerComponentWithSentry.ts b/packages/nextjs/src/common/wrapServerComponentWithSentry.ts index 427879b3e843..a0a1ae2f77aa 100644 --- a/packages/nextjs/src/common/wrapServerComponentWithSentry.ts +++ b/packages/nextjs/src/common/wrapServerComponentWithSentry.ts @@ -1,4 +1,5 @@ import { + SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, addTracingExtensions, captureException, continueTrace, @@ -61,12 +62,15 @@ export function wrapServerComponentWithSentry any> name: `${componentType} Server Component (${componentRoute})`, status: 'ok', origin: 'auto.function.nextjs', + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'component', + }, metadata: { + // eslint-disable-next-line deprecation/deprecation ...transactionContext.metadata, request: { headers: completeHeadersDict, }, - source: 'component', }, }, span => { diff --git a/packages/nextjs/test/config/withSentry.test.ts b/packages/nextjs/test/config/withSentry.test.ts index da43ec724944..91b61516a240 100644 --- a/packages/nextjs/test/config/withSentry.test.ts +++ b/packages/nextjs/test/config/withSentry.test.ts @@ -1,5 +1,5 @@ import * as SentryCore from '@sentry/core'; -import { addTracingExtensions } from '@sentry/core'; +import { SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, addTracingExtensions } from '@sentry/core'; import type { NextApiRequest, NextApiResponse } from 'next'; import type { AugmentedNextApiResponse, NextApiHandler } from '../../src/common/types'; @@ -45,8 +45,10 @@ describe('withSentry', () => { name: 'GET http://dogs.are.great', op: 'http.server', origin: 'auto.http.nextjs', + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route', + }, metadata: { - source: 'route', request: expect.objectContaining({ url: 'http://dogs.are.great' }), }, }, diff --git a/packages/nextjs/test/edge/edgeWrapperUtils.test.ts b/packages/nextjs/test/edge/edgeWrapperUtils.test.ts index 2a782250c7c5..97d6e7b103e1 100644 --- a/packages/nextjs/test/edge/edgeWrapperUtils.test.ts +++ b/packages/nextjs/test/edge/edgeWrapperUtils.test.ts @@ -1,4 +1,5 @@ import * as coreSdk from '@sentry/core'; +import { SEMANTIC_ATTRIBUTE_SENTRY_SOURCE } from '@sentry/core'; import { withEdgeWrapping } from '../../src/common/utils/edgeWrapperUtils'; @@ -81,7 +82,12 @@ describe('withEdgeWrapping', () => { expect(startSpanSpy).toHaveBeenCalledTimes(1); expect(startSpanSpy).toHaveBeenCalledWith( expect.objectContaining({ - metadata: { request: { headers: {} }, source: 'route' }, + metadata: { + request: { headers: {} }, + }, + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route', + }, name: 'some label', op: 'some op', origin: 'auto.function.nextjs.withEdgeWrapping', diff --git a/packages/nextjs/test/edge/withSentryAPI.test.ts b/packages/nextjs/test/edge/withSentryAPI.test.ts index b51ce3dfeca6..ea5e7c4319f0 100644 --- a/packages/nextjs/test/edge/withSentryAPI.test.ts +++ b/packages/nextjs/test/edge/withSentryAPI.test.ts @@ -1,4 +1,5 @@ import * as coreSdk from '@sentry/core'; +import { SEMANTIC_ATTRIBUTE_SENTRY_SOURCE } from '@sentry/core'; import { wrapApiHandlerWithSentry } from '../../src/edge'; @@ -52,7 +53,12 @@ describe('wrapApiHandlerWithSentry', () => { expect(startSpanSpy).toHaveBeenCalledTimes(1); expect(startSpanSpy).toHaveBeenCalledWith( expect.objectContaining({ - metadata: { request: { headers: {}, method: 'POST', url: 'https://sentry.io/' }, source: 'route' }, + metadata: { + request: { headers: {}, method: 'POST', url: 'https://sentry.io/' }, + }, + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route', + }, name: 'POST /user/[userId]/post/[postId]', op: 'http.server', origin: 'auto.function.nextjs.withEdgeWrapping', @@ -71,7 +77,10 @@ describe('wrapApiHandlerWithSentry', () => { expect(startSpanSpy).toHaveBeenCalledTimes(1); expect(startSpanSpy).toHaveBeenCalledWith( expect.objectContaining({ - metadata: { source: 'route' }, + metadata: {}, + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route', + }, name: 'handler (/user/[userId]/post/[postId])', op: 'http.server', origin: 'auto.function.nextjs.withEdgeWrapping', diff --git a/packages/node/src/handlers.ts b/packages/node/src/handlers.ts index cc4877125507..6d4d5b81e295 100644 --- a/packages/node/src/handlers.ts +++ b/packages/node/src/handlers.ts @@ -1,6 +1,7 @@ import type * as http from 'http'; /* eslint-disable @typescript-eslint/no-explicit-any */ import { + SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, captureException, continueTrace, flush, @@ -71,14 +72,17 @@ export function tracingHandler(): ( op: 'http.server', origin: 'auto.http.node.tracingHandler', ...ctx, + data: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: source, + }, metadata: { + // eslint-disable-next-line deprecation/deprecation ...ctx.metadata, // The request should already have been stored in `scope.sdkProcessingMetadata` (which will become // `event.sdkProcessingMetadata` the same way the metadata here will) by `sentryRequestMiddleware`, but on the // off chance someone is using `sentryTracingMiddleware` without `sentryRequestMiddleware`, it doesn't hurt to // be sure request: req, - source, }, }, // extra context passed to the tracesSampler @@ -333,7 +337,7 @@ export function trpcMiddleware(options: SentryTrpcMiddlewareOptions = {}) { if (sentryTransaction) { sentryTransaction.updateName(`trpc/${path}`); - sentryTransaction.setMetadata({ source: 'route' }); + sentryTransaction.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, 'route'); sentryTransaction.op = 'rpc.server'; const trpcContext: Record = { diff --git a/packages/node/test/handlers.test.ts b/packages/node/test/handlers.test.ts index d234994455bf..6a25a1bcc4b0 100644 --- a/packages/node/test/handlers.test.ts +++ b/packages/node/test/handlers.test.ts @@ -467,6 +467,7 @@ describe('tracingHandler', () => { // eslint-disable-next-line deprecation/deprecation const transaction = sentryCore.getCurrentScope().getTransaction(); + // eslint-disable-next-line deprecation/deprecation expect(transaction?.metadata.request).toEqual(req); }); }); diff --git a/packages/opentelemetry-node/src/spanprocessor.ts b/packages/opentelemetry-node/src/spanprocessor.ts index cb80f342c3e4..b6ca1fe53254 100644 --- a/packages/opentelemetry-node/src/spanprocessor.ts +++ b/packages/opentelemetry-node/src/spanprocessor.ts @@ -2,7 +2,14 @@ import type { Context } from '@opentelemetry/api'; import { SpanKind, context, trace } from '@opentelemetry/api'; import { suppressTracing } from '@opentelemetry/core'; import type { Span as OtelSpan, SpanProcessor as OtelSpanProcessor } from '@opentelemetry/sdk-trace-base'; -import { Transaction, addEventProcessor, addTracingExtensions, getClient, getCurrentHub } from '@sentry/core'; +import { + SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, + Transaction, + addEventProcessor, + addTracingExtensions, + getClient, + getCurrentHub, +} from '@sentry/core'; import type { DynamicSamplingContext, Span as SentrySpan, TraceparentData, TransactionContext } from '@sentry/types'; import { logger } from '@sentry/utils'; @@ -219,7 +226,7 @@ function updateTransactionWithOtelData(transaction: Transaction, otelSpan: OtelS transaction.op = op; transaction.updateName(description); - transaction.setMetadata({ source }); + transaction.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, source); } function convertOtelTimeToSeconds([seconds, nano]: [number, number]): number { diff --git a/packages/opentelemetry-node/src/utils/spanData.ts b/packages/opentelemetry-node/src/utils/spanData.ts index 1cdbacf74955..5cec7ee0f93f 100644 --- a/packages/opentelemetry-node/src/utils/spanData.ts +++ b/packages/opentelemetry-node/src/utils/spanData.ts @@ -51,6 +51,7 @@ export function addOtelSpanData( if (sentrySpan instanceof Transaction) { if (metadata) { + // eslint-disable-next-line deprecation/deprecation sentrySpan.setMetadata(metadata); } diff --git a/packages/opentelemetry-node/test/spanprocessor.test.ts b/packages/opentelemetry-node/test/spanprocessor.test.ts index f8a9d47951e1..9920e12ee62a 100644 --- a/packages/opentelemetry-node/test/spanprocessor.test.ts +++ b/packages/opentelemetry-node/test/spanprocessor.test.ts @@ -623,6 +623,7 @@ describe('SentrySpanProcessor', () => { otelSpan.end(); + // eslint-disable-next-line deprecation/deprecation expect(sentrySpan?.transaction?.metadata.source).toBe('url'); }); }); @@ -638,6 +639,7 @@ describe('SentrySpanProcessor', () => { otelSpan.end(); + // eslint-disable-next-line deprecation/deprecation expect(sentrySpan?.transaction?.metadata.source).toBe('route'); }); }); @@ -653,6 +655,7 @@ describe('SentrySpanProcessor', () => { otelSpan.end(); + // eslint-disable-next-line deprecation/deprecation expect(sentrySpan?.transaction?.metadata.source).toBe('route'); }); }); diff --git a/packages/opentelemetry/test/custom/transaction.test.ts b/packages/opentelemetry/test/custom/transaction.test.ts index a088a082b41f..108e85f598a8 100644 --- a/packages/opentelemetry/test/custom/transaction.test.ts +++ b/packages/opentelemetry/test/custom/transaction.test.ts @@ -149,6 +149,7 @@ describe('startTranscation', () => { expect(transaction['_sampled']).toBe(undefined); expect(transaction.spanRecorder).toBeDefined(); expect(transaction.spanRecorder?.spans).toHaveLength(1); + // eslint-disable-next-line deprecation/deprecation expect(transaction.metadata).toEqual({ source: 'custom', spanMetadata: {}, @@ -177,7 +178,7 @@ describe('startTranscation', () => { }); expect(transaction).toBeInstanceOf(OpenTelemetryTransaction); - + // eslint-disable-next-line deprecation/deprecation expect(transaction.metadata).toEqual({ source: 'custom', spanMetadata: {}, diff --git a/packages/react/package.json b/packages/react/package.json index 437642015f78..faec21438a92 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -30,6 +30,7 @@ }, "dependencies": { "@sentry/browser": "7.92.0", + "@sentry/core": "7.92.0", "@sentry/types": "7.92.0", "@sentry/utils": "7.92.0", "hoist-non-react-statics": "^3.3.2" diff --git a/packages/react/src/reactrouter.tsx b/packages/react/src/reactrouter.tsx index 8a42c5ff96f1..04995ee4bc44 100644 --- a/packages/react/src/reactrouter.tsx +++ b/packages/react/src/reactrouter.tsx @@ -1,4 +1,5 @@ import { WINDOW } from '@sentry/browser'; +import { SEMANTIC_ATTRIBUTE_SENTRY_SOURCE } from '@sentry/core'; import type { Transaction, TransactionSource } from '@sentry/types'; import hoistNonReactStatics from 'hoist-non-react-statics'; import * as React from 'react'; @@ -166,7 +167,7 @@ export function withSentryRouting

, R extends React const WrappedRoute: React.FC

= (props: P) => { if (activeTransaction && props && props.computedMatch && props.computedMatch.isExact) { activeTransaction.updateName(props.computedMatch.path); - activeTransaction.setMetadata({ source: 'route' }); + activeTransaction.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, 'route'); } // @ts-expect-error Setting more specific React Component typing for `R` generic above diff --git a/packages/react/src/reactrouterv6.tsx b/packages/react/src/reactrouterv6.tsx index 920a6b4f8a0d..de87e5bb6881 100644 --- a/packages/react/src/reactrouterv6.tsx +++ b/packages/react/src/reactrouterv6.tsx @@ -2,6 +2,7 @@ // https://gist.github.com/wontondon/e8c4bdf2888875e4c755712e99279536 import { WINDOW } from '@sentry/browser'; +import { SEMANTIC_ATTRIBUTE_SENTRY_SOURCE } from '@sentry/core'; import type { Transaction, TransactionContext, TransactionSource } from '@sentry/types'; import { getNumberOfUrlSegments, logger } from '@sentry/utils'; import hoistNonReactStatics from 'hoist-non-react-statics'; @@ -136,7 +137,7 @@ function updatePageloadTransaction( if (activeTransaction && branches) { const [name, source] = getNormalizedName(routes, location, branches, basename); activeTransaction.updateName(name); - activeTransaction.setMetadata({ source }); + activeTransaction.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, source); } } diff --git a/packages/react/test/reactrouterv4.test.tsx b/packages/react/test/reactrouterv4.test.tsx index 2b06b3a196a5..5849bb688598 100644 --- a/packages/react/test/reactrouterv4.test.tsx +++ b/packages/react/test/reactrouterv4.test.tsx @@ -1,3 +1,4 @@ +import { SEMANTIC_ATTRIBUTE_SENTRY_SOURCE } from '@sentry/core'; import { act, render } from '@testing-library/react'; import { createMemoryHistory } from 'history-4'; // biome-ignore lint/nursery/noUnusedImports: Need React import for JSX @@ -12,7 +13,7 @@ describe('React Router v4', () => { startTransactionOnPageLoad?: boolean; startTransactionOnLocationChange?: boolean; routes?: RouteConfig[]; - }): [jest.Mock, any, { mockUpdateName: jest.Mock; mockFinish: jest.Mock; mockSetMetadata: jest.Mock }] { + }): [jest.Mock, any, { mockUpdateName: jest.Mock; mockFinish: jest.Mock; mockSetAttribute: jest.Mock }] { const options = { matchPath: _opts && _opts.routes !== undefined ? matchPath : undefined, routes: undefined, @@ -23,16 +24,16 @@ describe('React Router v4', () => { const history = createMemoryHistory(); const mockFinish = jest.fn(); const mockUpdateName = jest.fn(); - const mockSetMetadata = jest.fn(); + const mockSetAttribute = jest.fn(); const mockStartTransaction = jest .fn() - .mockReturnValue({ updateName: mockUpdateName, end: mockFinish, setMetadata: mockSetMetadata }); + .mockReturnValue({ updateName: mockUpdateName, end: mockFinish, setAttribute: mockSetAttribute }); reactRouterV4Instrumentation(history, options.routes, options.matchPath)( mockStartTransaction, options.startTransactionOnPageLoad, options.startTransactionOnLocationChange, ); - return [mockStartTransaction, history, { mockUpdateName, mockFinish, mockSetMetadata }]; + return [mockStartTransaction, history, { mockUpdateName, mockFinish, mockSetAttribute }]; } it('starts a pageload transaction when instrumentation is started', () => { @@ -169,7 +170,7 @@ describe('React Router v4', () => { }); it('normalizes transaction name with custom Route', () => { - const [mockStartTransaction, history, { mockUpdateName, mockSetMetadata }] = createInstrumentation(); + const [mockStartTransaction, history, { mockUpdateName, mockSetAttribute }] = createInstrumentation(); const SentryRoute = withSentryRouting(Route); const { getByText } = render( @@ -196,11 +197,11 @@ describe('React Router v4', () => { }); expect(mockUpdateName).toHaveBeenCalledTimes(2); expect(mockUpdateName).toHaveBeenLastCalledWith('/users/:userid'); - expect(mockSetMetadata).toHaveBeenCalledWith({ source: 'route' }); + expect(mockSetAttribute).toHaveBeenCalledWith(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, 'route'); }); it('normalizes nested transaction names with custom Route', () => { - const [mockStartTransaction, history, { mockUpdateName, mockSetMetadata }] = createInstrumentation(); + const [mockStartTransaction, history, { mockUpdateName, mockSetAttribute }] = createInstrumentation(); const SentryRoute = withSentryRouting(Route); const { getByText } = render( @@ -227,7 +228,7 @@ describe('React Router v4', () => { }); expect(mockUpdateName).toHaveBeenCalledTimes(2); expect(mockUpdateName).toHaveBeenLastCalledWith('/organizations/:orgid/v1/:teamid'); - expect(mockSetMetadata).toHaveBeenLastCalledWith({ source: 'route' }); + expect(mockSetAttribute).toHaveBeenLastCalledWith(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, 'route'); act(() => { history.push('/organizations/543'); @@ -244,7 +245,7 @@ describe('React Router v4', () => { }); expect(mockUpdateName).toHaveBeenCalledTimes(3); expect(mockUpdateName).toHaveBeenLastCalledWith('/organizations/:orgid'); - expect(mockSetMetadata).toHaveBeenLastCalledWith({ source: 'route' }); + expect(mockSetAttribute).toHaveBeenLastCalledWith(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, 'route'); }); it('matches with route object', () => { diff --git a/packages/react/test/reactrouterv5.test.tsx b/packages/react/test/reactrouterv5.test.tsx index fba57df9a5f8..c571b3590b8f 100644 --- a/packages/react/test/reactrouterv5.test.tsx +++ b/packages/react/test/reactrouterv5.test.tsx @@ -1,3 +1,4 @@ +import { SEMANTIC_ATTRIBUTE_SENTRY_SOURCE } from '@sentry/core'; import { act, render } from '@testing-library/react'; import { createMemoryHistory } from 'history-4'; // biome-ignore lint/nursery/noUnusedImports: Need React import for JSX @@ -12,7 +13,7 @@ describe('React Router v5', () => { startTransactionOnPageLoad?: boolean; startTransactionOnLocationChange?: boolean; routes?: RouteConfig[]; - }): [jest.Mock, any, { mockUpdateName: jest.Mock; mockFinish: jest.Mock; mockSetMetadata: jest.Mock }] { + }): [jest.Mock, any, { mockUpdateName: jest.Mock; mockFinish: jest.Mock; mockSetAttribute: jest.Mock }] { const options = { matchPath: _opts && _opts.routes !== undefined ? matchPath : undefined, routes: undefined, @@ -23,16 +24,16 @@ describe('React Router v5', () => { const history = createMemoryHistory(); const mockFinish = jest.fn(); const mockUpdateName = jest.fn(); - const mockSetMetadata = jest.fn(); + const mockSetAttribute = jest.fn(); const mockStartTransaction = jest .fn() - .mockReturnValue({ updateName: mockUpdateName, end: mockFinish, setMetadata: mockSetMetadata }); + .mockReturnValue({ updateName: mockUpdateName, end: mockFinish, setAttribute: mockSetAttribute }); reactRouterV5Instrumentation(history, options.routes, options.matchPath)( mockStartTransaction, options.startTransactionOnPageLoad, options.startTransactionOnLocationChange, ); - return [mockStartTransaction, history, { mockUpdateName, mockFinish, mockSetMetadata }]; + return [mockStartTransaction, history, { mockUpdateName, mockFinish, mockSetAttribute }]; } it('starts a pageload transaction when instrumentation is started', () => { @@ -169,7 +170,7 @@ describe('React Router v5', () => { }); it('normalizes transaction name with custom Route', () => { - const [mockStartTransaction, history, { mockUpdateName, mockSetMetadata }] = createInstrumentation(); + const [mockStartTransaction, history, { mockUpdateName, mockSetAttribute }] = createInstrumentation(); const SentryRoute = withSentryRouting(Route); const { getByText } = render( @@ -196,11 +197,11 @@ describe('React Router v5', () => { }); expect(mockUpdateName).toHaveBeenCalledTimes(2); expect(mockUpdateName).toHaveBeenLastCalledWith('/users/:userid'); - expect(mockSetMetadata).toHaveBeenLastCalledWith({ source: 'route' }); + expect(mockSetAttribute).toHaveBeenLastCalledWith(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, 'route'); }); it('normalizes nested transaction names with custom Route', () => { - const [mockStartTransaction, history, { mockUpdateName, mockSetMetadata }] = createInstrumentation(); + const [mockStartTransaction, history, { mockUpdateName, mockSetAttribute }] = createInstrumentation(); const SentryRoute = withSentryRouting(Route); const { getByText } = render( @@ -228,7 +229,7 @@ describe('React Router v5', () => { }); expect(mockUpdateName).toHaveBeenCalledTimes(2); expect(mockUpdateName).toHaveBeenLastCalledWith('/organizations/:orgid/v1/:teamid'); - expect(mockSetMetadata).toHaveBeenLastCalledWith({ source: 'route' }); + expect(mockSetAttribute).toHaveBeenLastCalledWith(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, 'route'); act(() => { history.push('/organizations/543'); diff --git a/packages/react/test/reactrouterv6.4.test.tsx b/packages/react/test/reactrouterv6.4.test.tsx index a89bb50e1f82..d6b9c0c45b49 100644 --- a/packages/react/test/reactrouterv6.4.test.tsx +++ b/packages/react/test/reactrouterv6.4.test.tsx @@ -1,3 +1,4 @@ +import { SEMANTIC_ATTRIBUTE_SENTRY_SOURCE } from '@sentry/core'; import { render } from '@testing-library/react'; import { Request } from 'node-fetch'; import * as React from 'react'; @@ -25,7 +26,7 @@ describe('React Router v6.4', () => { function createInstrumentation(_opts?: { startTransactionOnPageLoad?: boolean; startTransactionOnLocationChange?: boolean; - }): [jest.Mock, { mockUpdateName: jest.Mock; mockFinish: jest.Mock; mockSetMetadata: jest.Mock }] { + }): [jest.Mock, { mockUpdateName: jest.Mock; mockFinish: jest.Mock; mockSetAttribute: jest.Mock }] { const options = { matchPath: _opts ? matchPath : undefined, startTransactionOnLocationChange: true, @@ -34,10 +35,10 @@ describe('React Router v6.4', () => { }; const mockFinish = jest.fn(); const mockUpdateName = jest.fn(); - const mockSetMetadata = jest.fn(); + const mockSetAttribute = jest.fn(); const mockStartTransaction = jest .fn() - .mockReturnValue({ updateName: mockUpdateName, end: mockFinish, setMetadata: mockSetMetadata }); + .mockReturnValue({ updateName: mockUpdateName, end: mockFinish, setAttribute: mockSetAttribute }); reactRouterV6Instrumentation( React.useEffect, @@ -46,7 +47,7 @@ describe('React Router v6.4', () => { createRoutesFromChildren, matchRoutes, )(mockStartTransaction, options.startTransactionOnPageLoad, options.startTransactionOnLocationChange); - return [mockStartTransaction, { mockUpdateName, mockFinish, mockSetMetadata }]; + return [mockStartTransaction, { mockUpdateName, mockFinish, mockSetAttribute }]; } describe('wrapCreateBrowserRouter', () => { @@ -246,7 +247,7 @@ describe('React Router v6.4', () => { }); it('updates pageload transaction to a parameterized route', () => { - const [mockStartTransaction, { mockUpdateName, mockSetMetadata }] = createInstrumentation(); + const [mockStartTransaction, { mockUpdateName, mockSetAttribute }] = createInstrumentation(); const sentryCreateBrowserRouter = wrapCreateBrowserRouter(createMemoryRouter as CreateRouterFunction); const router = sentryCreateBrowserRouter( @@ -272,7 +273,7 @@ describe('React Router v6.4', () => { expect(mockStartTransaction).toHaveBeenCalledTimes(1); expect(mockUpdateName).toHaveBeenLastCalledWith('/about/:page'); - expect(mockSetMetadata).toHaveBeenCalledWith({ source: 'route' }); + expect(mockSetAttribute).toHaveBeenCalledWith(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, 'route'); }); it('works with `basename` option', () => { diff --git a/packages/react/test/reactrouterv6.test.tsx b/packages/react/test/reactrouterv6.test.tsx index 965ce134bb74..df30c4596dbf 100644 --- a/packages/react/test/reactrouterv6.test.tsx +++ b/packages/react/test/reactrouterv6.test.tsx @@ -1,3 +1,4 @@ +import { SEMANTIC_ATTRIBUTE_SENTRY_SOURCE } from '@sentry/core'; import { render } from '@testing-library/react'; import * as React from 'react'; import { @@ -21,7 +22,7 @@ describe('React Router v6', () => { function createInstrumentation(_opts?: { startTransactionOnPageLoad?: boolean; startTransactionOnLocationChange?: boolean; - }): [jest.Mock, { mockUpdateName: jest.Mock; mockFinish: jest.Mock; mockSetMetadata: jest.Mock }] { + }): [jest.Mock, { mockUpdateName: jest.Mock; mockFinish: jest.Mock; mockSetAttribute: jest.Mock }] { const options = { matchPath: _opts ? matchPath : undefined, startTransactionOnLocationChange: true, @@ -30,10 +31,10 @@ describe('React Router v6', () => { }; const mockFinish = jest.fn(); const mockUpdateName = jest.fn(); - const mockSetMetadata = jest.fn(); + const mockSetAttribute = jest.fn(); const mockStartTransaction = jest .fn() - .mockReturnValue({ updateName: mockUpdateName, end: mockFinish, setMetadata: mockSetMetadata }); + .mockReturnValue({ updateName: mockUpdateName, end: mockFinish, setAttribute: mockSetAttribute }); reactRouterV6Instrumentation( React.useEffect, @@ -42,7 +43,7 @@ describe('React Router v6', () => { createRoutesFromChildren, matchRoutes, )(mockStartTransaction, options.startTransactionOnPageLoad, options.startTransactionOnLocationChange); - return [mockStartTransaction, { mockUpdateName, mockFinish, mockSetMetadata }]; + return [mockStartTransaction, { mockUpdateName, mockFinish, mockSetAttribute }]; } describe('withSentryReactRouterV6Routing', () => { @@ -545,7 +546,7 @@ describe('React Router v6', () => { }); it('does not add double slashes to URLS', () => { - const [mockStartTransaction, { mockUpdateName, mockSetMetadata }] = createInstrumentation(); + const [mockStartTransaction, { mockUpdateName, mockSetAttribute }] = createInstrumentation(); const wrappedUseRoutes = wrapUseRoutes(useRoutes); const Routes = () => @@ -588,11 +589,11 @@ describe('React Router v6', () => { expect(mockStartTransaction).toHaveBeenCalledTimes(1); // should be /tests not //tests expect(mockUpdateName).toHaveBeenLastCalledWith('/tests'); - expect(mockSetMetadata).toHaveBeenCalledWith({ source: 'route' }); + expect(mockSetAttribute).toHaveBeenCalledWith(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, 'route'); }); it('handles wildcard routes properly', () => { - const [mockStartTransaction, { mockUpdateName, mockSetMetadata }] = createInstrumentation(); + const [mockStartTransaction, { mockUpdateName, mockSetAttribute }] = createInstrumentation(); const wrappedUseRoutes = wrapUseRoutes(useRoutes); const Routes = () => @@ -634,7 +635,7 @@ describe('React Router v6', () => { expect(mockStartTransaction).toHaveBeenCalledTimes(1); expect(mockUpdateName).toHaveBeenLastCalledWith('/tests/:testId/*'); - expect(mockSetMetadata).toHaveBeenCalledWith({ source: 'route' }); + expect(mockSetAttribute).toHaveBeenCalledWith(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, 'route'); }); }); }); diff --git a/packages/remix/src/client/performance.tsx b/packages/remix/src/client/performance.tsx index 597f6daae48f..fc395e8ddedc 100644 --- a/packages/remix/src/client/performance.tsx +++ b/packages/remix/src/client/performance.tsx @@ -1,3 +1,4 @@ +import { SEMANTIC_ATTRIBUTE_SENTRY_SOURCE } from '@sentry/core'; import type { ErrorBoundaryProps } from '@sentry/react'; import { WINDOW, withErrorBoundary } from '@sentry/react'; import type { Transaction, TransactionContext } from '@sentry/types'; @@ -126,7 +127,7 @@ export function withSentry

, R extends React.Co _useEffect(() => { if (activeTransaction && matches && matches.length) { activeTransaction.updateName(matches[matches.length - 1].id); - activeTransaction.setMetadata({ source: 'route' }); + activeTransaction.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, 'route'); } isBaseLocation = true; diff --git a/packages/replay/src/replay.ts b/packages/replay/src/replay.ts index 20e59da7a902..27513fe5643d 100644 --- a/packages/replay/src/replay.ts +++ b/packages/replay/src/replay.ts @@ -1,6 +1,12 @@ /* eslint-disable max-lines */ // TODO: We might want to split this file up import { EventType, record } from '@sentry-internal/rrweb'; -import { captureException, getClient, getCurrentScope, spanToJSON } from '@sentry/core'; +import { + SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, + captureException, + getClient, + getCurrentScope, + spanToJSON, +} from '@sentry/core'; import type { ReplayRecordingMode, Transaction } from '@sentry/types'; import { logger } from '@sentry/utils'; @@ -701,7 +707,10 @@ export class ReplayContainer implements ReplayContainerInterface { public getCurrentRoute(): string | undefined { // eslint-disable-next-line deprecation/deprecation const lastTransaction = this.lastTransaction || getCurrentScope().getTransaction(); - if (!lastTransaction || !['route', 'custom'].includes(lastTransaction.metadata.source)) { + + const attributes = (lastTransaction && spanToJSON(lastTransaction).data) || {}; + const source = attributes[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]; + if (!lastTransaction || !source || !['route', 'custom'].includes(source)) { return undefined; } diff --git a/packages/serverless/src/awslambda.ts b/packages/serverless/src/awslambda.ts index d9cbe177efc8..b2101d89150c 100644 --- a/packages/serverless/src/awslambda.ts +++ b/packages/serverless/src/awslambda.ts @@ -22,6 +22,7 @@ import { isString, logger } from '@sentry/utils'; import type { Context, Handler } from 'aws-lambda'; import { performance } from 'perf_hooks'; +import { SEMANTIC_ATTRIBUTE_SENTRY_SOURCE } from '@sentry/core'; import { AWSServices } from './awsservices'; import { DEBUG_BUILD } from './debug-build'; import { markEventUnhandled } from './utils'; @@ -348,9 +349,8 @@ export function wrapHandler( op: 'function.aws.lambda', origin: 'auto.function.serverless', ...continueTraceContext, - metadata: { - ...continueTraceContext.metadata, - source: 'component', + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'component', }, }, span => { diff --git a/packages/serverless/src/gcpfunction/cloud_events.ts b/packages/serverless/src/gcpfunction/cloud_events.ts index cde99b9707b5..92a3eb0e37e7 100644 --- a/packages/serverless/src/gcpfunction/cloud_events.ts +++ b/packages/serverless/src/gcpfunction/cloud_events.ts @@ -1,4 +1,4 @@ -import { handleCallbackErrors } from '@sentry/core'; +import { SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, handleCallbackErrors } from '@sentry/core'; import { captureException, flush, getCurrentScope, startSpanManual } from '@sentry/node'; import { logger } from '@sentry/utils'; @@ -36,7 +36,7 @@ function _wrapCloudEventFunction( name: context.type || '', op: 'function.gcp.cloud_event', origin: 'auto.function.serverless.gcp_cloud_event', - metadata: { source: 'component' }, + attributes: { [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'component' }, }, span => { const scope = getCurrentScope(); diff --git a/packages/serverless/src/gcpfunction/events.ts b/packages/serverless/src/gcpfunction/events.ts index 539c0ee80094..79c609e9108c 100644 --- a/packages/serverless/src/gcpfunction/events.ts +++ b/packages/serverless/src/gcpfunction/events.ts @@ -1,4 +1,4 @@ -import { handleCallbackErrors } from '@sentry/core'; +import { SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, handleCallbackErrors } from '@sentry/core'; import { captureException, flush, getCurrentScope, startSpanManual } from '@sentry/node'; import { logger } from '@sentry/utils'; @@ -39,7 +39,7 @@ function _wrapEventFunction name: context.eventType, op: 'function.gcp.event', origin: 'auto.function.serverless.gcp_event', - metadata: { source: 'component' }, + attributes: { [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'component' }, }, span => { const scope = getCurrentScope(); diff --git a/packages/serverless/src/gcpfunction/http.ts b/packages/serverless/src/gcpfunction/http.ts index 609a75c81c1e..41fa620779c7 100644 --- a/packages/serverless/src/gcpfunction/http.ts +++ b/packages/serverless/src/gcpfunction/http.ts @@ -1,4 +1,4 @@ -import { Transaction, handleCallbackErrors } from '@sentry/core'; +import { SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, Transaction, handleCallbackErrors } from '@sentry/core'; import type { AddRequestDataToEventOptions } from '@sentry/node'; import { continueTrace, startSpanManual } from '@sentry/node'; import { getCurrentScope } from '@sentry/node'; @@ -79,10 +79,8 @@ function _wrapHttpFunction(fn: HttpFunction, wrapOptions: Partial { diff --git a/packages/serverless/test/awslambda.test.ts b/packages/serverless/test/awslambda.test.ts index f2aa18e9cb8d..a3d20018a78a 100644 --- a/packages/serverless/test/awslambda.test.ts +++ b/packages/serverless/test/awslambda.test.ts @@ -1,4 +1,5 @@ // NOTE: I have no idea how to fix this right now, and don't want to waste more time, as it builds just fine — Kamil +import { SEMANTIC_ATTRIBUTE_SENTRY_SOURCE } from '@sentry/core'; import * as SentryNode from '@sentry/node'; import type { Event } from '@sentry/types'; import type { Callback, Handler } from 'aws-lambda'; @@ -206,7 +207,10 @@ describe('AWSLambda', () => { name: 'functionName', op: 'function.aws.lambda', origin: 'auto.function.serverless', - metadata: { source: 'component' }, + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'component', + }, + metadata: {}, }; expect(rv).toStrictEqual(42); @@ -233,7 +237,10 @@ describe('AWSLambda', () => { name: 'functionName', op: 'function.aws.lambda', origin: 'auto.function.serverless', - metadata: { source: 'component' }, + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'component', + }, + metadata: {}, }; expect(SentryNode.startSpanManual).toBeCalledWith(fakeTransactionContext, expect.any(Function)); @@ -274,11 +281,13 @@ describe('AWSLambda', () => { origin: 'auto.function.serverless', name: 'functionName', traceId: '12312012123120121231201212312012', + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'component', + }, metadata: { dynamicSamplingContext: { release: '2.12.1', }, - source: 'component', }, }), expect.any(Function), @@ -311,7 +320,10 @@ describe('AWSLambda', () => { traceId: '12312012123120121231201212312012', parentSpanId: '1121201211212012', parentSampled: false, - metadata: { dynamicSamplingContext: {}, source: 'component' }, + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'component', + }, + metadata: { dynamicSamplingContext: {} }, }; expect(SentryNode.startSpanManual).toBeCalledWith(fakeTransactionContext, expect.any(Function)); @@ -338,7 +350,10 @@ describe('AWSLambda', () => { name: 'functionName', op: 'function.aws.lambda', origin: 'auto.function.serverless', - metadata: { source: 'component' }, + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'component', + }, + metadata: {}, }; expect(rv).toStrictEqual(42); @@ -376,7 +391,10 @@ describe('AWSLambda', () => { name: 'functionName', op: 'function.aws.lambda', origin: 'auto.function.serverless', - metadata: { source: 'component' }, + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'component', + }, + metadata: {}, }; expect(SentryNode.startSpanManual).toBeCalledWith(fakeTransactionContext, expect.any(Function)); @@ -418,7 +436,10 @@ describe('AWSLambda', () => { name: 'functionName', op: 'function.aws.lambda', origin: 'auto.function.serverless', - metadata: { source: 'component' }, + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'component', + }, + metadata: {}, }; expect(rv).toStrictEqual(42); @@ -456,7 +477,10 @@ describe('AWSLambda', () => { name: 'functionName', op: 'function.aws.lambda', origin: 'auto.function.serverless', - metadata: { source: 'component' }, + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'component', + }, + metadata: {}, }; expect(SentryNode.startSpanManual).toBeCalledWith(fakeTransactionContext, expect.any(Function)); diff --git a/packages/serverless/test/gcpfunction.test.ts b/packages/serverless/test/gcpfunction.test.ts index 19a3a2565cdd..f4486415988b 100644 --- a/packages/serverless/test/gcpfunction.test.ts +++ b/packages/serverless/test/gcpfunction.test.ts @@ -2,6 +2,7 @@ import * as domain from 'domain'; import * as SentryNode from '@sentry/node'; import type { Event, Integration } from '@sentry/types'; +import { SEMANTIC_ATTRIBUTE_SENTRY_SOURCE } from '@sentry/core'; import * as Sentry from '../src'; import { wrapCloudEventFunction, wrapEventFunction, wrapHttpFunction } from '../src/gcpfunction'; import type { @@ -111,7 +112,10 @@ describe('GCPFunction', () => { name: 'POST /path', op: 'function.gcp.http', origin: 'auto.function.serverless.gcp_http', - metadata: { source: 'route' }, + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route', + }, + metadata: {}, }; expect(SentryNode.startSpanManual).toBeCalledWith(fakeTransactionContext, expect.any(Function)); @@ -141,11 +145,13 @@ describe('GCPFunction', () => { traceId: '12312012123120121231201212312012', parentSpanId: '1121201211212012', parentSampled: false, + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route', + }, metadata: { dynamicSamplingContext: { release: '2.12.1', }, - source: 'route', }, }; @@ -172,7 +178,10 @@ describe('GCPFunction', () => { traceId: '12312012123120121231201212312012', parentSpanId: '1121201211212012', parentSampled: false, - metadata: { dynamicSamplingContext: {}, source: 'route' }, + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route', + }, + metadata: { dynamicSamplingContext: {} }, }; expect(SentryNode.startSpanManual).toBeCalledWith(fakeTransactionContext, expect.any(Function)); @@ -251,7 +260,9 @@ describe('GCPFunction', () => { name: 'event.type', op: 'function.gcp.event', origin: 'auto.function.serverless.gcp_event', - metadata: { source: 'component' }, + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'component', + }, }; expect(SentryNode.startSpanManual).toBeCalledWith(fakeTransactionContext, expect.any(Function)); @@ -272,7 +283,9 @@ describe('GCPFunction', () => { name: 'event.type', op: 'function.gcp.event', origin: 'auto.function.serverless.gcp_event', - metadata: { source: 'component' }, + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'component', + }, }; expect(SentryNode.startSpanManual).toBeCalledWith(fakeTransactionContext, expect.any(Function)); @@ -298,7 +311,9 @@ describe('GCPFunction', () => { name: 'event.type', op: 'function.gcp.event', origin: 'auto.function.serverless.gcp_event', - metadata: { source: 'component' }, + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'component', + }, }; expect(SentryNode.startSpanManual).toBeCalledWith(fakeTransactionContext, expect.any(Function)); @@ -323,7 +338,9 @@ describe('GCPFunction', () => { name: 'event.type', op: 'function.gcp.event', origin: 'auto.function.serverless.gcp_event', - metadata: { source: 'component' }, + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'component', + }, }; expect(SentryNode.startSpanManual).toBeCalledWith(fakeTransactionContext, expect.any(Function)); @@ -346,7 +363,9 @@ describe('GCPFunction', () => { name: 'event.type', op: 'function.gcp.event', origin: 'auto.function.serverless.gcp_event', - metadata: { source: 'component' }, + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'component', + }, }; expect(SentryNode.startSpanManual).toBeCalledWith(fakeTransactionContext, expect.any(Function)); @@ -367,7 +386,9 @@ describe('GCPFunction', () => { name: 'event.type', op: 'function.gcp.event', origin: 'auto.function.serverless.gcp_event', - metadata: { source: 'component' }, + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'component', + }, }; expect(SentryNode.startSpanManual).toBeCalledWith(fakeTransactionContext, expect.any(Function)); @@ -389,7 +410,9 @@ describe('GCPFunction', () => { name: 'event.type', op: 'function.gcp.event', origin: 'auto.function.serverless.gcp_event', - metadata: { source: 'component' }, + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'component', + }, }; expect(SentryNode.startSpanManual).toBeCalledWith(fakeTransactionContext, expect.any(Function)); @@ -444,7 +467,9 @@ describe('GCPFunction', () => { name: 'event.type', op: 'function.gcp.cloud_event', origin: 'auto.function.serverless.gcp_cloud_event', - metadata: { source: 'component' }, + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'component', + }, }; expect(SentryNode.startSpanManual).toBeCalledWith(fakeTransactionContext, expect.any(Function)); @@ -465,7 +490,9 @@ describe('GCPFunction', () => { name: 'event.type', op: 'function.gcp.cloud_event', origin: 'auto.function.serverless.gcp_cloud_event', - metadata: { source: 'component' }, + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'component', + }, }; expect(SentryNode.startSpanManual).toBeCalledWith(fakeTransactionContext, expect.any(Function)); @@ -488,7 +515,9 @@ describe('GCPFunction', () => { name: 'event.type', op: 'function.gcp.cloud_event', origin: 'auto.function.serverless.gcp_cloud_event', - metadata: { source: 'component' }, + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'component', + }, }; expect(SentryNode.startSpanManual).toBeCalledWith(fakeTransactionContext, expect.any(Function)); @@ -509,7 +538,9 @@ describe('GCPFunction', () => { name: 'event.type', op: 'function.gcp.cloud_event', origin: 'auto.function.serverless.gcp_cloud_event', - metadata: { source: 'component' }, + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'component', + }, }; expect(SentryNode.startSpanManual).toBeCalledWith(fakeTransactionContext, expect.any(Function)); @@ -531,7 +562,9 @@ describe('GCPFunction', () => { name: 'event.type', op: 'function.gcp.cloud_event', origin: 'auto.function.serverless.gcp_cloud_event', - metadata: { source: 'component' }, + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'component', + }, }; expect(SentryNode.startSpanManual).toBeCalledWith(fakeTransactionContext, expect.any(Function)); diff --git a/packages/sveltekit/src/client/router.ts b/packages/sveltekit/src/client/router.ts index ab967a9d0f13..f6601eb940b2 100644 --- a/packages/sveltekit/src/client/router.ts +++ b/packages/sveltekit/src/client/router.ts @@ -1,4 +1,4 @@ -import { getActiveTransaction } from '@sentry/core'; +import { SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, getActiveTransaction } from '@sentry/core'; import { WINDOW } from '@sentry/svelte'; import type { Span, Transaction, TransactionContext } from '@sentry/types'; @@ -43,8 +43,8 @@ function instrumentPageload(startTransactionFn: (context: TransactionContext) => tags: { ...DEFAULT_TAGS, }, - metadata: { - source: 'url', + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'url', }, }); @@ -57,7 +57,7 @@ function instrumentPageload(startTransactionFn: (context: TransactionContext) => if (pageloadTransaction && routeId) { pageloadTransaction.updateName(routeId); - pageloadTransaction.setMetadata({ source: 'route' }); + pageloadTransaction.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, 'route'); } }); } @@ -106,7 +106,7 @@ function instrumentNavigations(startTransactionFn: (context: TransactionContext) name: parameterizedRouteDestination || rawRouteDestination || 'unknown', op: 'navigation', origin: 'auto.navigation.sveltekit', - metadata: { source: parameterizedRouteDestination ? 'route' : 'url' }, + attributes: { [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: parameterizedRouteDestination ? 'route' : 'url' }, tags: { ...DEFAULT_TAGS, }, diff --git a/packages/sveltekit/test/client/router.test.ts b/packages/sveltekit/test/client/router.test.ts index cfb8ceb14275..8c4c9187a7ff 100644 --- a/packages/sveltekit/test/client/router.test.ts +++ b/packages/sveltekit/test/client/router.test.ts @@ -6,6 +6,7 @@ import { vi } from 'vitest'; import { navigating, page } from '$app/stores'; +import { SEMANTIC_ATTRIBUTE_SENTRY_SOURCE } from '@sentry/core'; import { svelteKitRoutingInstrumentation } from '../../src/client/router'; // we have to overwrite the global mock from `vitest.setup.ts` here to reset the @@ -27,7 +28,7 @@ describe('sveltekitRoutingInstrumentation', () => { returnedTransaction = { ...txnCtx, updateName: vi.fn(), - setMetadata: vi.fn(), + setAttribute: vi.fn(), startChild: vi.fn().mockImplementation(ctx => { return { ...mockedRoutingSpan, ...ctx }; }), @@ -59,8 +60,8 @@ describe('sveltekitRoutingInstrumentation', () => { tags: { 'routing.instrumentation': '@sentry/sveltekit', }, - metadata: { - source: 'url', + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'url', }, }); @@ -71,7 +72,7 @@ describe('sveltekitRoutingInstrumentation', () => { // This should update the transaction name with the parameterized route: expect(returnedTransaction?.updateName).toHaveBeenCalledTimes(1); expect(returnedTransaction?.updateName).toHaveBeenCalledWith('testRoute'); - expect(returnedTransaction?.setMetadata).toHaveBeenCalledWith({ source: 'route' }); + expect(returnedTransaction?.setAttribute).toHaveBeenCalledWith(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, 'route'); }); it("doesn't start a pageload transaction if `startTransactionOnPageLoad` is false", () => { @@ -109,9 +110,7 @@ describe('sveltekitRoutingInstrumentation', () => { name: '/users/[id]', op: 'navigation', origin: 'auto.navigation.sveltekit', - metadata: { - source: 'route', - }, + attributes: { [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route' }, tags: { 'routing.instrumentation': '@sentry/sveltekit', }, @@ -161,9 +160,7 @@ describe('sveltekitRoutingInstrumentation', () => { name: '/users/[id]', op: 'navigation', origin: 'auto.navigation.sveltekit', - metadata: { - source: 'route', - }, + attributes: { [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route' }, tags: { 'routing.instrumentation': '@sentry/sveltekit', }, diff --git a/packages/tracing-internal/src/browser/browsertracing.ts b/packages/tracing-internal/src/browser/browsertracing.ts index 687f5c057ded..1c8723a9705b 100644 --- a/packages/tracing-internal/src/browser/browsertracing.ts +++ b/packages/tracing-internal/src/browser/browsertracing.ts @@ -1,6 +1,7 @@ /* eslint-disable max-lines */ import type { Hub, IdleTransaction } from '@sentry/core'; import { + SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, TRACING_DEFAULTS, addTracingExtensions, getActiveTransaction, @@ -318,6 +319,7 @@ export class BrowserTracing implements Integration { ...context, ...traceparentData, metadata: { + // eslint-disable-next-line deprecation/deprecation ...context.metadata, dynamicSamplingContext: traceparentData && !dynamicSamplingContext ? {} : dynamicSamplingContext, }, @@ -331,13 +333,21 @@ export class BrowserTracing implements Integration { const finalContext = modifiedContext === undefined ? { ...expandedContext, sampled: false } : modifiedContext; // If `beforeNavigate` set a custom name, record that fact + // eslint-disable-next-line deprecation/deprecation finalContext.metadata = finalContext.name !== expandedContext.name - ? { ...finalContext.metadata, source: 'custom' } - : finalContext.metadata; + ? // eslint-disable-next-line deprecation/deprecation + { ...finalContext.metadata, source: 'custom' } + : // eslint-disable-next-line deprecation/deprecation + finalContext.metadata; this._latestRouteName = finalContext.name; - this._latestRouteSource = finalContext.metadata && finalContext.metadata.source; + + const sourceFromData = context.data && context.data[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]; + // eslint-disable-next-line deprecation/deprecation + const sourceFromMetadata = finalContext.metadata && finalContext.metadata.source; + + this._latestRouteSource = sourceFromData || sourceFromMetadata; // eslint-disable-next-line deprecation/deprecation if (finalContext.sampled === false) { @@ -423,8 +433,8 @@ export class BrowserTracing implements Integration { name: this._latestRouteName, op, trimEnd: true, - metadata: { - source: this._latestRouteSource || 'url', + data: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: this._latestRouteSource || 'url', }, }; diff --git a/packages/tracing-internal/src/node/integrations/express.ts b/packages/tracing-internal/src/node/integrations/express.ts index 0c7f938b56fb..7dabd973252e 100644 --- a/packages/tracing-internal/src/node/integrations/express.ts +++ b/packages/tracing-internal/src/node/integrations/express.ts @@ -1,4 +1,5 @@ /* eslint-disable max-lines */ +import { SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, spanToJSON } from '@sentry/core'; import type { Hub, Integration, PolymorphicRequest, Transaction } from '@sentry/types'; import { GLOBAL_OBJ, @@ -374,14 +375,15 @@ function instrumentRouter(appOrRouter: ExpressRouter): void { } const transaction = res.__sentry_transaction; - if (transaction && transaction.metadata.source !== 'custom') { + const attributes = (transaction && spanToJSON(transaction).data) || {}; + if (transaction && attributes[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE] !== 'custom') { // If the request URL is '/' or empty, the reconstructed route will be empty. // Therefore, we fall back to setting the final route to '/' in this case. const finalRoute = req._reconstructedRoute || '/'; const [name, source] = extractPathForTransaction(req, { path: true, method: true, customRoute: finalRoute }); transaction.updateName(name); - transaction.setMetadata({ source }); + transaction.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, source); } } diff --git a/packages/tracing/test/hub.test.ts b/packages/tracing/test/hub.test.ts index f7cf93cc5e32..a7fbf5e86b62 100644 --- a/packages/tracing/test/hub.test.ts +++ b/packages/tracing/test/hub.test.ts @@ -1,7 +1,7 @@ /* eslint-disable deprecation/deprecation */ /* eslint-disable @typescript-eslint/unbound-method */ import { BrowserClient } from '@sentry/browser'; -import { Hub, makeMain } from '@sentry/core'; +import { Hub, SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE, makeMain } from '@sentry/core'; import * as utilsModule from '@sentry/utils'; // for mocking import { logger } from '@sentry/utils'; @@ -16,7 +16,7 @@ import { addExtensionMethods(); const mathRandom = jest.spyOn(Math, 'random'); -jest.spyOn(Transaction.prototype, 'setMetadata'); +jest.spyOn(Transaction.prototype, 'setAttribute'); jest.spyOn(logger, 'warn'); jest.spyOn(logger, 'log'); jest.spyOn(logger, 'error'); @@ -286,9 +286,7 @@ describe('Hub', () => { makeMain(hub); hub.startTransaction({ name: 'dogpark', sampled: true }); - expect(Transaction.prototype.setMetadata).toHaveBeenCalledWith({ - sampleRate: 1.0, - }); + expect(Transaction.prototype.setAttribute).toHaveBeenCalledWith(SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE, 1); }); it('should record sampling method and rate when sampling decision comes from tracesSampler', () => { @@ -298,9 +296,7 @@ describe('Hub', () => { makeMain(hub); hub.startTransaction({ name: 'dogpark' }); - expect(Transaction.prototype.setMetadata).toHaveBeenCalledWith({ - sampleRate: 0.1121, - }); + expect(Transaction.prototype.setAttribute).toHaveBeenCalledWith(SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE, 0.1121); }); it('should record sampling method when sampling decision is inherited', () => { @@ -309,7 +305,7 @@ describe('Hub', () => { makeMain(hub); hub.startTransaction({ name: 'dogpark', parentSampled: true }); - expect(Transaction.prototype.setMetadata).toHaveBeenCalledTimes(0); + expect(Transaction.prototype.setAttribute).toHaveBeenCalledTimes(0); }); it('should record sampling method and rate when sampling decision comes from traceSampleRate', () => { @@ -318,9 +314,7 @@ describe('Hub', () => { makeMain(hub); hub.startTransaction({ name: 'dogpark' }); - expect(Transaction.prototype.setMetadata).toHaveBeenCalledWith({ - sampleRate: 0.1121, - }); + expect(Transaction.prototype.setAttribute).toHaveBeenCalledWith(SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE, 0.1121); }); }); diff --git a/packages/tracing/test/span.test.ts b/packages/tracing/test/span.test.ts index 8413b975de03..d47919d7acdb 100644 --- a/packages/tracing/test/span.test.ts +++ b/packages/tracing/test/span.test.ts @@ -1,6 +1,6 @@ /* eslint-disable deprecation/deprecation */ import { BrowserClient } from '@sentry/browser'; -import { Hub, Scope, makeMain } from '@sentry/core'; +import { Hub, SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, Scope, makeMain } from '@sentry/core'; import type { BaseTransportOptions, ClientOptions, TransactionSource } from '@sentry/types'; import { Span, TRACEPARENT_REGEXP, Transaction } from '../src'; @@ -645,9 +645,7 @@ describe('Span', () => { test('is included when transaction metadata is set', () => { const spy = jest.spyOn(hub as any, 'captureEvent') as any; const transaction = hub.startTransaction({ name: 'test', sampled: true }); - transaction.setMetadata({ - source: 'url', - }); + transaction.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, 'url'); expect(spy).toHaveBeenCalledTimes(0); transaction.end(); diff --git a/packages/tracing/test/transaction.test.ts b/packages/tracing/test/transaction.test.ts index 12c6c799883a..3d048c9a3c3f 100644 --- a/packages/tracing/test/transaction.test.ts +++ b/packages/tracing/test/transaction.test.ts @@ -1,5 +1,6 @@ /* eslint-disable deprecation/deprecation */ import { BrowserClient, Hub } from '@sentry/browser'; +import { SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE, SEMANTIC_ATTRIBUTE_SENTRY_SOURCE } from '@sentry/core'; import { Transaction, addExtensionMethods } from '../src'; import { getDefaultBrowserClientOptions } from './testutils'; @@ -65,7 +66,7 @@ describe('`Transaction` class', () => { describe('`updateName` method', () => { it('does not change the source', () => { const transaction = new Transaction({ name: 'dogpark' }); - transaction.setMetadata({ source: 'route' }); + transaction.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, 'route'); transaction.updateName('ballpit'); expect(transaction.name).toEqual('ballpit'); @@ -162,6 +163,7 @@ describe('`Transaction` class', () => { contexts: { foo: { key: 'val' }, trace: { + data: { [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: 1 }, span_id: transaction.spanId, trace_id: transaction.traceId, origin: 'manual', @@ -189,6 +191,7 @@ describe('`Transaction` class', () => { expect.objectContaining({ contexts: { trace: { + data: { [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: 1 }, span_id: transaction.spanId, trace_id: transaction.traceId, origin: 'manual', diff --git a/packages/types/src/transaction.ts b/packages/types/src/transaction.ts index c144c5a1281d..d07c5ce435c1 100644 --- a/packages/types/src/transaction.ts +++ b/packages/types/src/transaction.ts @@ -30,6 +30,7 @@ export interface TransactionContext extends SpanContext { /** * Metadata associated with the transaction, for internal SDK use. + * @deprecated Use attributes or store data on the scope instead. */ metadata?: Partial; } @@ -88,7 +89,8 @@ export interface Transaction extends TransactionContext, Omit): void; @@ -174,7 +176,10 @@ export interface SamplingContext extends CustomSamplingContext { } export interface TransactionMetadata { - /** The sample rate used when sampling this transaction */ + /** + * The sample rate used when sampling this transaction. + * @deprecated Use `SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE` attribute instead. + */ sampleRate?: number; /** @@ -196,10 +201,16 @@ export interface TransactionMetadata { /** TODO: If we rm -rf `instrumentServer`, this can go, too */ requestPath?: string; - /** Information on how a transaction name was generated. */ + /** + * Information on how a transaction name was generated. + * @deprecated Use `SEMANTIC_ATTRIBUTE_SENTRY_SOURCE` attribute instead. + */ source: TransactionSource; - /** Metadata for the transaction's spans, keyed by spanId */ + /** + * Metadata for the transaction's spans, keyed by spanId. + * @deprecated This will be removed in v8. + */ spanMetadata: { [spanId: string]: { [key: string]: unknown } }; } diff --git a/packages/utils/src/requestdata.ts b/packages/utils/src/requestdata.ts index 0249b7a2b481..5e96a5077020 100644 --- a/packages/utils/src/requestdata.ts +++ b/packages/utils/src/requestdata.ts @@ -72,10 +72,13 @@ export function addRequestDataToTransaction( deps?: InjectedNodeDeps, ): void { if (!transaction) return; + // eslint-disable-next-line deprecation/deprecation if (!transaction.metadata.source || transaction.metadata.source === 'url') { // Attempt to grab a parameterized route off of the request const [name, source] = extractPathForTransaction(req, { path: true, method: true }); transaction.updateName(name); + // TODO: SEMANTIC_ATTRIBUTE_SENTRY_SOURCE is in core, align this once we merge utils & core + // eslint-disable-next-line deprecation/deprecation transaction.setMetadata({ source }); } transaction.setData('url', req.originalUrl || req.url); diff --git a/packages/vue/src/router.ts b/packages/vue/src/router.ts index 5acc19bb825c..07bb9e8fe1ed 100644 --- a/packages/vue/src/router.ts +++ b/packages/vue/src/router.ts @@ -1,4 +1,5 @@ import { WINDOW, captureException } from '@sentry/browser'; +import { SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, spanToJSON } from '@sentry/core'; import type { Transaction, TransactionContext, TransactionSource } from '@sentry/types'; import { getActiveTransaction } from './tracing'; @@ -72,8 +73,8 @@ export function vueRouterInstrumentation( op: 'pageload', origin: 'auto.pageload.vue', tags, - metadata: { - source: 'url', + data: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'url', }, }); } @@ -91,7 +92,7 @@ export function vueRouterInstrumentation( // hence only '==' instead of '===', because `undefined == null` evaluates to `true` const isPageLoadNavigation = from.name == null && from.matched.length === 0; - const data = { + const data: Record = { params: to.params, query: to.query, }; @@ -111,9 +112,10 @@ export function vueRouterInstrumentation( // eslint-disable-next-line deprecation/deprecation const pageloadTransaction = getActiveTransaction(); if (pageloadTransaction) { - if (pageloadTransaction.metadata.source !== 'custom') { + const attributes = spanToJSON(pageloadTransaction).data || {}; + if (attributes[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE] !== 'custom') { pageloadTransaction.updateName(transactionName); - pageloadTransaction.setMetadata({ source: transactionSource }); + pageloadTransaction.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, transactionSource); } pageloadTransaction.setData('params', data.params); pageloadTransaction.setData('query', data.query); @@ -121,15 +123,13 @@ export function vueRouterInstrumentation( } if (startTransactionOnLocationChange && !isPageLoadNavigation) { + data[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE] = transactionSource; startTransaction({ name: transactionName, op: 'navigation', origin: 'auto.navigation.vue', tags, data, - metadata: { - source: transactionSource, - }, }); } diff --git a/packages/vue/test/router.test.ts b/packages/vue/test/router.test.ts index 2e937e02f154..061bcdd3e1f9 100644 --- a/packages/vue/test/router.test.ts +++ b/packages/vue/test/router.test.ts @@ -1,4 +1,5 @@ import * as SentryBrowser from '@sentry/browser'; +import { SEMANTIC_ATTRIBUTE_SENTRY_SOURCE } from '@sentry/core'; import type { Transaction } from '@sentry/types'; import { vueRouterInstrumentation } from '../src'; @@ -100,10 +101,8 @@ describe('vueRouterInstrumentation()', () => { expect(mockStartTransaction).toHaveBeenCalledTimes(2); expect(mockStartTransaction).toHaveBeenCalledWith({ name: transactionName, - metadata: { - source: transactionSource, - }, data: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: transactionSource, params: to.params, query: to.query, }, @@ -128,7 +127,7 @@ describe('vueRouterInstrumentation()', () => { const mockedTxn = { updateName: jest.fn(), setData: jest.fn(), - setMetadata: jest.fn(), + setAttribute: jest.fn(), metadata: {}, }; const customMockStartTxn = { ...mockStartTransaction }.mockImplementation(_ => { @@ -146,8 +145,8 @@ describe('vueRouterInstrumentation()', () => { expect(customMockStartTxn).toHaveBeenCalledTimes(1); expect(customMockStartTxn).toHaveBeenCalledWith({ name: '/', - metadata: { - source: 'url', + data: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'url', }, op: 'pageload', origin: 'auto.pageload.vue', @@ -165,7 +164,7 @@ describe('vueRouterInstrumentation()', () => { expect(mockVueRouter.beforeEach).toHaveBeenCalledTimes(1); expect(mockedTxn.updateName).toHaveBeenCalledWith(transactionName); - expect(mockedTxn.setMetadata).toHaveBeenCalledWith({ source: transactionSource }); + expect(mockedTxn.setAttribute).toHaveBeenCalledWith(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, transactionSource); expect(mockedTxn.setData).toHaveBeenNthCalledWith(1, 'params', to.params); expect(mockedTxn.setData).toHaveBeenNthCalledWith(2, 'query', to.query); @@ -190,10 +189,8 @@ describe('vueRouterInstrumentation()', () => { // first startTx call happens when the instrumentation is initialized (for pageloads) expect(mockStartTransaction).toHaveBeenLastCalledWith({ name: '/login', - metadata: { - source: 'route', - }, data: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route', params: to.params, query: to.query, }, @@ -222,10 +219,8 @@ describe('vueRouterInstrumentation()', () => { // first startTx call happens when the instrumentation is initialized (for pageloads) expect(mockStartTransaction).toHaveBeenLastCalledWith({ name: 'login-screen', - metadata: { - source: 'custom', - }, data: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'custom', params: to.params, query: to.query, }, @@ -241,10 +236,13 @@ describe('vueRouterInstrumentation()', () => { const mockedTxn = { updateName: jest.fn(), setData: jest.fn(), + setAttribute: jest.fn(), name: '', - metadata: { - source: 'url', - }, + toJSON: () => ({ + data: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'url', + }, + }), }; const customMockStartTxn = { ...mockStartTransaction }.mockImplementation(_ => { return mockedTxn; @@ -261,8 +259,8 @@ describe('vueRouterInstrumentation()', () => { expect(customMockStartTxn).toHaveBeenCalledTimes(1); expect(customMockStartTxn).toHaveBeenCalledWith({ name: '/', - metadata: { - source: 'url', + data: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'url', }, op: 'pageload', origin: 'auto.pageload.vue', @@ -274,7 +272,11 @@ describe('vueRouterInstrumentation()', () => { // now we give the transaction a custom name, thereby simulating what would // happen when users use the `beforeNavigate` hook mockedTxn.name = 'customTxnName'; - mockedTxn.metadata.source = 'custom'; + mockedTxn.toJSON = () => ({ + data: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'custom', + }, + }); const beforeEachCallback = mockVueRouter.beforeEach.mock.calls[0][0]; beforeEachCallback(testRoutes['normalRoute1'], testRoutes['initialPageloadRoute'], mockNext); @@ -282,7 +284,7 @@ describe('vueRouterInstrumentation()', () => { expect(mockVueRouter.beforeEach).toHaveBeenCalledTimes(1); expect(mockedTxn.updateName).not.toHaveBeenCalled(); - expect(mockedTxn.metadata.source).toEqual('custom'); + expect(mockedTxn.setAttribute).not.toHaveBeenCalled(); expect(mockedTxn.name).toEqual('customTxnName'); }); @@ -344,10 +346,8 @@ describe('vueRouterInstrumentation()', () => { // first startTx call happens when the instrumentation is initialized (for pageloads) expect(mockStartTransaction).toHaveBeenLastCalledWith({ name: '/login', - metadata: { - source: 'route', - }, data: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route', params: to.params, query: to.query, }, From 89cef6bed4eee76ecca0f7018b558124665ee278 Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Tue, 9 Jan 2024 10:40:28 +0100 Subject: [PATCH 31/43] fix(core): Ensure we copy passed in span data/tags/attributes (#10105) So we do not mutate this if we update data there. Noticed this here: https://github.com/getsentry/sentry-javascript/pull/10097 --- packages/core/src/tracing/span.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/core/src/tracing/span.ts b/packages/core/src/tracing/span.ts index b436d0bd64e7..80a1a3da4d32 100644 --- a/packages/core/src/tracing/span.ts +++ b/packages/core/src/tracing/span.ts @@ -139,9 +139,9 @@ export class Span implements SpanInterface { this._traceId = spanContext.traceId || uuid4(); this._spanId = spanContext.spanId || uuid4().substring(16); this.startTimestamp = spanContext.startTimestamp || timestampInSeconds(); - this.tags = spanContext.tags || {}; - this.data = spanContext.data || {}; - this.attributes = spanContext.attributes || {}; + this.tags = spanContext.tags ? { ...spanContext.tags } : {}; + this.data = spanContext.data ? { ...spanContext.data } : {}; + this.attributes = spanContext.attributes ? { ...spanContext.attributes } : {}; this.instrumenter = spanContext.instrumenter || 'sentry'; this.origin = spanContext.origin || 'manual'; // eslint-disable-next-line deprecation/deprecation From d41e39e34e3afdae915893dbcf8abe6e3d4caf72 Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Tue, 9 Jan 2024 10:57:42 +0100 Subject: [PATCH 32/43] feat(core): Deprecate `scope.setTransactionName()` (#10113) This can be replaced by an event processor. We only use this in a single place right now. Closes https://github.com/getsentry/sentry-javascript/issues/5660 --- MIGRATION.md | 4 +++ .../suites/replay/dsc/test.ts | 20 ++++++++++++--- .../envelope-header-transaction-name/init.js | 5 +++- .../suites/tracing/envelope-header/init.js | 5 +++- packages/core/src/scope.ts | 7 ++++-- .../core/src/utils/applyScopeDataToEvent.ts | 12 ++++++++- packages/serverless/src/awslambda.ts | 5 +++- .../serverless/test/__mocks__/@sentry/node.ts | 2 -- packages/serverless/test/awslambda.test.ts | 25 ++++++++++++------- packages/types/src/scope.ts | 2 ++ 10 files changed, 66 insertions(+), 21 deletions(-) diff --git a/MIGRATION.md b/MIGRATION.md index fbff1464c634..75f6ce20b59f 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -8,6 +8,10 @@ npx @sentry/migr8@latest This will let you select which updates to run, and automatically update your code. Make sure to still review all code changes! +## Deprecate `scope.setTransactionName()` + +Instead, either set this as attributes or tags, or use an event processor to set `event.transaction`. + ## Deprecate `scope.getTransaction()` and `getActiveTransaction()` Instead, you should not rely on the active transaction, but just use `startSpan()` APIs, which handle this for you. diff --git a/dev-packages/browser-integration-tests/suites/replay/dsc/test.ts b/dev-packages/browser-integration-tests/suites/replay/dsc/test.ts index 4468a254bde4..63c103e51259 100644 --- a/dev-packages/browser-integration-tests/suites/replay/dsc/test.ts +++ b/dev-packages/browser-integration-tests/suites/replay/dsc/test.ts @@ -33,7 +33,10 @@ sentryTest( await page.evaluate(() => { const scope = (window as unknown as TestWindow).Sentry.getCurrentScope(); scope.setUser({ id: 'user123', segment: 'segmentB' }); - scope.setTransactionName('testTransactionDSC'); + scope.addEventProcessor(event => { + event.transaction = 'testTransactionDSC'; + return event; + }); }); const req0 = await transactionReq; @@ -78,7 +81,10 @@ sentryTest( await page.evaluate(() => { const scope = (window as unknown as TestWindow).Sentry.getCurrentScope(); scope.setUser({ id: 'user123', segment: 'segmentB' }); - scope.setTransactionName('testTransactionDSC'); + scope.addEventProcessor(event => { + event.transaction = 'testTransactionDSC'; + return event; + }); }); const req0 = await transactionReq; @@ -135,7 +141,10 @@ sentryTest( await page.evaluate(() => { const scope = (window as unknown as TestWindow).Sentry.getCurrentScope(); scope.setUser({ id: 'user123', segment: 'segmentB' }); - scope.setTransactionName('testTransactionDSC'); + scope.addEventProcessor(event => { + event.transaction = 'testTransactionDSC'; + return event; + }); }); const req0 = await transactionReq; @@ -183,7 +192,10 @@ sentryTest( await page.evaluate(async () => { const scope = (window as unknown as TestWindow).Sentry.getCurrentScope(); scope.setUser({ id: 'user123', segment: 'segmentB' }); - scope.setTransactionName('testTransactionDSC'); + scope.addEventProcessor(event => { + event.transaction = 'testTransactionDSC'; + return event; + }); }); const req0 = await transactionReq; diff --git a/dev-packages/browser-integration-tests/suites/tracing/envelope-header-transaction-name/init.js b/dev-packages/browser-integration-tests/suites/tracing/envelope-header-transaction-name/init.js index 7d000c0ac2cd..c2fcbb33a24c 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/envelope-header-transaction-name/init.js +++ b/dev-packages/browser-integration-tests/suites/tracing/envelope-header-transaction-name/init.js @@ -13,5 +13,8 @@ Sentry.init({ const scope = Sentry.getCurrentScope(); scope.setUser({ id: 'user123', segment: 'segmentB' }); -scope.setTransactionName('testTransactionDSC'); +scope.addEventProcessor(event => { + event.transaction = 'testTransactionDSC'; + return event; +}); scope.getTransaction().setMetadata({ source: 'custom' }); diff --git a/dev-packages/browser-integration-tests/suites/tracing/envelope-header/init.js b/dev-packages/browser-integration-tests/suites/tracing/envelope-header/init.js index f382a49c153d..1528306fcbde 100644 --- a/dev-packages/browser-integration-tests/suites/tracing/envelope-header/init.js +++ b/dev-packages/browser-integration-tests/suites/tracing/envelope-header/init.js @@ -13,4 +13,7 @@ Sentry.init({ const scope = Sentry.getCurrentScope(); scope.setUser({ id: 'user123', segment: 'segmentB' }); -scope.setTransactionName('testTransactionDSC'); +scope.addEventProcessor(event => { + event.transaction = 'testTransactionDSC'; + return event; +}); diff --git a/packages/core/src/scope.ts b/packages/core/src/scope.ts index 54bb9fc52b40..5d172ab95b60 100644 --- a/packages/core/src/scope.ts +++ b/packages/core/src/scope.ts @@ -89,7 +89,9 @@ export class Scope implements ScopeInterface { // eslint-disable-next-line deprecation/deprecation protected _level?: Severity | SeverityLevel; - /** Transaction Name */ + /** + * Transaction Name + */ protected _transactionName?: string; /** Span */ @@ -281,7 +283,8 @@ export class Scope implements ScopeInterface { } /** - * @inheritDoc + * Sets the transaction name on the scope for future events. + * @deprecated Use extra or tags instead. */ public setTransactionName(name?: string): this { this._transactionName = name; diff --git a/packages/core/src/utils/applyScopeDataToEvent.ts b/packages/core/src/utils/applyScopeDataToEvent.ts index d16b0b04806f..ef9796680cb9 100644 --- a/packages/core/src/utils/applyScopeDataToEvent.ts +++ b/packages/core/src/utils/applyScopeDataToEvent.ts @@ -37,6 +37,7 @@ export function mergeScopeData(data: ScopeData, mergeData: ScopeData): void { eventProcessors, attachments, propagationContext, + // eslint-disable-next-line deprecation/deprecation transactionName, span, } = mergeData; @@ -52,6 +53,7 @@ export function mergeScopeData(data: ScopeData, mergeData: ScopeData): void { } if (transactionName) { + // eslint-disable-next-line deprecation/deprecation data.transactionName = transactionName; } @@ -122,7 +124,15 @@ export function mergeArray( } function applyDataToEvent(event: Event, data: ScopeData): void { - const { extra, tags, user, contexts, level, transactionName } = data; + const { + extra, + tags, + user, + contexts, + level, + // eslint-disable-next-line deprecation/deprecation + transactionName, + } = data; if (extra && Object.keys(extra).length) { event.extra = { ...extra, ...event.extra }; diff --git a/packages/serverless/src/awslambda.ts b/packages/serverless/src/awslambda.ts index b2101d89150c..9cf05355ee93 100644 --- a/packages/serverless/src/awslambda.ts +++ b/packages/serverless/src/awslambda.ts @@ -224,7 +224,10 @@ function enhanceScopeWithEnvironmentData(scope: Scope, context: Context, startTi * @param context AWS Lambda context that will be used to extract some part of the data */ function enhanceScopeWithTransactionData(scope: Scope, context: Context): void { - scope.setTransactionName(context.functionName); + scope.addEventProcessor(event => { + event.transaction = context.functionName; + return event; + }); scope.setTag('server_name', process.env._AWS_XRAY_DAEMON_ADDRESS || process.env.SENTRY_NAME || hostname()); scope.setTag('url', `awslambda:///${context.functionName}`); } diff --git a/packages/serverless/test/__mocks__/@sentry/node.ts b/packages/serverless/test/__mocks__/@sentry/node.ts index d37bbbd2023c..fb929737f8d4 100644 --- a/packages/serverless/test/__mocks__/@sentry/node.ts +++ b/packages/serverless/test/__mocks__/@sentry/node.ts @@ -11,7 +11,6 @@ export const continueTrace = origSentry.continueTrace; export const fakeScope = { addEventProcessor: jest.fn(), - setTransactionName: jest.fn(), setTag: jest.fn(), setContext: jest.fn(), setSpan: jest.fn(), @@ -46,7 +45,6 @@ export const resetMocks = (): void => { fakeSpan.setHttpStatus.mockClear(); fakeScope.addEventProcessor.mockClear(); - fakeScope.setTransactionName.mockClear(); fakeScope.setTag.mockClear(); fakeScope.setContext.mockClear(); fakeScope.setSpan.mockClear(); diff --git a/packages/serverless/test/awslambda.test.ts b/packages/serverless/test/awslambda.test.ts index a3d20018a78a..0da204b31fa4 100644 --- a/packages/serverless/test/awslambda.test.ts +++ b/packages/serverless/test/awslambda.test.ts @@ -42,7 +42,14 @@ const fakeCallback: Callback = (err, result) => { function expectScopeSettings() { // @ts-expect-error see "Why @ts-expect-error" note - expect(SentryNode.fakeScope.setTransactionName).toBeCalledWith('functionName'); + expect(SentryNode.fakeScope.addEventProcessor).toBeCalledTimes(1); + // Test than an event processor to add `transaction` is registered for the scope + // @ts-expect-error see "Why @ts-expect-error" note + const eventProcessor = SentryNode.fakeScope.addEventProcessor.mock.calls[0][0]; + const event: Event = {}; + eventProcessor(event); + expect(event).toEqual({ transaction: 'functionName' }); + // @ts-expect-error see "Why @ts-expect-error" note expect(SentryNode.fakeScope.setTag).toBeCalledWith('server_name', expect.anything()); // @ts-expect-error see "Why @ts-expect-error" note @@ -186,7 +193,7 @@ describe('AWSLambda', () => { await wrappedHandler(fakeEvent, fakeContext, fakeCallback); // @ts-expect-error see "Why @ts-expect-error" note - expect(SentryNode.fakeScope.setTransactionName).toBeCalledTimes(0); + expect(SentryNode.fakeScope.addEventProcessor).toBeCalledTimes(0); // @ts-expect-error see "Why @ts-expect-error" note expect(SentryNode.fakeScope.setTag).toBeCalledTimes(0); expect(SentryNode.startSpanManual).toBeCalledTimes(0); @@ -195,7 +202,7 @@ describe('AWSLambda', () => { describe('wrapHandler() on sync handler', () => { test('successful execution', async () => { - expect.assertions(9); + expect.assertions(10); const handler: Handler = (_event, _context, callback) => { callback(null, 42); @@ -222,7 +229,7 @@ describe('AWSLambda', () => { }); test('unsuccessful execution', async () => { - expect.assertions(9); + expect.assertions(10); const error = new Error('sorry'); const handler: Handler = (_event, _context, callback) => { @@ -301,7 +308,7 @@ describe('AWSLambda', () => { }); test('capture error', async () => { - expect.assertions(9); + expect.assertions(10); const error = new Error('wat'); const handler: Handler = (_event, _context, _callback) => { @@ -338,7 +345,7 @@ describe('AWSLambda', () => { describe('wrapHandler() on async handler', () => { test('successful execution', async () => { - expect.assertions(9); + expect.assertions(10); const handler: Handler = async (_event, _context) => { return 42; @@ -376,7 +383,7 @@ describe('AWSLambda', () => { }); test('capture error', async () => { - expect.assertions(9); + expect.assertions(10); const error = new Error('wat'); const handler: Handler = async (_event, _context) => { @@ -424,7 +431,7 @@ describe('AWSLambda', () => { describe('wrapHandler() on async handler with a callback method (aka incorrect usage)', () => { test('successful execution', async () => { - expect.assertions(9); + expect.assertions(10); const handler: Handler = async (_event, _context, _callback) => { return 42; @@ -462,7 +469,7 @@ describe('AWSLambda', () => { }); test('capture error', async () => { - expect.assertions(9); + expect.assertions(10); const error = new Error('wat'); const handler: Handler = async (_event, _context, _callback) => { diff --git a/packages/types/src/scope.ts b/packages/types/src/scope.ts index 8d6c1a9d26aa..af94c30c2549 100644 --- a/packages/types/src/scope.ts +++ b/packages/types/src/scope.ts @@ -41,6 +41,7 @@ export interface ScopeData { sdkProcessingMetadata: { [key: string]: unknown }; fingerprint: string[]; level?: SeverityLevel; + /** @deprecated This will be removed in v8. */ transactionName?: string; span?: Span; } @@ -125,6 +126,7 @@ export interface Scope { /** * Sets the transaction name on the scope for future events. + * @deprecated Use extra or tags instead. */ setTransactionName(name?: string): this; From f70128d28a036883d10a6416821825fa682aa43b Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Tue, 9 Jan 2024 13:26:34 +0100 Subject: [PATCH 33/43] feat(core): Deprecate `scope.getSpan()` & `scope.setSpan()` (#10114) These APIs are not compatible with OTEL tracing, and thus will not be public API anymore in v8. In v8, we'll add new (?) methods to the scope that are "internal", as for non-node based SDKs we'll still need to keep the active span on the scope. But these should not be reflected publicly, and not in types' Scope. Note that we'll also need to make sure to use types' `Scope` for all callbacks etc., which we currently don't do. --- MIGRATION.md | 5 +++ .../create-next-app/pages/api/success.ts | 1 + .../tracing-new/apollo-graphql/scenario.ts | 1 + .../auto-instrument/mongodb/scenario.ts | 1 + .../mysql/withConnect/scenario.ts | 1 + .../mysql/withoutCallback/scenario.ts | 1 + .../mysql/withoutConnect/scenario.ts | 1 + .../auto-instrument/pg/scenario.ts | 1 + .../suites/tracing-new/prisma-orm/scenario.ts | 1 + .../tracePropagationTargets/scenario.ts | 1 + .../suites/tracing/apollo-graphql/scenario.ts | 1 + .../auto-instrument/mongodb/scenario.ts | 1 + .../tracing/auto-instrument/mysql/scenario.ts | 1 + .../tracing/auto-instrument/pg/scenario.ts | 1 + .../suites/tracing/prisma-orm/scenario.ts | 1 + .../tracePropagationTargets/scenario.ts | 1 + packages/astro/src/server/middleware.ts | 3 +- packages/astro/test/server/middleware.test.ts | 7 ++- packages/core/src/scope.ts | 9 ++-- packages/core/src/server-runtime-client.ts | 1 + packages/core/src/tracing/hubextensions.ts | 1 + packages/core/src/tracing/idletransaction.ts | 2 + packages/core/src/tracing/trace.ts | 16 +++++-- packages/core/test/lib/tracing/trace.test.ts | 44 +++++++++++-------- .../nextjs/src/common/utils/wrapperUtils.ts | 5 ++- .../src/edge/wrapApiHandlerWithSentry.ts | 4 +- packages/nextjs/test/clientSdk.test.ts | 1 + packages/node/src/handlers.ts | 5 ++- packages/node/src/integrations/hapi/index.ts | 1 + packages/node/src/integrations/http.ts | 3 +- .../node/src/integrations/undici/index.ts | 4 +- packages/node/test/integrations/http.test.ts | 13 ++++++ .../node/test/integrations/undici.test.ts | 4 +- .../test/spanprocessor.test.ts | 2 + .../opentelemetry/test/custom/scope.test.ts | 2 + packages/remix/src/utils/instrumentServer.ts | 7 ++- packages/sveltekit/src/server/handle.ts | 4 +- .../tracing-internal/src/browser/request.ts | 3 +- packages/tracing-internal/src/common/fetch.ts | 4 +- .../src/node/integrations/apollo.ts | 1 + .../src/node/integrations/graphql.ts | 4 ++ .../src/node/integrations/mongo.ts | 1 + .../src/node/integrations/mysql.ts | 1 + .../src/node/integrations/postgres.ts | 1 + .../test/browser/backgroundtab.test.ts | 1 + packages/tracing/test/idletransaction.test.ts | 16 +++++++ packages/types/src/scope.ts | 4 +- 47 files changed, 150 insertions(+), 44 deletions(-) diff --git a/MIGRATION.md b/MIGRATION.md index 75f6ce20b59f..c7587fc10c76 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -8,6 +8,11 @@ npx @sentry/migr8@latest This will let you select which updates to run, and automatically update your code. Make sure to still review all code changes! +## Deprecate `scope.getSpan()` and `scope.setSpan()` + +Instead, you can get the currently active span via `Sentry.getActiveSpan()`. +Setting a span on the scope happens automatically when you use the new performance APIs `startSpan()` and `startSpanManual()`. + ## Deprecate `scope.setTransactionName()` Instead, either set this as attributes or tags, or use an event processor to set `event.transaction`. diff --git a/dev-packages/e2e-tests/test-applications/create-next-app/pages/api/success.ts b/dev-packages/e2e-tests/test-applications/create-next-app/pages/api/success.ts index a54eb321c385..56eeb9189882 100644 --- a/dev-packages/e2e-tests/test-applications/create-next-app/pages/api/success.ts +++ b/dev-packages/e2e-tests/test-applications/create-next-app/pages/api/success.ts @@ -5,6 +5,7 @@ import type { NextApiRequest, NextApiResponse } from 'next'; export default function handler(req: NextApiRequest, res: NextApiResponse) { // eslint-disable-next-line deprecation/deprecation const transaction = Sentry.startTransaction({ name: 'test-transaction', op: 'e2e-test' }); + // eslint-disable-next-line deprecation/deprecation Sentry.getCurrentHub().getScope().setSpan(transaction); // eslint-disable-next-line deprecation/deprecation diff --git a/dev-packages/node-integration-tests/suites/tracing-new/apollo-graphql/scenario.ts b/dev-packages/node-integration-tests/suites/tracing-new/apollo-graphql/scenario.ts index 6e9ea3a5f097..1584274bce7d 100644 --- a/dev-packages/node-integration-tests/suites/tracing-new/apollo-graphql/scenario.ts +++ b/dev-packages/node-integration-tests/suites/tracing-new/apollo-graphql/scenario.ts @@ -30,6 +30,7 @@ const server = new ApolloServer({ // eslint-disable-next-line deprecation/deprecation const transaction = Sentry.startTransaction({ name: 'test_transaction', op: 'transaction' }); +// eslint-disable-next-line deprecation/deprecation Sentry.getCurrentScope().setSpan(transaction); // eslint-disable-next-line @typescript-eslint/no-floating-promises diff --git a/dev-packages/node-integration-tests/suites/tracing-new/auto-instrument/mongodb/scenario.ts b/dev-packages/node-integration-tests/suites/tracing-new/auto-instrument/mongodb/scenario.ts index 63949c64d531..67d8e13750de 100644 --- a/dev-packages/node-integration-tests/suites/tracing-new/auto-instrument/mongodb/scenario.ts +++ b/dev-packages/node-integration-tests/suites/tracing-new/auto-instrument/mongodb/scenario.ts @@ -22,6 +22,7 @@ async function run(): Promise { op: 'transaction', }); + // eslint-disable-next-line deprecation/deprecation Sentry.getCurrentScope().setSpan(transaction); try { diff --git a/dev-packages/node-integration-tests/suites/tracing-new/auto-instrument/mysql/withConnect/scenario.ts b/dev-packages/node-integration-tests/suites/tracing-new/auto-instrument/mysql/withConnect/scenario.ts index a1b2343e4a61..8273d4dfa96a 100644 --- a/dev-packages/node-integration-tests/suites/tracing-new/auto-instrument/mysql/withConnect/scenario.ts +++ b/dev-packages/node-integration-tests/suites/tracing-new/auto-instrument/mysql/withConnect/scenario.ts @@ -25,6 +25,7 @@ const transaction = Sentry.startTransaction({ name: 'Test Transaction', }); +// eslint-disable-next-line deprecation/deprecation Sentry.getCurrentScope().setSpan(transaction); connection.query('SELECT 1 + 1 AS solution', function () { diff --git a/dev-packages/node-integration-tests/suites/tracing-new/auto-instrument/mysql/withoutCallback/scenario.ts b/dev-packages/node-integration-tests/suites/tracing-new/auto-instrument/mysql/withoutCallback/scenario.ts index aa6d8ca9ade3..9a436695d63f 100644 --- a/dev-packages/node-integration-tests/suites/tracing-new/auto-instrument/mysql/withoutCallback/scenario.ts +++ b/dev-packages/node-integration-tests/suites/tracing-new/auto-instrument/mysql/withoutCallback/scenario.ts @@ -25,6 +25,7 @@ const transaction = Sentry.startTransaction({ name: 'Test Transaction', }); +// eslint-disable-next-line deprecation/deprecation Sentry.getCurrentScope().setSpan(transaction); const query = connection.query('SELECT 1 + 1 AS solution'); diff --git a/dev-packages/node-integration-tests/suites/tracing-new/auto-instrument/mysql/withoutConnect/scenario.ts b/dev-packages/node-integration-tests/suites/tracing-new/auto-instrument/mysql/withoutConnect/scenario.ts index 9a4b2dc4141a..d88b2d1c8d24 100644 --- a/dev-packages/node-integration-tests/suites/tracing-new/auto-instrument/mysql/withoutConnect/scenario.ts +++ b/dev-packages/node-integration-tests/suites/tracing-new/auto-instrument/mysql/withoutConnect/scenario.ts @@ -19,6 +19,7 @@ const transaction = Sentry.startTransaction({ name: 'Test Transaction', }); +// eslint-disable-next-line deprecation/deprecation Sentry.getCurrentScope().setSpan(transaction); connection.query('SELECT 1 + 1 AS solution', function () { diff --git a/dev-packages/node-integration-tests/suites/tracing-new/auto-instrument/pg/scenario.ts b/dev-packages/node-integration-tests/suites/tracing-new/auto-instrument/pg/scenario.ts index ad90c387ed20..47fb37e054f7 100644 --- a/dev-packages/node-integration-tests/suites/tracing-new/auto-instrument/pg/scenario.ts +++ b/dev-packages/node-integration-tests/suites/tracing-new/auto-instrument/pg/scenario.ts @@ -14,6 +14,7 @@ const transaction = Sentry.startTransaction({ name: 'Test Transaction', }); +// eslint-disable-next-line deprecation/deprecation Sentry.getCurrentScope().setSpan(transaction); const client = new pg.Client(); diff --git a/dev-packages/node-integration-tests/suites/tracing-new/prisma-orm/scenario.ts b/dev-packages/node-integration-tests/suites/tracing-new/prisma-orm/scenario.ts index 26c3b81f6e44..6191dbf31d75 100644 --- a/dev-packages/node-integration-tests/suites/tracing-new/prisma-orm/scenario.ts +++ b/dev-packages/node-integration-tests/suites/tracing-new/prisma-orm/scenario.ts @@ -19,6 +19,7 @@ async function run(): Promise { op: 'transaction', }); + // eslint-disable-next-line deprecation/deprecation Sentry.getCurrentScope().setSpan(transaction); try { diff --git a/dev-packages/node-integration-tests/suites/tracing-new/tracePropagationTargets/scenario.ts b/dev-packages/node-integration-tests/suites/tracing-new/tracePropagationTargets/scenario.ts index 2a8a34dae4f9..600b5ef71038 100644 --- a/dev-packages/node-integration-tests/suites/tracing-new/tracePropagationTargets/scenario.ts +++ b/dev-packages/node-integration-tests/suites/tracing-new/tracePropagationTargets/scenario.ts @@ -13,6 +13,7 @@ Sentry.init({ // eslint-disable-next-line deprecation/deprecation const transaction = Sentry.startTransaction({ name: 'test_transaction' }); +// eslint-disable-next-line deprecation/deprecation Sentry.getCurrentScope().setSpan(transaction); http.get('http://match-this-url.com/api/v0'); diff --git a/dev-packages/node-integration-tests/suites/tracing/apollo-graphql/scenario.ts b/dev-packages/node-integration-tests/suites/tracing/apollo-graphql/scenario.ts index aac11a641032..6a699fa07af7 100644 --- a/dev-packages/node-integration-tests/suites/tracing/apollo-graphql/scenario.ts +++ b/dev-packages/node-integration-tests/suites/tracing/apollo-graphql/scenario.ts @@ -32,6 +32,7 @@ const server = new ApolloServer({ // eslint-disable-next-line deprecation/deprecation const transaction = Sentry.startTransaction({ name: 'test_transaction', op: 'transaction' }); +// eslint-disable-next-line deprecation/deprecation Sentry.getCurrentScope().setSpan(transaction); // eslint-disable-next-line @typescript-eslint/no-floating-promises diff --git a/dev-packages/node-integration-tests/suites/tracing/auto-instrument/mongodb/scenario.ts b/dev-packages/node-integration-tests/suites/tracing/auto-instrument/mongodb/scenario.ts index f11e7f39d923..51359ac726da 100644 --- a/dev-packages/node-integration-tests/suites/tracing/auto-instrument/mongodb/scenario.ts +++ b/dev-packages/node-integration-tests/suites/tracing/auto-instrument/mongodb/scenario.ts @@ -23,6 +23,7 @@ async function run(): Promise { op: 'transaction', }); + // eslint-disable-next-line deprecation/deprecation Sentry.getCurrentScope().setSpan(transaction); try { diff --git a/dev-packages/node-integration-tests/suites/tracing/auto-instrument/mysql/scenario.ts b/dev-packages/node-integration-tests/suites/tracing/auto-instrument/mysql/scenario.ts index de77c5fa7c8a..ce53d776fe54 100644 --- a/dev-packages/node-integration-tests/suites/tracing/auto-instrument/mysql/scenario.ts +++ b/dev-packages/node-integration-tests/suites/tracing/auto-instrument/mysql/scenario.ts @@ -26,6 +26,7 @@ const transaction = Sentry.startTransaction({ name: 'Test Transaction', }); +// eslint-disable-next-line deprecation/deprecation Sentry.getCurrentScope().setSpan(transaction); connection.query('SELECT 1 + 1 AS solution', function () { diff --git a/dev-packages/node-integration-tests/suites/tracing/auto-instrument/pg/scenario.ts b/dev-packages/node-integration-tests/suites/tracing/auto-instrument/pg/scenario.ts index 1963c61fc31b..f9bfa0de0294 100644 --- a/dev-packages/node-integration-tests/suites/tracing/auto-instrument/pg/scenario.ts +++ b/dev-packages/node-integration-tests/suites/tracing/auto-instrument/pg/scenario.ts @@ -15,6 +15,7 @@ const transaction = Sentry.startTransaction({ name: 'Test Transaction', }); +// eslint-disable-next-line deprecation/deprecation Sentry.getCurrentScope().setSpan(transaction); const client = new pg.Client(); diff --git a/dev-packages/node-integration-tests/suites/tracing/prisma-orm/scenario.ts b/dev-packages/node-integration-tests/suites/tracing/prisma-orm/scenario.ts index 087d5ce860f2..b5003141caec 100644 --- a/dev-packages/node-integration-tests/suites/tracing/prisma-orm/scenario.ts +++ b/dev-packages/node-integration-tests/suites/tracing/prisma-orm/scenario.ts @@ -21,6 +21,7 @@ async function run(): Promise { op: 'transaction', }); + // eslint-disable-next-line deprecation/deprecation Sentry.getCurrentScope().setSpan(transaction); try { diff --git a/dev-packages/node-integration-tests/suites/tracing/tracePropagationTargets/scenario.ts b/dev-packages/node-integration-tests/suites/tracing/tracePropagationTargets/scenario.ts index f1e395992b46..8ecb7ed3cc61 100644 --- a/dev-packages/node-integration-tests/suites/tracing/tracePropagationTargets/scenario.ts +++ b/dev-packages/node-integration-tests/suites/tracing/tracePropagationTargets/scenario.ts @@ -15,6 +15,7 @@ Sentry.init({ // eslint-disable-next-line deprecation/deprecation const transaction = Sentry.startTransaction({ name: 'test_transaction' }); +// eslint-disable-next-line deprecation/deprecation Sentry.getCurrentScope().setSpan(transaction); http.get('http://match-this-url.com/api/v0'); diff --git a/packages/astro/src/server/middleware.ts b/packages/astro/src/server/middleware.ts index 79ac0b09a987..d5cc61b73e95 100644 --- a/packages/astro/src/server/middleware.ts +++ b/packages/astro/src/server/middleware.ts @@ -2,6 +2,7 @@ import { SEMANTIC_ATTRIBUTE_SENTRY_SOURCE } from '@sentry/core'; import { captureException, continueTrace, + getActiveSpan, getClient, getCurrentScope, runWithAsyncContext, @@ -70,7 +71,7 @@ export const handleRequest: (options?: MiddlewareOptions) => MiddlewareResponseH // if there is an active span, we know that this handle call is nested and hence // we don't create a new domain for it. If we created one, nested server calls would // create new transactions instead of adding a child span to the currently active span. - if (getCurrentScope().getSpan()) { + if (getActiveSpan()) { return instrumentRequest(ctx, next, handlerOptions); } return runWithAsyncContext(() => { diff --git a/packages/astro/test/server/middleware.test.ts b/packages/astro/test/server/middleware.test.ts index 03a4d2ee1dc4..9fa5bc430c90 100644 --- a/packages/astro/test/server/middleware.test.ts +++ b/packages/astro/test/server/middleware.test.ts @@ -1,6 +1,6 @@ import { SEMANTIC_ATTRIBUTE_SENTRY_SOURCE } from '@sentry/core'; import * as SentryNode from '@sentry/node'; -import type { Client } from '@sentry/types'; +import type { Client, Span } from '@sentry/types'; import { vi } from 'vitest'; import { handleRequest, interpolateRouteFromUrlAndParams } from '../../src/server/middleware'; @@ -15,7 +15,9 @@ vi.mock('../../src/server/meta', () => ({ describe('sentryMiddleware', () => { const startSpanSpy = vi.spyOn(SentryNode, 'startSpan'); - const getSpanMock = vi.fn(() => {}); + const getSpanMock = vi.fn(() => { + return {} as Span | undefined; + }); const setUserMock = vi.fn(); beforeEach(() => { @@ -26,6 +28,7 @@ describe('sentryMiddleware', () => { getSpan: getSpanMock, } as any; }); + vi.spyOn(SentryNode, 'getActiveSpan').mockImplementation(getSpanMock); vi.spyOn(SentryNode, 'getClient').mockImplementation(() => ({}) as Client); }); diff --git a/packages/core/src/scope.ts b/packages/core/src/scope.ts index 5d172ab95b60..255af68dd48e 100644 --- a/packages/core/src/scope.ts +++ b/packages/core/src/scope.ts @@ -308,7 +308,9 @@ export class Scope implements ScopeInterface { } /** - * @inheritDoc + * Sets the Span on the scope. + * @param span Span + * @deprecated Instead of setting a span on a scope, use `startSpan()`/`startSpanManual()` instead. */ public setSpan(span?: Span): this { this._span = span; @@ -317,7 +319,8 @@ export class Scope implements ScopeInterface { } /** - * @inheritDoc + * Returns the `Span` if there is one. + * @deprecated Use `getActiveSpan()` instead. */ public getSpan(): Span | undefined { return this._span; @@ -330,7 +333,7 @@ export class Scope implements ScopeInterface { public getTransaction(): Transaction | undefined { // Often, this span (if it exists at all) will be a transaction, but it's not guaranteed to be. Regardless, it will // have a pointer to the currently-active transaction. - const span = this.getSpan(); + const span = this._span; return span && span.transaction; } diff --git a/packages/core/src/server-runtime-client.ts b/packages/core/src/server-runtime-client.ts index 44b1eea5d388..b21e162c5b1c 100644 --- a/packages/core/src/server-runtime-client.ts +++ b/packages/core/src/server-runtime-client.ts @@ -255,6 +255,7 @@ export class ServerRuntimeClient< return [undefined, undefined]; } + // eslint-disable-next-line deprecation/deprecation const span = scope.getSpan(); if (span) { const samplingContext = span.transaction ? span.transaction.getDynamicSamplingContext() : undefined; diff --git a/packages/core/src/tracing/hubextensions.ts b/packages/core/src/tracing/hubextensions.ts index fa4fd078b9ed..c33f80db150d 100644 --- a/packages/core/src/tracing/hubextensions.ts +++ b/packages/core/src/tracing/hubextensions.ts @@ -13,6 +13,7 @@ import { Transaction } from './transaction'; /** Returns all trace headers that are currently on the top scope. */ function traceHeaders(this: Hub): { [key: string]: string } { const scope = this.getScope(); + // eslint-disable-next-line deprecation/deprecation const span = scope.getSpan(); return span diff --git a/packages/core/src/tracing/idletransaction.ts b/packages/core/src/tracing/idletransaction.ts index 4643e6b0c97b..af567d5dcd22 100644 --- a/packages/core/src/tracing/idletransaction.ts +++ b/packages/core/src/tracing/idletransaction.ts @@ -124,6 +124,7 @@ export class IdleTransaction extends Transaction { // We set the transaction here on the scope so error events pick up the trace // context and attach it to the error. DEBUG_BUILD && logger.log(`Setting idle transaction on scope. Span ID: ${this.spanContext().spanId}`); + // eslint-disable-next-line deprecation/deprecation _idleHub.getScope().setSpan(this); } @@ -198,6 +199,7 @@ export class IdleTransaction extends Transaction { const scope = this._idleHub.getScope(); // eslint-disable-next-line deprecation/deprecation if (scope.getTransaction() === this) { + // eslint-disable-next-line deprecation/deprecation scope.setSpan(undefined); } } diff --git a/packages/core/src/tracing/trace.ts b/packages/core/src/tracing/trace.ts index b244d54254f0..015bf4757b8f 100644 --- a/packages/core/src/tracing/trace.ts +++ b/packages/core/src/tracing/trace.ts @@ -143,11 +143,13 @@ export function trace( ): T { const hub = getCurrentHub(); const scope = getCurrentScope(); + // eslint-disable-next-line deprecation/deprecation const parentSpan = scope.getSpan(); const ctx = normalizeContext(context); const activeSpan = createChildSpanOrTransaction(hub, parentSpan, ctx); + // eslint-disable-next-line deprecation/deprecation scope.setSpan(activeSpan); return handleCallbackErrors( @@ -158,6 +160,7 @@ export function trace( }, () => { activeSpan && activeSpan.end(); + // eslint-disable-next-line deprecation/deprecation scope.setSpan(parentSpan); afterFinish(); }, @@ -180,10 +183,11 @@ export function startSpan(context: StartSpanOptions, callback: (span: Span | return withScope(context.scope, scope => { const hub = getCurrentHub(); - const scopeForSpan = context.scope || scope; - const parentSpan = scopeForSpan.getSpan(); + // eslint-disable-next-line deprecation/deprecation + const parentSpan = scope.getSpan(); const activeSpan = createChildSpanOrTransaction(hub, parentSpan, ctx); + // eslint-disable-next-line deprecation/deprecation scope.setSpan(activeSpan); return handleCallbackErrors( @@ -223,9 +227,11 @@ export function startSpanManual( return withScope(context.scope, scope => { const hub = getCurrentHub(); + // eslint-disable-next-line deprecation/deprecation const parentSpan = scope.getSpan(); const activeSpan = createChildSpanOrTransaction(hub, parentSpan, ctx); + // eslint-disable-next-line deprecation/deprecation scope.setSpan(activeSpan); function finishAndSetSpan(): void { @@ -261,7 +267,10 @@ export function startInactiveSpan(context: StartSpanOptions): Span | undefined { const ctx = normalizeContext(context); const hub = getCurrentHub(); - const parentSpan = context.scope ? context.scope.getSpan() : getActiveSpan(); + const parentSpan = context.scope + ? // eslint-disable-next-line deprecation/deprecation + context.scope.getSpan() + : getActiveSpan(); return parentSpan ? // eslint-disable-next-line deprecation/deprecation parentSpan.startChild(ctx) @@ -273,6 +282,7 @@ export function startInactiveSpan(context: StartSpanOptions): Span | undefined { * Returns the currently active span. */ export function getActiveSpan(): Span | undefined { + // eslint-disable-next-line deprecation/deprecation return getCurrentScope().getSpan(); } diff --git a/packages/core/test/lib/tracing/trace.test.ts b/packages/core/test/lib/tracing/trace.test.ts index 1924af197ecf..d2a9ea8034d9 100644 --- a/packages/core/test/lib/tracing/trace.test.ts +++ b/packages/core/test/lib/tracing/trace.test.ts @@ -1,6 +1,13 @@ import { Hub, addTracingExtensions, getCurrentScope, makeMain } from '../../../src'; import { Scope } from '../../../src/scope'; -import { Span, continueTrace, startInactiveSpan, startSpan, startSpanManual } from '../../../src/tracing'; +import { + Span, + continueTrace, + getActiveSpan, + startInactiveSpan, + startSpan, + startSpanManual, +} from '../../../src/tracing'; import { TestClient, getDefaultTestClientOptions } from '../../mocks/client'; beforeAll(() => { @@ -196,11 +203,11 @@ describe('startSpan', () => { startSpan({ name: 'GET users/[id]' }, span => { expect(getCurrentScope()).not.toBe(initialScope); - expect(getCurrentScope().getSpan()).toBe(span); + expect(getActiveSpan()).toBe(span); }); expect(getCurrentScope()).toBe(initialScope); - expect(initialScope.getSpan()).toBe(undefined); + expect(getActiveSpan()).toBe(undefined); }); it('allows to pass a scope', () => { @@ -208,18 +215,19 @@ describe('startSpan', () => { const manualScope = new Scope(); const parentSpan = new Span({ spanId: 'parent-span-id' }); + // eslint-disable-next-line deprecation/deprecation manualScope.setSpan(parentSpan); startSpan({ name: 'GET users/[id]', scope: manualScope }, span => { expect(getCurrentScope()).not.toBe(initialScope); expect(getCurrentScope()).toBe(manualScope); - expect(getCurrentScope().getSpan()).toBe(span); + expect(getActiveSpan()).toBe(span); expect(span?.parentSpanId).toBe('parent-span-id'); }); expect(getCurrentScope()).toBe(initialScope); - expect(initialScope.getSpan()).toBe(undefined); + expect(getActiveSpan()).toBe(undefined); }); }); @@ -238,16 +246,16 @@ describe('startSpanManual', () => { startSpanManual({ name: 'GET users/[id]' }, (span, finish) => { expect(getCurrentScope()).not.toBe(initialScope); - expect(getCurrentScope().getSpan()).toBe(span); + expect(getActiveSpan()).toBe(span); finish(); // Is still the active span - expect(getCurrentScope().getSpan()).toBe(span); + expect(getActiveSpan()).toBe(span); }); expect(getCurrentScope()).toBe(initialScope); - expect(initialScope.getSpan()).toBe(undefined); + expect(getActiveSpan()).toBe(undefined); }); it('allows to pass a scope', () => { @@ -255,22 +263,23 @@ describe('startSpanManual', () => { const manualScope = new Scope(); const parentSpan = new Span({ spanId: 'parent-span-id' }); + // eslint-disable-next-line deprecation/deprecation manualScope.setSpan(parentSpan); startSpanManual({ name: 'GET users/[id]', scope: manualScope }, (span, finish) => { expect(getCurrentScope()).not.toBe(initialScope); expect(getCurrentScope()).toBe(manualScope); - expect(getCurrentScope().getSpan()).toBe(span); + expect(getActiveSpan()).toBe(span); expect(span?.parentSpanId).toBe('parent-span-id'); finish(); // Is still the active span - expect(getCurrentScope().getSpan()).toBe(span); + expect(getActiveSpan()).toBe(span); }); expect(getCurrentScope()).toBe(initialScope); - expect(initialScope.getSpan()).toBe(undefined); + expect(getActiveSpan()).toBe(undefined); }); it('allows to pass a `startTime`', () => { @@ -296,34 +305,31 @@ describe('startInactiveSpan', () => { }); it('does not set span on scope', () => { - const initialScope = getCurrentScope(); - const span = startInactiveSpan({ name: 'GET users/[id]' }); expect(span).toBeDefined(); - expect(initialScope.getSpan()).toBeUndefined(); + expect(getActiveSpan()).toBeUndefined(); span?.end(); - expect(initialScope.getSpan()).toBeUndefined(); + expect(getActiveSpan()).toBeUndefined(); }); it('allows to pass a scope', () => { - const initialScope = getCurrentScope(); - const manualScope = new Scope(); const parentSpan = new Span({ spanId: 'parent-span-id' }); + // eslint-disable-next-line deprecation/deprecation manualScope.setSpan(parentSpan); const span = startInactiveSpan({ name: 'GET users/[id]', scope: manualScope }); expect(span).toBeDefined(); expect(span?.parentSpanId).toBe('parent-span-id'); - expect(initialScope.getSpan()).toBeUndefined(); + expect(getActiveSpan()).toBeUndefined(); span?.end(); - expect(initialScope.getSpan()).toBeUndefined(); + expect(getActiveSpan()).toBeUndefined(); }); it('allows to pass a `startTime`', () => { diff --git a/packages/nextjs/src/common/utils/wrapperUtils.ts b/packages/nextjs/src/common/utils/wrapperUtils.ts index 5911fdf79694..f7e0917f2c39 100644 --- a/packages/nextjs/src/common/utils/wrapperUtils.ts +++ b/packages/nextjs/src/common/utils/wrapperUtils.ts @@ -2,6 +2,7 @@ import type { IncomingMessage, ServerResponse } from 'http'; import { SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, captureException, + getActiveSpan, getActiveTransaction, getCurrentScope, runWithAsyncContext, @@ -86,7 +87,7 @@ export function withTracedServerSideDataFetcher Pr return async function (this: unknown, ...args: Parameters): Promise> { return runWithAsyncContext(async () => { const scope = getCurrentScope(); - const previousSpan: Span | undefined = getTransactionFromRequest(req) ?? scope.getSpan(); + const previousSpan: Span | undefined = getTransactionFromRequest(req) ?? getActiveSpan(); let dataFetcherSpan; const sentryTrace = @@ -156,6 +157,7 @@ export function withTracedServerSideDataFetcher Pr }); } + // eslint-disable-next-line deprecation/deprecation scope.setSpan(dataFetcherSpan); scope.setSDKProcessingMetadata({ request: req }); @@ -169,6 +171,7 @@ export function withTracedServerSideDataFetcher Pr throw e; } finally { dataFetcherSpan.end(); + // eslint-disable-next-line deprecation/deprecation scope.setSpan(previousSpan); if (!platformSupportsStreaming()) { await flushQueue(); diff --git a/packages/nextjs/src/edge/wrapApiHandlerWithSentry.ts b/packages/nextjs/src/edge/wrapApiHandlerWithSentry.ts index dabba7741e01..71e3072d68b5 100644 --- a/packages/nextjs/src/edge/wrapApiHandlerWithSentry.ts +++ b/packages/nextjs/src/edge/wrapApiHandlerWithSentry.ts @@ -1,4 +1,4 @@ -import { getCurrentScope } from '@sentry/core'; +import { getActiveSpan } from '@sentry/core'; import { withEdgeWrapping } from '../common/utils/edgeWrapperUtils'; import type { EdgeRouteHandler } from './types'; @@ -14,7 +14,7 @@ export function wrapApiHandlerWithSentry( apply: (wrappingTarget, thisArg, args: Parameters) => { const req = args[0]; - const activeSpan = getCurrentScope().getSpan(); + const activeSpan = getActiveSpan(); const wrappedHandler = withEdgeWrapping(wrappingTarget, { spanDescription: diff --git a/packages/nextjs/test/clientSdk.test.ts b/packages/nextjs/test/clientSdk.test.ts index fd43f1bee9ad..c77016a1e99c 100644 --- a/packages/nextjs/test/clientSdk.test.ts +++ b/packages/nextjs/test/clientSdk.test.ts @@ -90,6 +90,7 @@ describe('Client init()', () => { const transportSend = jest.spyOn(hub.getClient()!.getTransport()!, 'send'); // Ensure we have no current span, so our next span is a transaction + // eslint-disable-next-line deprecation/deprecation getCurrentScope().setSpan(undefined); SentryReact.startSpan({ name: '/404' }, () => { diff --git a/packages/node/src/handlers.ts b/packages/node/src/handlers.ts index 6d4d5b81e295..3a4738d8d2f3 100644 --- a/packages/node/src/handlers.ts +++ b/packages/node/src/handlers.ts @@ -5,6 +5,7 @@ import { captureException, continueTrace, flush, + getActiveSpan, getClient, getCurrentScope, hasTracingEnabled, @@ -91,6 +92,7 @@ export function tracingHandler(): ( ); // We put the transaction on the scope so users can attach children to it + // eslint-disable-next-line deprecation/deprecation getCurrentScope().setSpan(transaction); // We also set __sentry_transaction on the response so people can grab the transaction there to add @@ -275,7 +277,8 @@ export function errorHandler(options?: { // For some reason we need to set the transaction on the scope again // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access const transaction = (res as any).__sentry_transaction as Span; - if (transaction && _scope.getSpan() === undefined) { + if (transaction && !getActiveSpan()) { + // eslint-disable-next-line deprecation/deprecation _scope.setSpan(transaction); } diff --git a/packages/node/src/integrations/hapi/index.ts b/packages/node/src/integrations/hapi/index.ts index f63207b59c7a..3776cf449ce8 100644 --- a/packages/node/src/integrations/hapi/index.ts +++ b/packages/node/src/integrations/hapi/index.ts @@ -86,6 +86,7 @@ export const hapiTracingPlugin = { }, ); + // eslint-disable-next-line deprecation/deprecation getCurrentScope().setSpan(transaction); return h.continue; diff --git a/packages/node/src/integrations/http.ts b/packages/node/src/integrations/http.ts index d69eb86bb833..658f914a8155 100644 --- a/packages/node/src/integrations/http.ts +++ b/packages/node/src/integrations/http.ts @@ -3,6 +3,7 @@ import type * as https from 'https'; import type { Hub } from '@sentry/core'; import { addBreadcrumb, + getActiveSpan, getClient, getCurrentHub, getCurrentScope, @@ -253,7 +254,7 @@ function _createWrappedRequestMethodFactory( } const scope = getCurrentScope(); - const parentSpan = scope.getSpan(); + const parentSpan = getActiveSpan(); const data = getRequestSpanData(requestUrl, requestOptions); diff --git a/packages/node/src/integrations/undici/index.ts b/packages/node/src/integrations/undici/index.ts index b53281f3c2d1..25ef141d6b83 100644 --- a/packages/node/src/integrations/undici/index.ts +++ b/packages/node/src/integrations/undici/index.ts @@ -1,5 +1,6 @@ import { addBreadcrumb, + getActiveSpan, getClient, getCurrentHub, getCurrentScope, @@ -156,8 +157,7 @@ export class Undici implements Integration { const clientOptions = client.getOptions(); const scope = getCurrentScope(); - - const parentSpan = scope.getSpan(); + const parentSpan = getActiveSpan(); const span = this._shouldCreateSpan(stringUrl) ? createRequestSpan(parentSpan, request, stringUrl) : undefined; if (span) { diff --git a/packages/node/test/integrations/http.test.ts b/packages/node/test/integrations/http.test.ts index ead5e2469494..5a1603988ee2 100644 --- a/packages/node/test/integrations/http.test.ts +++ b/packages/node/test/integrations/http.test.ts @@ -22,6 +22,7 @@ const originalHttpRequest = http.request; describe('tracing', () => { afterEach(() => { + // eslint-disable-next-line deprecation/deprecation sentryCore.getCurrentHub().getScope().setSpan(undefined); }); @@ -48,6 +49,8 @@ describe('tracing', () => { jest.spyOn(sentryCore, 'getCurrentHub').mockReturnValue(hub); jest.spyOn(sentryCore, 'getCurrentScope').mockImplementation(() => hub.getScope()); + // eslint-disable-next-line deprecation/deprecation + jest.spyOn(sentryCore, 'getActiveSpan').mockImplementation(() => hub.getScope().getSpan()); jest.spyOn(sentryCore, 'getClient').mockReturnValue(hub.getClient()); const transaction = startInactiveSpan({ @@ -58,6 +61,7 @@ describe('tracing', () => { expect(transaction).toBeInstanceOf(Transaction); + // eslint-disable-next-line deprecation/deprecation hub.getScope().setSpan(transaction); return transaction; @@ -75,6 +79,8 @@ describe('tracing', () => { const hub = new Hub(new NodeClient(options)); jest.spyOn(sentryCore, 'getCurrentHub').mockReturnValue(hub); jest.spyOn(sentryCore, 'getCurrentScope').mockImplementation(() => hub.getScope()); + // eslint-disable-next-line deprecation/deprecation + jest.spyOn(sentryCore, 'getActiveSpan').mockImplementation(() => hub.getScope().getSpan()); jest.spyOn(sentryCore, 'getClient').mockReturnValue(hub.getClient()); return hub; } @@ -361,6 +367,8 @@ describe('tracing', () => { jest.spyOn(sentryCore, 'getCurrentHub').mockReturnValue(hub); jest.spyOn(sentryCore, 'getCurrentScope').mockImplementation(() => hub.getScope()); + // eslint-disable-next-line deprecation/deprecation + jest.spyOn(sentryCore, 'getActiveSpan').mockImplementation(() => hub.getScope().getSpan()); jest.spyOn(sentryCore, 'getClient').mockReturnValue(hub.getClient()); const client = new NodeClient(options); @@ -373,6 +381,7 @@ describe('tracing', () => { function createTransactionAndPutOnScope(hub: Hub) { addTracingExtensions(); const transaction = startInactiveSpan({ name: 'dogpark' }); + // eslint-disable-next-line deprecation/deprecation hub.getScope().setSpan(transaction); return transaction; } @@ -388,6 +397,8 @@ describe('tracing', () => { jest.spyOn(sentryCore, 'getCurrentHub').mockReturnValue(hub); jest.spyOn(sentryCore, 'getCurrentScope').mockImplementation(() => hub.getScope()); + // eslint-disable-next-line deprecation/deprecation + jest.spyOn(sentryCore, 'getActiveSpan').mockImplementation(() => hub.getScope().getSpan()); jest.spyOn(sentryCore, 'getClient').mockReturnValue(hub.getClient()); httpIntegration.setupOnce( @@ -497,6 +508,8 @@ describe('tracing', () => { jest.spyOn(sentryCore, 'getCurrentHub').mockReturnValue(hub); jest.spyOn(sentryCore, 'getCurrentScope').mockImplementation(() => hub.getScope()); + // eslint-disable-next-line deprecation/deprecation + jest.spyOn(sentryCore, 'getActiveSpan').mockImplementation(() => hub.getScope().getSpan()); jest.spyOn(sentryCore, 'getClient').mockReturnValue(hub.getClient()); httpIntegration.setupOnce( diff --git a/packages/node/test/integrations/undici.test.ts b/packages/node/test/integrations/undici.test.ts index ad2c9a55ded1..ce2f475c57d0 100644 --- a/packages/node/test/integrations/undici.test.ts +++ b/packages/node/test/integrations/undici.test.ts @@ -1,5 +1,5 @@ import * as http from 'http'; -import { Transaction, startSpan } from '@sentry/core'; +import { Transaction, getActiveSpan, startSpan } from '@sentry/core'; import { spanToTraceHeader } from '@sentry/core'; import { Hub, makeMain, runWithAsyncContext } from '@sentry/core'; import type { fetch as FetchType } from 'undici'; @@ -182,7 +182,7 @@ conditionalTest({ min: 16 })('Undici integration', () => { // ignore } - expect(hub.getScope().getSpan()).toBeUndefined(); + expect(getActiveSpan()).toBeUndefined(); }); it('does create a span if `shouldCreateSpanForRequest` is defined', async () => { diff --git a/packages/opentelemetry-node/test/spanprocessor.test.ts b/packages/opentelemetry-node/test/spanprocessor.test.ts index 9920e12ee62a..c1d3c209ecca 100644 --- a/packages/opentelemetry-node/test/spanprocessor.test.ts +++ b/packages/opentelemetry-node/test/spanprocessor.test.ts @@ -122,6 +122,7 @@ describe('SentrySpanProcessor', () => { expect(sentrySpan?.spanContext().spanId).toEqual(childOtelSpan.spanContext().spanId); expect(sentrySpan?.parentSpanId).toEqual(sentrySpanTransaction?.spanContext().spanId); + // eslint-disable-next-line deprecation/deprecation expect(hub.getScope().getSpan()).toBeUndefined(); child.end(endTime); @@ -162,6 +163,7 @@ describe('SentrySpanProcessor', () => { expect(sentrySpan?.spanContext().spanId).toEqual(childOtelSpan.spanContext().spanId); expect(sentrySpan?.parentSpanId).toEqual(parentOtelSpan.spanContext().spanId); + // eslint-disable-next-line deprecation/deprecation expect(hub.getScope().getSpan()).toBeUndefined(); child.end(endTime); diff --git a/packages/opentelemetry/test/custom/scope.test.ts b/packages/opentelemetry/test/custom/scope.test.ts index 41827fcd772d..1662f571331c 100644 --- a/packages/opentelemetry/test/custom/scope.test.ts +++ b/packages/opentelemetry/test/custom/scope.test.ts @@ -81,12 +81,14 @@ describe('NodeExperimentalScope', () => { // Pretend we have a _span set scope['_span'] = {} as any; + // eslint-disable-next-line deprecation/deprecation expect(scope.getSpan()).toBeUndefined(); }); it('setSpan is a noop', () => { const scope = new OpenTelemetryScope(); + // eslint-disable-next-line deprecation/deprecation scope.setSpan({} as any); expect(scope['_span']).toBeUndefined(); diff --git a/packages/remix/src/utils/instrumentServer.ts b/packages/remix/src/utils/instrumentServer.ts index b1383725a94d..4b1109549d47 100644 --- a/packages/remix/src/utils/instrumentServer.ts +++ b/packages/remix/src/utils/instrumentServer.ts @@ -1,5 +1,6 @@ /* eslint-disable max-lines */ import { + getActiveSpan, getActiveTransaction, getClient, getCurrentScope, @@ -257,11 +258,13 @@ function makeWrappedDataFunction( if (span) { // Assign data function to hub to be able to see `db` transactions (if any) as children. + // eslint-disable-next-line deprecation/deprecation currentScope.setSpan(span); } res = await origFn.call(this, args); + // eslint-disable-next-line deprecation/deprecation currentScope.setSpan(activeTransaction); span?.end(); } catch (err) { @@ -299,10 +302,9 @@ function getTraceAndBaggage(): { } { // eslint-disable-next-line deprecation/deprecation const transaction = getActiveTransaction(); - const currentScope = getCurrentScope(); if (isNodeEnv() && hasTracingEnabled()) { - const span = currentScope.getSpan(); + const span = getActiveSpan(); if (span && transaction) { const dynamicSamplingContext = transaction.getDynamicSamplingContext(); @@ -418,6 +420,7 @@ export function startRequestHandlerTransaction( }, }); + // eslint-disable-next-line deprecation/deprecation hub.getScope().setSpan(transaction); return transaction; } diff --git a/packages/sveltekit/src/server/handle.ts b/packages/sveltekit/src/server/handle.ts index d6eb5555fefd..cf195551ef61 100644 --- a/packages/sveltekit/src/server/handle.ts +++ b/packages/sveltekit/src/server/handle.ts @@ -1,4 +1,4 @@ -import { getCurrentScope, spanToTraceHeader } from '@sentry/core'; +import { getActiveSpan, getCurrentScope, spanToTraceHeader } from '@sentry/core'; import { getActiveTransaction, runWithAsyncContext, startSpan } from '@sentry/core'; import { captureException } from '@sentry/node'; /* eslint-disable @sentry-internal/sdk/no-optional-chaining */ @@ -143,7 +143,7 @@ export function sentryHandle(handlerOptions?: SentryHandleOptions): Handle { // if there is an active transaction, we know that this handle call is nested and hence // we don't create a new domain for it. If we created one, nested server calls would // create new transactions instead of adding a child span to the currently active span. - if (getCurrentScope().getSpan()) { + if (getActiveSpan()) { return instrumentHandle(input, options); } return runWithAsyncContext(() => { diff --git a/packages/tracing-internal/src/browser/request.ts b/packages/tracing-internal/src/browser/request.ts index 655a6e803306..d7c4c7892189 100644 --- a/packages/tracing-internal/src/browser/request.ts +++ b/packages/tracing-internal/src/browser/request.ts @@ -1,5 +1,6 @@ /* eslint-disable max-lines */ import { + getActiveSpan, getClient, getCurrentScope, getDynamicSamplingContextFromClient, @@ -271,7 +272,7 @@ export function xhrCallback( } const scope = getCurrentScope(); - const parentSpan = scope.getSpan(); + const parentSpan = getActiveSpan(); const span = shouldCreateSpanResult && parentSpan diff --git a/packages/tracing-internal/src/common/fetch.ts b/packages/tracing-internal/src/common/fetch.ts index 47cf443f2cb5..cc04885e4436 100644 --- a/packages/tracing-internal/src/common/fetch.ts +++ b/packages/tracing-internal/src/common/fetch.ts @@ -1,4 +1,5 @@ import { + getActiveSpan, getClient, getCurrentScope, getDynamicSamplingContextFromClient, @@ -73,7 +74,7 @@ export function instrumentFetchRequest( const scope = getCurrentScope(); const client = getClient(); - const parentSpan = scope.getSpan(); + const parentSpan = getActiveSpan(); const { method, url } = handlerData.fetchData; @@ -129,6 +130,7 @@ export function addTracingHeadersToFetchRequest( }, requestSpan?: Span, ): PolymorphicRequestHeaders | undefined { + // eslint-disable-next-line deprecation/deprecation const span = requestSpan || scope.getSpan(); const transaction = span && span.transaction; diff --git a/packages/tracing-internal/src/node/integrations/apollo.ts b/packages/tracing-internal/src/node/integrations/apollo.ts index dec97b0df729..cb87edae9cf2 100644 --- a/packages/tracing-internal/src/node/integrations/apollo.ts +++ b/packages/tracing-internal/src/node/integrations/apollo.ts @@ -189,6 +189,7 @@ function wrapResolver( fill(model[resolverGroupName], resolverName, function (orig: () => unknown | Promise) { return function (this: unknown, ...args: unknown[]) { const scope = getCurrentHub().getScope(); + // eslint-disable-next-line deprecation/deprecation const parentSpan = scope.getSpan(); // eslint-disable-next-line deprecation/deprecation const span = parentSpan?.startChild({ diff --git a/packages/tracing-internal/src/node/integrations/graphql.ts b/packages/tracing-internal/src/node/integrations/graphql.ts index bea4370340ee..a87ee071af81 100644 --- a/packages/tracing-internal/src/node/integrations/graphql.ts +++ b/packages/tracing-internal/src/node/integrations/graphql.ts @@ -52,6 +52,7 @@ export class GraphQL implements LazyLoadedIntegration { fill(pkg, 'execute', function (orig: () => void | Promise) { return function (this: unknown, ...args: unknown[]) { const scope = getCurrentHub().getScope(); + // eslint-disable-next-line deprecation/deprecation const parentSpan = scope.getSpan(); // eslint-disable-next-line deprecation/deprecation @@ -61,6 +62,7 @@ export class GraphQL implements LazyLoadedIntegration { origin: 'auto.graphql.graphql', }); + // eslint-disable-next-line deprecation/deprecation scope?.setSpan(span); const rv = orig.call(this, ...args); @@ -68,6 +70,7 @@ export class GraphQL implements LazyLoadedIntegration { if (isThenable(rv)) { return rv.then((res: unknown) => { span?.end(); + // eslint-disable-next-line deprecation/deprecation scope?.setSpan(parentSpan); return res; @@ -75,6 +78,7 @@ export class GraphQL implements LazyLoadedIntegration { } span?.end(); + // eslint-disable-next-line deprecation/deprecation scope?.setSpan(parentSpan); return rv; }; diff --git a/packages/tracing-internal/src/node/integrations/mongo.ts b/packages/tracing-internal/src/node/integrations/mongo.ts index 9451e12b50da..3a91da4b327a 100644 --- a/packages/tracing-internal/src/node/integrations/mongo.ts +++ b/packages/tracing-internal/src/node/integrations/mongo.ts @@ -175,6 +175,7 @@ export class Mongo implements LazyLoadedIntegration { return function (this: unknown, ...args: unknown[]) { const lastArg = args[args.length - 1]; const scope = getCurrentHub().getScope(); + // eslint-disable-next-line deprecation/deprecation const parentSpan = scope.getSpan(); // Check if the operation was passed a callback. (mapReduce requires a different check, as diff --git a/packages/tracing-internal/src/node/integrations/mysql.ts b/packages/tracing-internal/src/node/integrations/mysql.ts index d1283a47815c..16753c4a9e08 100644 --- a/packages/tracing-internal/src/node/integrations/mysql.ts +++ b/packages/tracing-internal/src/node/integrations/mysql.ts @@ -104,6 +104,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(); + // eslint-disable-next-line deprecation/deprecation const parentSpan = scope.getSpan(); // eslint-disable-next-line deprecation/deprecation diff --git a/packages/tracing-internal/src/node/integrations/postgres.ts b/packages/tracing-internal/src/node/integrations/postgres.ts index 3a5ee78641dc..2f1128683222 100644 --- a/packages/tracing-internal/src/node/integrations/postgres.ts +++ b/packages/tracing-internal/src/node/integrations/postgres.ts @@ -105,6 +105,7 @@ export class Postgres implements LazyLoadedIntegration { fill(Client.prototype, 'query', function (orig: PgClientQuery) { return function (this: PgClientThis, config: unknown, values: unknown, callback: unknown) { const scope = getCurrentHub().getScope(); + // eslint-disable-next-line deprecation/deprecation const parentSpan = scope.getSpan(); const data: Record = { diff --git a/packages/tracing-internal/test/browser/backgroundtab.test.ts b/packages/tracing-internal/test/browser/backgroundtab.test.ts index 704f3ba89b1c..dcb423fd094a 100644 --- a/packages/tracing-internal/test/browser/backgroundtab.test.ts +++ b/packages/tracing-internal/test/browser/backgroundtab.test.ts @@ -30,6 +30,7 @@ conditionalTest({ min: 10 })('registerBackgroundTabDetection', () => { afterEach(() => { events = {}; + // eslint-disable-next-line deprecation/deprecation hub.getScope().setSpan(undefined); }); diff --git a/packages/tracing/test/idletransaction.test.ts b/packages/tracing/test/idletransaction.test.ts index 7e6b186ea82f..17a4400542a6 100644 --- a/packages/tracing/test/idletransaction.test.ts +++ b/packages/tracing/test/idletransaction.test.ts @@ -97,6 +97,7 @@ describe('IdleTransaction', () => { // @ts-expect-error need to pass in hub const otherTransaction = new Transaction({ name: 'bar' }, hub); + // eslint-disable-next-line deprecation/deprecation hub.getScope().setSpan(otherTransaction); transaction.end(); @@ -117,6 +118,7 @@ describe('IdleTransaction', () => { const mockFinish = jest.spyOn(transaction, 'end'); transaction.initSpanRecorder(10); expect(transaction.activities).toMatchObject({}); + // eslint-disable-next-line deprecation/deprecation getCurrentScope().setSpan(transaction); const span = startInactiveSpan({ name: 'inner' })!; @@ -135,6 +137,7 @@ describe('IdleTransaction', () => { const transaction = new IdleTransaction({ name: 'foo' }, hub); transaction.initSpanRecorder(10); expect(transaction.activities).toMatchObject({}); + // eslint-disable-next-line deprecation/deprecation getCurrentScope().setSpan(transaction); startInactiveSpan({ name: 'inner', startTimestamp: 1234, endTimestamp: 5678 }); @@ -146,6 +149,7 @@ describe('IdleTransaction', () => { const mockFinish = jest.spyOn(transaction, 'end'); transaction.initSpanRecorder(10); expect(transaction.activities).toMatchObject({}); + // eslint-disable-next-line deprecation/deprecation getCurrentScope().setSpan(transaction); startSpanManual({ name: 'inner1' }, span => { @@ -169,6 +173,7 @@ describe('IdleTransaction', () => { transaction.initSpanRecorder(10); transaction.registerBeforeFinishCallback(mockCallback1); transaction.registerBeforeFinishCallback(mockCallback2); + // eslint-disable-next-line deprecation/deprecation getCurrentScope().setSpan(transaction); expect(mockCallback1).toHaveBeenCalledTimes(0); @@ -186,6 +191,7 @@ describe('IdleTransaction', () => { it('filters spans on finish', () => { const transaction = new IdleTransaction({ name: 'foo', startTimestamp: 1234 }, hub); transaction.initSpanRecorder(10); + // eslint-disable-next-line deprecation/deprecation getCurrentScope().setSpan(transaction); // regular child - should be kept @@ -220,6 +226,7 @@ describe('IdleTransaction', () => { it('filters out spans that exceed final timeout', () => { const transaction = new IdleTransaction({ name: 'foo', startTimestamp: 1234 }, hub, 1000, 3000); transaction.initSpanRecorder(10); + // eslint-disable-next-line deprecation/deprecation getCurrentScope().setSpan(transaction); const span = startInactiveSpan({ name: 'span', startTimestamp: transaction.startTimestamp + 2 })!; @@ -256,6 +263,7 @@ describe('IdleTransaction', () => { it('does not finish if a activity is started', () => { const transaction = new IdleTransaction({ name: 'foo', startTimestamp: 1234 }, hub); transaction.initSpanRecorder(10); + // eslint-disable-next-line deprecation/deprecation getCurrentScope().setSpan(transaction); startInactiveSpan({ name: 'span' }); @@ -268,6 +276,7 @@ describe('IdleTransaction', () => { const idleTimeout = 10; const transaction = new IdleTransaction({ name: 'foo', startTimestamp: 1234 }, hub, idleTimeout); transaction.initSpanRecorder(10); + // eslint-disable-next-line deprecation/deprecation getCurrentScope().setSpan(transaction); startSpan({ name: 'span1' }, () => {}); @@ -285,6 +294,7 @@ describe('IdleTransaction', () => { const idleTimeout = 10; const transaction = new IdleTransaction({ name: 'foo', startTimestamp: 1234 }, hub, idleTimeout); transaction.initSpanRecorder(10); + // eslint-disable-next-line deprecation/deprecation getCurrentScope().setSpan(transaction); startSpan({ name: 'span1' }, () => {}); @@ -304,6 +314,7 @@ describe('IdleTransaction', () => { const idleTimeout = 10; const transaction = new IdleTransaction({ name: 'foo', startTimestamp: 1234 }, hub, idleTimeout); transaction.initSpanRecorder(10); + // eslint-disable-next-line deprecation/deprecation getCurrentScope().setSpan(transaction); const firstSpan = startInactiveSpan({ name: 'span1' })!; @@ -319,6 +330,7 @@ describe('IdleTransaction', () => { const idleTimeout = 10; const transaction = new IdleTransaction({ name: 'foo', startTimestamp: 1234 }, hub, idleTimeout); transaction.initSpanRecorder(10); + // eslint-disable-next-line deprecation/deprecation getCurrentScope().setSpan(transaction); const firstSpan = startInactiveSpan({ name: 'span1' })!; @@ -340,6 +352,7 @@ describe('IdleTransaction', () => { const idleTimeout = 10; const transaction = new IdleTransaction({ name: 'foo', startTimestamp: 1234 }, hub, idleTimeout); transaction.initSpanRecorder(10); + // eslint-disable-next-line deprecation/deprecation getCurrentScope().setSpan(transaction); startSpan({ name: 'span' }, () => {}); @@ -355,6 +368,7 @@ describe('IdleTransaction', () => { const idleTimeout = 10; const transaction = new IdleTransaction({ name: 'foo', startTimestamp: 1234 }, hub, idleTimeout); transaction.initSpanRecorder(10); + // eslint-disable-next-line deprecation/deprecation getCurrentScope().setSpan(transaction); startSpan({ name: 'span' }, () => {}); @@ -402,6 +416,7 @@ describe('IdleTransaction', () => { const transaction = new IdleTransaction({ name: 'foo' }, hub, TRACING_DEFAULTS.idleTimeout); const mockFinish = jest.spyOn(transaction, 'end'); transaction.initSpanRecorder(10); + // eslint-disable-next-line deprecation/deprecation getCurrentScope().setSpan(transaction); expect(mockFinish).toHaveBeenCalledTimes(0); @@ -424,6 +439,7 @@ describe('IdleTransaction', () => { const transaction = new IdleTransaction({ name: 'foo' }, hub, TRACING_DEFAULTS.idleTimeout, 50000); const mockFinish = jest.spyOn(transaction, 'end'); transaction.initSpanRecorder(10); + // eslint-disable-next-line deprecation/deprecation getCurrentScope().setSpan(transaction); expect(mockFinish).toHaveBeenCalledTimes(0); diff --git a/packages/types/src/scope.ts b/packages/types/src/scope.ts index af94c30c2549..fd51eae8e5c4 100644 --- a/packages/types/src/scope.ts +++ b/packages/types/src/scope.ts @@ -140,11 +140,13 @@ export interface Scope { /** * Sets the Span on the scope. * @param span Span + * @deprecated Instead of setting a span on a scope, use `startSpan()`/`startSpanManual()` instead. */ setSpan(span?: Span): this; /** - * Returns the `Span` if there is one + * Returns the `Span` if there is one. + * @deprecated Use `getActiveSpan()` instead. */ getSpan(): Span | undefined; From 15ab8eeb2e84fd0ac1e06c8b8448c447250b284e Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Tue, 9 Jan 2024 15:54:03 +0100 Subject: [PATCH 34/43] fix(core): Do not run `setup` for integration on client multiple times (#10116) Currently, if you do: ```js const myIntegration = new InboundFilters(); const myIntegration2 = new InboundFilters(); const myIntegration3 = new InboundFilters(); client.addIntegration(myIntegration); client.addIntegration(myIntegration2); client.addIntegration(myIntegration3); ``` All of these will have their `setup` hooks called, and thus they will be initialized multiple times. However, all but the last will be discarded from the client and not be accessible anymore via e.g. `getIntegration()` or similar. This used to not matter because everything was guarded through `setupOnce()` anyhow, but i would say this is more of a bug and not really expected. With this change, an integration can only be added to a client once, if you try to add it again it will noop. --- packages/core/src/integration.ts | 4 ++ packages/core/test/lib/integration.test.ts | 70 ++++++++++------------ 2 files changed, 35 insertions(+), 39 deletions(-) diff --git a/packages/core/src/integration.ts b/packages/core/src/integration.ts index f9be8b325782..16b0c145b0b4 100644 --- a/packages/core/src/integration.ts +++ b/packages/core/src/integration.ts @@ -101,6 +101,10 @@ export function setupIntegrations(client: Client, integrations: Integration[]): /** Setup a single integration. */ export function setupIntegration(client: Client, integration: Integration, integrationIndex: IntegrationIndex): void { + if (integrationIndex[integration.name]) { + DEBUG_BUILD && logger.log(`Integration skipped because it was already installed: ${integration.name}`); + return; + } integrationIndex[integration.name] = integration; // `setupOnce` is only called the first time diff --git a/packages/core/test/lib/integration.test.ts b/packages/core/test/lib/integration.test.ts index 137a7dce4df3..19e22773b59b 100644 --- a/packages/core/test/lib/integration.test.ts +++ b/packages/core/test/lib/integration.test.ts @@ -377,7 +377,7 @@ describe('setupIntegration', () => { setupIntegration(client2, integration3, integrationIndex); setupIntegration(client2, integration4, integrationIndex); - expect(integrationIndex).toEqual({ test: integration4 }); + expect(integrationIndex).toEqual({ test: integration1 }); expect(integration1.setupOnce).toHaveBeenCalledTimes(1); expect(integration2.setupOnce).not.toHaveBeenCalled(); expect(integration3.setupOnce).not.toHaveBeenCalled(); @@ -394,32 +394,32 @@ describe('setupIntegration', () => { const client1 = getTestClient(); const client2 = getTestClient(); - const integrationIndex = {}; + const integrationIndex1 = {}; + const integrationIndex2 = {}; const integration1 = new CustomIntegration(); const integration2 = new CustomIntegration(); const integration3 = new CustomIntegration(); const integration4 = new CustomIntegration(); - setupIntegration(client1, integration1, integrationIndex); - setupIntegration(client1, integration2, integrationIndex); - setupIntegration(client2, integration3, integrationIndex); - setupIntegration(client2, integration4, integrationIndex); + setupIntegration(client1, integration1, integrationIndex1); + setupIntegration(client1, integration2, integrationIndex1); + setupIntegration(client2, integration3, integrationIndex2); + setupIntegration(client2, integration4, integrationIndex2); - expect(integrationIndex).toEqual({ test: integration4 }); + expect(integrationIndex1).toEqual({ test: integration1 }); + expect(integrationIndex2).toEqual({ test: integration3 }); expect(integration1.setupOnce).toHaveBeenCalledTimes(1); expect(integration2.setupOnce).not.toHaveBeenCalled(); expect(integration3.setupOnce).not.toHaveBeenCalled(); expect(integration4.setupOnce).not.toHaveBeenCalled(); expect(integration1.setup).toHaveBeenCalledTimes(1); - expect(integration2.setup).toHaveBeenCalledTimes(1); + expect(integration2.setup).toHaveBeenCalledTimes(0); expect(integration3.setup).toHaveBeenCalledTimes(1); - expect(integration4.setup).toHaveBeenCalledTimes(1); + expect(integration4.setup).toHaveBeenCalledTimes(0); expect(integration1.setup).toHaveBeenCalledWith(client1); - expect(integration2.setup).toHaveBeenCalledWith(client1); expect(integration3.setup).toHaveBeenCalledWith(client2); - expect(integration4.setup).toHaveBeenCalledWith(client2); }); it('binds preprocessEvent for each client', () => { @@ -432,18 +432,20 @@ describe('setupIntegration', () => { const client1 = getTestClient(); const client2 = getTestClient(); - const integrationIndex = {}; + const integrationIndex1 = {}; + const integrationIndex2 = {}; const integration1 = new CustomIntegration(); const integration2 = new CustomIntegration(); const integration3 = new CustomIntegration(); const integration4 = new CustomIntegration(); - setupIntegration(client1, integration1, integrationIndex); - setupIntegration(client1, integration2, integrationIndex); - setupIntegration(client2, integration3, integrationIndex); - setupIntegration(client2, integration4, integrationIndex); + setupIntegration(client1, integration1, integrationIndex1); + setupIntegration(client1, integration2, integrationIndex1); + setupIntegration(client2, integration3, integrationIndex2); + setupIntegration(client2, integration4, integrationIndex2); - expect(integrationIndex).toEqual({ test: integration4 }); + expect(integrationIndex1).toEqual({ test: integration1 }); + expect(integrationIndex2).toEqual({ test: integration3 }); expect(integration1.setupOnce).toHaveBeenCalledTimes(1); expect(integration2.setupOnce).not.toHaveBeenCalled(); expect(integration3.setupOnce).not.toHaveBeenCalled(); @@ -456,14 +458,12 @@ describe('setupIntegration', () => { client2.captureEvent({ event_id: '2c' }); expect(integration1.preprocessEvent).toHaveBeenCalledTimes(2); - expect(integration2.preprocessEvent).toHaveBeenCalledTimes(2); + expect(integration2.preprocessEvent).toHaveBeenCalledTimes(0); expect(integration3.preprocessEvent).toHaveBeenCalledTimes(3); - expect(integration4.preprocessEvent).toHaveBeenCalledTimes(3); + expect(integration4.preprocessEvent).toHaveBeenCalledTimes(0); expect(integration1.preprocessEvent).toHaveBeenLastCalledWith({ event_id: '1b' }, {}, client1); - expect(integration2.preprocessEvent).toHaveBeenLastCalledWith({ event_id: '1b' }, {}, client1); expect(integration3.preprocessEvent).toHaveBeenLastCalledWith({ event_id: '2c' }, {}, client2); - expect(integration4.preprocessEvent).toHaveBeenLastCalledWith({ event_id: '2c' }, {}, client2); }); it('allows to mutate events in preprocessEvent', async () => { @@ -504,18 +504,20 @@ describe('setupIntegration', () => { const client1 = getTestClient(); const client2 = getTestClient(); - const integrationIndex = {}; + const integrationIndex1 = {}; + const integrationIndex2 = {}; const integration1 = new CustomIntegration(); const integration2 = new CustomIntegration(); const integration3 = new CustomIntegration(); const integration4 = new CustomIntegration(); - setupIntegration(client1, integration1, integrationIndex); - setupIntegration(client1, integration2, integrationIndex); - setupIntegration(client2, integration3, integrationIndex); - setupIntegration(client2, integration4, integrationIndex); + setupIntegration(client1, integration1, integrationIndex1); + setupIntegration(client1, integration2, integrationIndex1); + setupIntegration(client2, integration3, integrationIndex2); + setupIntegration(client2, integration4, integrationIndex2); - expect(integrationIndex).toEqual({ test: integration4 }); + expect(integrationIndex1).toEqual({ test: integration1 }); + expect(integrationIndex2).toEqual({ test: integration3 }); expect(integration1.setupOnce).toHaveBeenCalledTimes(1); expect(integration2.setupOnce).not.toHaveBeenCalled(); expect(integration3.setupOnce).not.toHaveBeenCalled(); @@ -528,30 +530,20 @@ describe('setupIntegration', () => { client2.captureEvent({ event_id: '2c' }); expect(integration1.processEvent).toHaveBeenCalledTimes(2); - expect(integration2.processEvent).toHaveBeenCalledTimes(2); + expect(integration2.processEvent).toHaveBeenCalledTimes(0); expect(integration3.processEvent).toHaveBeenCalledTimes(3); - expect(integration4.processEvent).toHaveBeenCalledTimes(3); + expect(integration4.processEvent).toHaveBeenCalledTimes(0); expect(integration1.processEvent).toHaveBeenLastCalledWith( expect.objectContaining({ event_id: '1b' }), {}, client1, ); - expect(integration2.processEvent).toHaveBeenLastCalledWith( - expect.objectContaining({ event_id: '1b' }), - {}, - client1, - ); expect(integration3.processEvent).toHaveBeenLastCalledWith( expect.objectContaining({ event_id: '2c' }), {}, client2, ); - expect(integration4.processEvent).toHaveBeenLastCalledWith( - expect.objectContaining({ event_id: '2c' }), - {}, - client2, - ); }); it('allows to mutate events in processEvent', async () => { From 5623fd84c206bc63cd7c30525c0d4cf1c2ecd23d Mon Sep 17 00:00:00 2001 From: Abhijeet Prasad Date: Tue, 9 Jan 2024 10:08:24 -0500 Subject: [PATCH 35/43] ref(ember): Use new span APIs (#10111) --- packages/ember/addon/index.ts | 2 +- .../sentry-performance.ts | 51 +++++++------------ 2 files changed, 20 insertions(+), 33 deletions(-) diff --git a/packages/ember/addon/index.ts b/packages/ember/addon/index.ts index 4179df0d1dd2..57429c64f789 100644 --- a/packages/ember/addon/index.ts +++ b/packages/ember/addon/index.ts @@ -6,10 +6,10 @@ import { startSpan } from '@sentry/browser'; import type { BrowserOptions } from '@sentry/browser'; import * as Sentry from '@sentry/browser'; import { SDK_VERSION } from '@sentry/browser'; -import type { Transaction } from '@sentry/types'; import { GLOBAL_OBJ } from '@sentry/utils'; import Ember from 'ember'; +import type { Transaction } from '@sentry/types'; import type { EmberSentryConfig, GlobalConfig, OwnConfig } from './types'; function _getSentryInitConfig(): EmberSentryConfig['sentry'] { diff --git a/packages/ember/addon/instance-initializers/sentry-performance.ts b/packages/ember/addon/instance-initializers/sentry-performance.ts index d61801fa6d17..999474b798bc 100644 --- a/packages/ember/addon/instance-initializers/sentry-performance.ts +++ b/packages/ember/addon/instance-initializers/sentry-performance.ts @@ -12,7 +12,7 @@ import type { Span, Transaction } from '@sentry/types'; import { GLOBAL_OBJ, browserPerformanceTimeOrigin, timestampInSeconds } from '@sentry/utils'; import type { BrowserClient } from '..'; -import { getActiveTransaction } from '..'; +import { getActiveSpan, startInactiveSpan } from '..'; import type { EmberRouterMain, EmberSentryConfig, GlobalConfig, OwnConfig, StartTransactionFunction } from '../types'; type SentryTestRouterService = RouterService & { @@ -149,10 +149,9 @@ export function _instrumentEmberRouter( 'routing.instrumentation': '@sentry/ember', }, }); - // eslint-disable-next-line deprecation/deprecation - transitionSpan = activeTransaction?.startChild({ + transitionSpan = startInactiveSpan({ op: 'ui.ember.transition', - description: `route:${fromRoute} -> route:${toRoute}`, + name: `route:${fromRoute} -> route:${toRoute}`, origin: 'auto.ui.ember', }); }); @@ -196,9 +195,8 @@ function _instrumentEmberRunloop(config: EmberSentryConfig): void { if (previousInstance) { return; } - // eslint-disable-next-line deprecation/deprecation - const activeTransaction = getActiveTransaction(); - if (!activeTransaction) { + const activeSpan = getActiveSpan(); + if (!activeSpan) { return; } if (currentQueueSpan) { @@ -213,24 +211,20 @@ function _instrumentEmberRunloop(config: EmberSentryConfig): void { const minQueueDuration = minimumRunloopQueueDuration ?? 5; if ((now - currentQueueStart) * 1000 >= minQueueDuration) { - activeTransaction - // eslint-disable-next-line deprecation/deprecation - ?.startChild({ - op: `ui.ember.runloop.${queue}`, - origin: 'auto.ui.ember', - startTimestamp: currentQueueStart, - endTimestamp: now, - }) - .end(); + startInactiveSpan({ + name: 'runloop', + op: `ui.ember.runloop.${queue}`, + origin: 'auto.ui.ember', + startTimestamp: currentQueueStart, + })?.end(now); } currentQueueStart = undefined; } // Setup for next queue - // eslint-disable-next-line deprecation/deprecation - const stillActiveTransaction = getActiveTransaction(); - if (!stillActiveTransaction) { + const stillActiveSpan = getActiveSpan(); + if (!stillActiveSpan) { return; } currentQueueStart = timestampInSeconds(); @@ -290,16 +284,12 @@ function processComponentRenderAfter( const componentRenderDuration = now - begin.now; if (componentRenderDuration * 1000 >= minComponentDuration) { - // eslint-disable-next-line deprecation/deprecation - const activeTransaction = getActiveTransaction(); - // eslint-disable-next-line deprecation/deprecation - activeTransaction?.startChild({ + startInactiveSpan({ + name: payload.containerKey || payload.object, op, - description: payload.containerKey || payload.object, origin: 'auto.ui.ember', startTimestamp: begin.now, - endTimestamp: now, - }); + })?.end(now); } } @@ -377,15 +367,12 @@ function _instrumentInitialLoad(config: EmberSentryConfig): void { const startTimestamp = (measure.startTime + browserPerformanceTimeOrigin) / 1000; const endTimestamp = startTimestamp + measure.duration / 1000; - // eslint-disable-next-line deprecation/deprecation - const transaction = getActiveTransaction(); - // eslint-disable-next-line deprecation/deprecation - const span = transaction?.startChild({ + startInactiveSpan({ op: 'ui.ember.init', + name: 'init', origin: 'auto.ui.ember', startTimestamp, - }); - span?.end(endTimestamp); + })?.end(endTimestamp); performance.clearMarks(startName); performance.clearMarks(endName); From eff57fa07316a9c654ec2bdc1da352b9050833ad Mon Sep 17 00:00:00 2001 From: Tim Fish Date: Tue, 9 Jan 2024 18:24:05 +0100 Subject: [PATCH 36/43] feat(node): Make `getModuleFromFilename` compatible with ESM (#10061) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This removes usage of `require.main.filename` to bring better ESM compatability. An extra bonus in that now `createGetModuleFromFilename` is ESM compatible, we can use it in the Anr worker when `appRootPath` is supplied (ie. Electron only for now). ### ⚠️ Fingerprinting This may change `module` fingerprinting for ESM since previously when `require.main.filename` was not available, `process.cwd()` was used. --- packages/node-experimental/src/index.ts | 2 + packages/node/src/index.ts | 9 ++- packages/node/src/integrations/anr/worker.ts | 5 +- packages/node/src/module.ts | 83 +++++++++----------- packages/node/src/sdk.ts | 4 +- packages/node/test/module.test.ts | 39 ++++----- 6 files changed, 68 insertions(+), 74 deletions(-) diff --git a/packages/node-experimental/src/index.ts b/packages/node-experimental/src/index.ts index f9d065de52db..d44fb10da9c1 100644 --- a/packages/node-experimental/src/index.ts +++ b/packages/node-experimental/src/index.ts @@ -52,7 +52,9 @@ export { extractRequestData, // eslint-disable-next-line deprecation/deprecation deepReadDirSync, + // eslint-disable-next-line deprecation/deprecation getModuleFromFilename, + createGetModuleFromFilename, close, createTransport, // eslint-disable-next-line deprecation/deprecation diff --git a/packages/node/src/index.ts b/packages/node/src/index.ts index ba0f2333df6d..21735a64d6a1 100644 --- a/packages/node/src/index.ts +++ b/packages/node/src/index.ts @@ -86,7 +86,14 @@ export { defaultIntegrations, init, defaultStackParser, getSentryRelease } from export { addRequestDataToEvent, DEFAULT_USER_INCLUDES, extractRequestData } from '@sentry/utils'; // eslint-disable-next-line deprecation/deprecation export { deepReadDirSync } from './utils'; -export { getModuleFromFilename } from './module'; + +import { createGetModuleFromFilename } from './module'; +/** + * @deprecated use `createGetModuleFromFilename` instead. + */ +export const getModuleFromFilename = createGetModuleFromFilename(); +export { createGetModuleFromFilename }; + // eslint-disable-next-line deprecation/deprecation export { enableAnrDetection } from './integrations/anr/legacy'; diff --git a/packages/node/src/integrations/anr/worker.ts b/packages/node/src/integrations/anr/worker.ts index f87ff8bb672f..4f9278b5b78f 100644 --- a/packages/node/src/integrations/anr/worker.ts +++ b/packages/node/src/integrations/anr/worker.ts @@ -15,6 +15,8 @@ import { } from '@sentry/utils'; import { Session as InspectorSession } from 'inspector'; import { parentPort, workerData } from 'worker_threads'; + +import { createGetModuleFromFilename } from '../../module'; import { makeNodeTransport } from '../../transports'; import type { WorkerStartData } from './common'; @@ -158,8 +160,9 @@ if (options.captureStackTrace) { // copy the frames const callFrames = [...event.params.callFrames]; + const getModuleName = options.appRootPath ? createGetModuleFromFilename(options.appRootPath) : () => undefined; const stackFrames = callFrames.map(frame => - callFrameToStackFrame(frame, scripts.get(frame.location.scriptId), () => undefined), + callFrameToStackFrame(frame, scripts.get(frame.location.scriptId), getModuleName), ); // Evaluate a script in the currently paused context diff --git a/packages/node/src/module.ts b/packages/node/src/module.ts index 73364493ddb2..a2e93fe99115 100644 --- a/packages/node/src/module.ts +++ b/packages/node/src/module.ts @@ -8,62 +8,51 @@ function normalizeWindowsPath(path: string): string { .replace(/\\/g, '/'); // replace all `\` instances with `/` } -// We cache this so we don't have to recompute it -let basePath: string | undefined; - -function getBasePath(): string { - if (!basePath) { - const baseDir = - require && require.main && require.main.filename ? dirname(require.main.filename) : global.process.cwd(); - basePath = `${baseDir}/`; - } - - return basePath; -} - -/** Gets the module from a filename */ -export function getModuleFromFilename( - filename: string | undefined, - basePath: string = getBasePath(), +/** Creates a function that gets the module name from a filename */ +export function createGetModuleFromFilename( + basePath: string = dirname(process.argv[1]), isWindows: boolean = sep === '\\', -): string | undefined { - if (!filename) { - return; - } - +): (filename: string | undefined) => string | undefined { const normalizedBase = isWindows ? normalizeWindowsPath(basePath) : basePath; - const normalizedFilename = isWindows ? normalizeWindowsPath(filename) : filename; - // eslint-disable-next-line prefer-const - let { dir, base: file, ext } = posix.parse(normalizedFilename); + return (filename: string | undefined) => { + if (!filename) { + return; + } - if (ext === '.js' || ext === '.mjs' || ext === '.cjs') { - file = file.slice(0, ext.length * -1); - } + const normalizedFilename = isWindows ? normalizeWindowsPath(filename) : filename; - if (!dir) { - // No dirname whatsoever - dir = '.'; - } + // eslint-disable-next-line prefer-const + let { dir, base: file, ext } = posix.parse(normalizedFilename); - let n = dir.lastIndexOf('/node_modules'); - if (n > -1) { - return `${dir.slice(n + 14).replace(/\//g, '.')}:${file}`; - } + if (ext === '.js' || ext === '.mjs' || ext === '.cjs') { + file = file.slice(0, ext.length * -1); + } - // Let's see if it's a part of the main module - // To be a part of main module, it has to share the same base - n = `${dir}/`.lastIndexOf(normalizedBase, 0); - if (n === 0) { - let moduleName = dir.slice(normalizedBase.length).replace(/\//g, '.'); + if (!dir) { + // No dirname whatsoever + dir = '.'; + } - if (moduleName) { - moduleName += ':'; + let n = dir.lastIndexOf('/node_modules'); + if (n > -1) { + return `${dir.slice(n + 14).replace(/\//g, '.')}:${file}`; } - moduleName += file; - return moduleName; - } + // Let's see if it's a part of the main module + // To be a part of main module, it has to share the same base + n = `${dir}/`.lastIndexOf(normalizedBase, 0); + if (n === 0) { + let moduleName = dir.slice(normalizedBase.length).replace(/\//g, '.'); + + if (moduleName) { + moduleName += ':'; + } + moduleName += file; + + return moduleName; + } - return file; + return file; + }; } diff --git a/packages/node/src/sdk.ts b/packages/node/src/sdk.ts index 88a6b1a9074c..59a895b0809c 100644 --- a/packages/node/src/sdk.ts +++ b/packages/node/src/sdk.ts @@ -34,7 +34,7 @@ import { Spotlight, Undici, } from './integrations'; -import { getModuleFromFilename } from './module'; +import { createGetModuleFromFilename } from './module'; import { makeNodeTransport } from './transports'; import type { NodeClientOptions, NodeOptions } from './types'; @@ -240,7 +240,7 @@ export function getSentryRelease(fallback?: string): string | undefined { } /** Node.js stack parser */ -export const defaultStackParser: StackParser = createStackParser(nodeStackLineParser(getModuleFromFilename)); +export const defaultStackParser: StackParser = createStackParser(nodeStackLineParser(createGetModuleFromFilename())); /** * Enable automatic Session Tracking for the node process. diff --git a/packages/node/test/module.test.ts b/packages/node/test/module.test.ts index 04a6a95a7888..3fdcdccfa6eb 100644 --- a/packages/node/test/module.test.ts +++ b/packages/node/test/module.test.ts @@ -1,40 +1,33 @@ -import { getModuleFromFilename } from '../src/module'; +import { createGetModuleFromFilename } from '../src/module'; -describe('getModuleFromFilename', () => { - test('Windows', () => { - expect( - getModuleFromFilename('C:\\Users\\Tim\\node_modules\\some-dep\\module.js', 'C:\\Users\\Tim\\', true), - ).toEqual('some-dep:module'); +const getModuleFromFilenameWindows = createGetModuleFromFilename('C:\\Users\\Tim\\', true); +const getModuleFromFilenamePosix = createGetModuleFromFilename('/Users/Tim/'); - expect(getModuleFromFilename('C:\\Users\\Tim\\some\\more\\feature.js', 'C:\\Users\\Tim\\', true)).toEqual( - 'some.more:feature', +describe('createGetModuleFromFilename', () => { + test('Windows', () => { + expect(getModuleFromFilenameWindows('C:\\Users\\Tim\\node_modules\\some-dep\\module.js')).toEqual( + 'some-dep:module', ); + expect(getModuleFromFilenameWindows('C:\\Users\\Tim\\some\\more\\feature.js')).toEqual('some.more:feature'); }); test('POSIX', () => { - expect(getModuleFromFilename('/Users/Tim/node_modules/some-dep/module.js', '/Users/Tim/')).toEqual( - 'some-dep:module', - ); - - expect(getModuleFromFilename('/Users/Tim/some/more/feature.js', '/Users/Tim/')).toEqual('some.more:feature'); - expect(getModuleFromFilename('/Users/Tim/main.js', '/Users/Tim/')).toEqual('main'); + expect(getModuleFromFilenamePosix('/Users/Tim/node_modules/some-dep/module.js')).toEqual('some-dep:module'); + expect(getModuleFromFilenamePosix('/Users/Tim/some/more/feature.js')).toEqual('some.more:feature'); + expect(getModuleFromFilenamePosix('/Users/Tim/main.js')).toEqual('main'); }); test('.mjs', () => { - expect(getModuleFromFilename('/Users/Tim/node_modules/some-dep/module.mjs', '/Users/Tim/')).toEqual( - 'some-dep:module', - ); + expect(getModuleFromFilenamePosix('/Users/Tim/node_modules/some-dep/module.mjs')).toEqual('some-dep:module'); }); test('.cjs', () => { - expect(getModuleFromFilename('/Users/Tim/node_modules/some-dep/module.cjs', '/Users/Tim/')).toEqual( - 'some-dep:module', - ); + expect(getModuleFromFilenamePosix('/Users/Tim/node_modules/some-dep/module.cjs')).toEqual('some-dep:module'); }); test('node internal', () => { - expect(getModuleFromFilename('node.js', '/Users/Tim/')).toEqual('node'); - expect(getModuleFromFilename('node:internal/process/task_queues', '/Users/Tim/')).toEqual('task_queues'); - expect(getModuleFromFilename('node:internal/timers', '/Users/Tim/')).toEqual('timers'); + expect(getModuleFromFilenamePosix('node.js')).toEqual('node'); + expect(getModuleFromFilenamePosix('node:internal/process/task_queues')).toEqual('task_queues'); + expect(getModuleFromFilenamePosix('node:internal/timers')).toEqual('timers'); }); }); From a157d98c3ad00ee4b13ead683517cab78a9d32e6 Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Wed, 10 Jan 2024 08:45:39 +0100 Subject: [PATCH 37/43] feat(replay): Update rrweb to 2.7.3 (#10072) This bump contains the following changes: - fix(rrweb): Use unpatched requestAnimationFrame when possible [#150](https://github.com/getsentry/rrweb/pull/150) - ref: Avoid async in canvas [#143](https://github.com/getsentry/rrweb/pull/143) - feat: Bundle canvas worker manually [#144](https://github.com/getsentry/rrweb/pull/144) - build: Build for ES2020 [#142](https://github.com/getsentry/rrweb/pull/142) Extracted out from https://github.com/getsentry/sentry-javascript/pull/9826 Closes https://github.com/getsentry/sentry-javascript/issues/6946 --- .../browser-integration-tests/package.json | 2 +- packages/replay/package.json | 4 +- yarn.lock | 42 +++++++++---------- 3 files changed, 24 insertions(+), 24 deletions(-) diff --git a/dev-packages/browser-integration-tests/package.json b/dev-packages/browser-integration-tests/package.json index 5c880aa59b8c..7641db7f8499 100644 --- a/dev-packages/browser-integration-tests/package.json +++ b/dev-packages/browser-integration-tests/package.json @@ -45,7 +45,7 @@ "dependencies": { "@babel/preset-typescript": "^7.16.7", "@playwright/test": "^1.31.1", - "@sentry-internal/rrweb": "2.6.0", + "@sentry-internal/rrweb": "2.7.3", "@sentry/browser": "7.92.0", "@sentry/tracing": "7.92.0", "axios": "1.6.0", diff --git a/packages/replay/package.json b/packages/replay/package.json index 54e8f7f11988..7cb1051110a6 100644 --- a/packages/replay/package.json +++ b/packages/replay/package.json @@ -54,8 +54,8 @@ "devDependencies": { "@babel/core": "^7.17.5", "@sentry-internal/replay-worker": "7.92.0", - "@sentry-internal/rrweb": "2.6.0", - "@sentry-internal/rrweb-snapshot": "2.6.0", + "@sentry-internal/rrweb": "2.7.3", + "@sentry-internal/rrweb-snapshot": "2.7.3", "fflate": "^0.8.1", "jsdom-worker": "^0.2.1" }, diff --git a/yarn.lock b/yarn.lock index 0887fc534495..3f7c2a8141c7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5231,33 +5231,33 @@ semver "7.3.2" semver-intersect "1.4.0" -"@sentry-internal/rrdom@2.6.0": - version "2.6.0" - resolved "https://registry.yarnpkg.com/@sentry-internal/rrdom/-/rrdom-2.6.0.tgz#19b5ab7a01ad5031be2d4bcedd4afedb44ee2bed" - integrity sha512-XqxOhLk/CdrKh0toOKeQ6mOcjLDK3B1KY/UVqM9VwhdVhiHeMwPj6GjJUoNkEXh0MwkDM0pzIMv95oSq7hGhPg== +"@sentry-internal/rrdom@2.7.3": + version "2.7.3" + resolved "https://registry.yarnpkg.com/@sentry-internal/rrdom/-/rrdom-2.7.3.tgz#2efe68a9cf23de9a8970acf4303748cdd7866b20" + integrity sha512-XD14G4Lv3ppvJlR7VkkCgHTKu1ylh7yvXdSsN5/FyGTH+IAXQIKL5nINIgWZTN3noNBWV9R0vcHDufXG/WktWA== dependencies: - "@sentry-internal/rrweb-snapshot" "2.6.0" + "@sentry-internal/rrweb-snapshot" "2.7.3" -"@sentry-internal/rrweb-snapshot@2.6.0": - version "2.6.0" - resolved "https://registry.yarnpkg.com/@sentry-internal/rrweb-snapshot/-/rrweb-snapshot-2.6.0.tgz#1b214c9ab4645138a02ef255c95d811bd852f996" - integrity sha512-dlduO37avs5HBP8zRxFHlhRb7ZP6p3SrgMSztPCCnfYr/XAB/rn5yeVn9U2FDYdrgyUzPjFWfYWFvm1eJuEMSg== +"@sentry-internal/rrweb-snapshot@2.7.3": + version "2.7.3" + resolved "https://registry.yarnpkg.com/@sentry-internal/rrweb-snapshot/-/rrweb-snapshot-2.7.3.tgz#9a7173825a31c07ccf27a5956f154400e11fdd97" + integrity sha512-mSZuBPmWia3x9wCuaJiZMD9ZVDnFv7TSG1Nz9X4ZqWb3DdaxB2MogGUU/2aTVqmRj6F91nl+GHb5NpmNYojUsw== -"@sentry-internal/rrweb-types@2.6.0": - version "2.6.0" - resolved "https://registry.yarnpkg.com/@sentry-internal/rrweb-types/-/rrweb-types-2.6.0.tgz#e526994125db6684ce9402d96f64318f062bebb0" - integrity sha512-mPPumdbyNHF24zShvZqzqgkZRsJHhlNpglGTS0cR/PkX2QdG0CtsPVFpaYj6UQAFGpfb2Aj7VdkKuuzX4RX69w== +"@sentry-internal/rrweb-types@2.7.3": + version "2.7.3" + resolved "https://registry.yarnpkg.com/@sentry-internal/rrweb-types/-/rrweb-types-2.7.3.tgz#e38737bc5c31aa9dfb8ce8faf46af374c2aa7cbb" + integrity sha512-EqALxhZtvH0rimYfj7J48DRC+fj+AGsZ/VDdOPKh3MQXptTyHncoWBj4ZtB1AaH7foYUr+2wkyxl3HqMVwe+6g== dependencies: - "@sentry-internal/rrweb-snapshot" "2.6.0" + "@sentry-internal/rrweb-snapshot" "2.7.3" -"@sentry-internal/rrweb@2.6.0": - version "2.6.0" - resolved "https://registry.yarnpkg.com/@sentry-internal/rrweb/-/rrweb-2.6.0.tgz#0976e0d021c965b491a5193546a735f78dad9107" - integrity sha512-N+v0cgft/mikwIH5MPIspWNEqHa3E/01rA+IwozTs/TUp2e8sJmF3qxR0+OeBGZU0ln4soG1o18FjsCmadqbeQ== +"@sentry-internal/rrweb@2.7.3": + version "2.7.3" + resolved "https://registry.yarnpkg.com/@sentry-internal/rrweb/-/rrweb-2.7.3.tgz#900ce7b1bd8ec43b5557d73698cc1b4dc9f47f72" + integrity sha512-K2pksQ1FgDumv54r1o0+RAKB3Qx3Zgx6OrQAkU/SI6/7HcBIxe7b4zepg80NyvnfohUs/relw2EoD3K3kqd8tg== dependencies: - "@sentry-internal/rrdom" "2.6.0" - "@sentry-internal/rrweb-snapshot" "2.6.0" - "@sentry-internal/rrweb-types" "2.6.0" + "@sentry-internal/rrdom" "2.7.3" + "@sentry-internal/rrweb-snapshot" "2.7.3" + "@sentry-internal/rrweb-types" "2.7.3" "@types/css-font-loading-module" "0.0.7" "@xstate/fsm" "^1.4.0" base64-arraybuffer "^1.0.1" From a56fc29884d905f469df3873e3178f2c2fc6f3af Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Wed, 10 Jan 2024 10:15:38 +0100 Subject: [PATCH 38/43] feat(core): Deprecate span `tags`, `data`, `context` & setters (#10053) Also deprecate direct access to `span.attributes` (instead use `spanToJSON(span)`). There are a few usages that we still need to figure out how we will replace them...! --- MIGRATION.md | 5 ++ .../startTransaction/basic_usage/test.ts | 1 + .../nextjs-app-dir/tests/transactions.test.ts | 6 +- .../mysql/withoutCallback/scenario.ts | 4 +- .../mysql/withoutCallback/test.ts | 12 ++-- docs/v8-new-performance-apis.md | 10 +-- .../browser/src/profiling/hubextensions.ts | 2 + packages/bun/src/integrations/bunserver.ts | 6 +- .../bun/test/integrations/bunserver.test.ts | 2 + packages/core/src/tracing/idletransaction.ts | 2 +- packages/core/src/tracing/span.ts | 63 ++++++++++++++----- packages/core/src/tracing/transaction.ts | 12 ++-- packages/core/src/utils/prepareEvent.ts | 11 +++- packages/core/test/lib/tracing/span.test.ts | 25 +++++--- .../common/withServerActionInstrumentation.ts | 13 ++-- packages/node/src/handlers.ts | 2 + packages/node/src/integrations/hapi/types.ts | 1 + packages/node/test/handlers.test.ts | 5 +- packages/node/test/integrations/http.test.ts | 21 ++++--- .../opentelemetry-node/src/spanprocessor.ts | 20 +++--- .../test/spanprocessor.test.ts | 26 ++++---- packages/opentelemetry/src/spanExporter.ts | 22 ++++--- packages/sveltekit/src/client/router.ts | 1 + packages/sveltekit/test/client/router.test.ts | 2 + .../src/browser/backgroundtab.ts | 2 + .../src/browser/browsertracing.ts | 1 + .../src/browser/metrics/index.ts | 20 +++++- .../tracing-internal/src/browser/request.ts | 7 ++- packages/tracing-internal/src/common/fetch.ts | 2 +- .../src/node/integrations/mysql.ts | 4 +- .../test/browser/backgroundtab.test.ts | 1 + .../test/browser/request.test.ts | 2 +- packages/types/src/span.ts | 13 +++- packages/types/src/transaction.ts | 12 ++-- packages/utils/src/requestdata.ts | 7 ++- packages/vue/src/router.ts | 3 + 36 files changed, 234 insertions(+), 114 deletions(-) diff --git a/MIGRATION.md b/MIGRATION.md index c7587fc10c76..57fd79bcb18e 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -100,6 +100,11 @@ In v8, the Span class is heavily reworked. The following properties & methods ar * `span.description`: Use `spanToJSON(span).description` instead. * `transaction.setMetadata()`: Use attributes instead, or set data on the scope. * `transaction.metadata`: Use attributes instead, or set data on the scope. +* `span.tags`: Set tags on the surrounding scope instead, or use attributes. +* `span.data`: Use `spanToJSON(span).data` instead. +* `span.setTag()`: Use `span.setAttribute()` instead or set tags on the surrounding scope. +* `span.setData()`: Use `span.setAttribute()` instead. +* `transaction.setContext()`: Set context on the surrounding scope instead. ## Deprecate `pushScope` & `popScope` in favor of `withScope` diff --git a/dev-packages/browser-integration-tests/suites/public-api/startTransaction/basic_usage/test.ts b/dev-packages/browser-integration-tests/suites/public-api/startTransaction/basic_usage/test.ts index 04e39e012639..1012ae61e1ff 100644 --- a/dev-packages/browser-integration-tests/suites/public-api/startTransaction/basic_usage/test.ts +++ b/dev-packages/browser-integration-tests/suites/public-api/startTransaction/basic_usage/test.ts @@ -31,6 +31,7 @@ sentryTest('should report finished spans as children of the root transaction', a const span_1 = transaction.spans?.[0]; expect(span_1?.op).toBe('span_1'); expect(span_1?.parentSpanId).toEqual(rootSpanId); + // eslint-disable-next-line deprecation/deprecation expect(span_1?.data).toMatchObject({ foo: 'bar', baz: [1, 2, 3] }); const span_3 = transaction.spans?.[1]; diff --git a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/transactions.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/transactions.test.ts index ca4f4cb0d1ff..f975ec49e606 100644 --- a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/transactions.test.ts +++ b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/transactions.test.ts @@ -130,9 +130,9 @@ test('Should send a transaction for instrumented server actions', async ({ page await page.getByText('Run Action').click(); expect(await serverComponentTransactionPromise).toBeDefined(); - expect((await serverComponentTransactionPromise).contexts?.trace?.data?.['server_action_form_data']).toEqual( - expect.objectContaining({ 'some-text-value': 'some-default-value' }), - ); + expect( + (await serverComponentTransactionPromise).contexts?.trace?.data?.['server_action_form_data.some-text-value'], + ).toEqual('some-default-value'); expect((await serverComponentTransactionPromise).contexts?.trace?.data?.['server_action_result']).toEqual({ city: 'Vienna', }); diff --git a/dev-packages/node-integration-tests/suites/tracing-new/auto-instrument/mysql/withoutCallback/scenario.ts b/dev-packages/node-integration-tests/suites/tracing-new/auto-instrument/mysql/withoutCallback/scenario.ts index 9a436695d63f..2b06970b5243 100644 --- a/dev-packages/node-integration-tests/suites/tracing-new/auto-instrument/mysql/withoutCallback/scenario.ts +++ b/dev-packages/node-integration-tests/suites/tracing-new/auto-instrument/mysql/withoutCallback/scenario.ts @@ -32,10 +32,10 @@ const query = connection.query('SELECT 1 + 1 AS solution'); const query2 = connection.query('SELECT NOW()', ['1', '2']); query.on('end', () => { - transaction.setTag('result_done', 'yes'); + transaction.setAttribute('result_done', 'yes'); query2.on('end', () => { - transaction.setTag('result_done2', 'yes'); + transaction.setAttribute('result_done2', 'yes'); // Wait a bit to ensure the queries completed setTimeout(() => { diff --git a/dev-packages/node-integration-tests/suites/tracing-new/auto-instrument/mysql/withoutCallback/test.ts b/dev-packages/node-integration-tests/suites/tracing-new/auto-instrument/mysql/withoutCallback/test.ts index 0f6dee99d59b..f83e4297b8ba 100644 --- a/dev-packages/node-integration-tests/suites/tracing-new/auto-instrument/mysql/withoutCallback/test.ts +++ b/dev-packages/node-integration-tests/suites/tracing-new/auto-instrument/mysql/withoutCallback/test.ts @@ -7,11 +7,15 @@ test('should auto-instrument `mysql` package when using query without callback', expect(envelope).toHaveLength(3); assertSentryTransaction(envelope[2], { - transaction: 'Test Transaction', - tags: { - result_done: 'yes', - result_done2: 'yes', + contexts: { + trace: { + data: { + result_done: 'yes', + result_done2: 'yes', + }, + }, }, + transaction: 'Test Transaction', spans: [ { description: 'SELECT 1 + 1 AS solution', diff --git a/docs/v8-new-performance-apis.md b/docs/v8-new-performance-apis.md index e7ec274bbb10..2f531858dd2c 100644 --- a/docs/v8-new-performance-apis.md +++ b/docs/v8-new-performance-apis.md @@ -50,8 +50,8 @@ below to see which things used to exist, and how they can/should be mapped going | `status` | use utility method TODO | | `sampled` | `spanIsSampled(span)` | | `startTimestamp` | `startTime` - note that this has a different format! | -| `tags` | `spanGetAttributes(span)`, or set tags on the scope | -| `data` | `spanGetAttributes(span)` | +| `tags` | use attributes, or set tags on the scope | +| `data` | `spanToJSON(span).data` | | `transaction` | ??? Removed | | `instrumenter` | Removed | | `finish()` | `end()` | @@ -72,13 +72,13 @@ In addition, a transaction has this API: | Old name | Replace with | | --------------------------- | ------------------------------------------------ | -| `name` | `spanGetName(span)` (TODO) | +| `name` | `spanToJSON(span).description` | | `trimEnd` | Removed | | `parentSampled` | `spanIsSampled(span)` & `spanContext().isRemote` | -| `metadata` | `spanGetMetadata(span)` | +| `metadata` | Use attributes instead or set on scope | | `setContext()` | Set context on scope instead | | `setMeasurement()` | ??? TODO | -| `setMetadata()` | `spanSetMetadata(span, metadata)` | +| `setMetadata()` | Use attributes instead or set on scope | | `getDynamicSamplingContext` | ??? TODO | ### Attributes vs. Data vs. Tags vs. Context diff --git a/packages/browser/src/profiling/hubextensions.ts b/packages/browser/src/profiling/hubextensions.ts index 01a3bfc2bfac..9fd156a1b90b 100644 --- a/packages/browser/src/profiling/hubextensions.ts +++ b/packages/browser/src/profiling/hubextensions.ts @@ -156,6 +156,8 @@ export function startProfileForTransaction(transaction: Transaction): Transactio // Always call onProfileHandler to ensure stopProfiling is called and the timeout is cleared. void onProfileHandler().then( () => { + // TODO: Can we rewrite this to use attributes? + // eslint-disable-next-line deprecation/deprecation transaction.setContext('profile', { profile_id: profileId, start_timestamp: startTimestamp }); originalEnd(); }, diff --git a/packages/bun/src/integrations/bunserver.ts b/packages/bun/src/integrations/bunserver.ts index 695b26edc144..fec3aae439af 100644 --- a/packages/bun/src/integrations/bunserver.ts +++ b/packages/bun/src/integrations/bunserver.ts @@ -4,6 +4,7 @@ import { captureException, continueTrace, convertIntegrationFnToClass, + getCurrentScope, runWithAsyncContext, startSpan, } from '@sentry/core'; @@ -90,9 +91,10 @@ function instrumentBunServeOptions(serveOptions: Parameters[0] >); if (response && response.status) { span?.setHttpStatus(response.status); - span?.setData('http.response.status_code', response.status); + span?.setAttribute('http.response.status_code', response.status); if (span instanceof Transaction) { - span.setContext('response', { + const scope = getCurrentScope(); + scope.setContext('response', { headers: response.headers.toJSON(), status_code: response.status, }); diff --git a/packages/bun/test/integrations/bunserver.test.ts b/packages/bun/test/integrations/bunserver.test.ts index 59b242b5ea7c..6356300562ac 100644 --- a/packages/bun/test/integrations/bunserver.test.ts +++ b/packages/bun/test/integrations/bunserver.test.ts @@ -26,6 +26,7 @@ describe('Bun Serve Integration', () => { test('generates a transaction around a request', async () => { client.on('finishTransaction', transaction => { expect(transaction.status).toBe('ok'); + // eslint-disable-next-line deprecation/deprecation expect(transaction.tags).toEqual({ 'http.status_code': '200', }); @@ -48,6 +49,7 @@ describe('Bun Serve Integration', () => { test('generates a post transaction', async () => { client.on('finishTransaction', transaction => { expect(transaction.status).toBe('ok'); + // eslint-disable-next-line deprecation/deprecation expect(transaction.tags).toEqual({ 'http.status_code': '200', }); diff --git a/packages/core/src/tracing/idletransaction.ts b/packages/core/src/tracing/idletransaction.ts index af567d5dcd22..2735146dbbc4 100644 --- a/packages/core/src/tracing/idletransaction.ts +++ b/packages/core/src/tracing/idletransaction.ts @@ -146,7 +146,7 @@ export class IdleTransaction extends Transaction { this.activities = {}; if (this.op === 'ui.action.click') { - this.setTag(FINISH_REASON_TAG, this._finishReason); + this.setAttribute(FINISH_REASON_TAG, this._finishReason); } if (this.spanRecorder) { diff --git a/packages/core/src/tracing/span.ts b/packages/core/src/tracing/span.ts index 80a1a3da4d32..10a4d97efa08 100644 --- a/packages/core/src/tracing/span.ts +++ b/packages/core/src/tracing/span.ts @@ -86,21 +86,18 @@ export class Span implements SpanInterface { public op?: string; /** - * @inheritDoc + * Tags for the span. + * @deprecated Use `getSpanAttributes(span)` instead. */ public tags: { [key: string]: Primitive }; /** - * @inheritDoc + * Data for the span. + * @deprecated Use `getSpanAttributes(span)` instead. */ // eslint-disable-next-line @typescript-eslint/no-explicit-any public data: { [key: string]: any }; - /** - * @inheritDoc - */ - public attributes: SpanAttributes; - /** * List of spans that were finalized */ @@ -125,6 +122,7 @@ export class Span implements SpanInterface { protected _spanId: string; protected _sampled: boolean | undefined; protected _name?: string; + protected _attributes: SpanAttributes; private _logMessage?: string; @@ -139,9 +137,11 @@ export class Span implements SpanInterface { this._traceId = spanContext.traceId || uuid4(); this._spanId = spanContext.spanId || uuid4().substring(16); this.startTimestamp = spanContext.startTimestamp || timestampInSeconds(); + // eslint-disable-next-line deprecation/deprecation this.tags = spanContext.tags ? { ...spanContext.tags } : {}; + // eslint-disable-next-line deprecation/deprecation this.data = spanContext.data ? { ...spanContext.data } : {}; - this.attributes = spanContext.attributes ? { ...spanContext.attributes } : {}; + this._attributes = spanContext.attributes ? { ...spanContext.attributes } : {}; this.instrumenter = spanContext.instrumenter || 'sentry'; this.origin = spanContext.origin || 'manual'; // eslint-disable-next-line deprecation/deprecation @@ -165,7 +165,7 @@ export class Span implements SpanInterface { } } - // This rule conflicts with another rule :( + // This rule conflicts with another eslint rule :( /* eslint-disable @typescript-eslint/member-ordering */ /** @@ -175,6 +175,7 @@ export class Span implements SpanInterface { public get name(): string { return this._name || ''; } + /** * Update the name of the span. * @deprecated Use `spanToJSON(span).description` instead. @@ -247,6 +248,22 @@ export class Span implements SpanInterface { this._sampled = sampled; } + /** + * Attributes for the span. + * @deprecated Use `getSpanAttributes(span)` instead. + */ + public get attributes(): SpanAttributes { + return this._attributes; + } + + /** + * Attributes for the span. + * @deprecated Use `setAttributes()` instead. + */ + public set attributes(attributes: SpanAttributes) { + this._attributes = attributes; + } + /* eslint-enable @typescript-eslint/member-ordering */ /** @inheritdoc */ @@ -296,18 +313,29 @@ export class Span implements SpanInterface { } /** - * @inheritDoc + * Sets the tag attribute on the current span. + * + * Can also be used to unset a tag, by passing `undefined`. + * + * @param key Tag key + * @param value Tag value + * @deprecated Use `setAttribute()` instead. */ public setTag(key: string, value: Primitive): this { + // eslint-disable-next-line deprecation/deprecation this.tags = { ...this.tags, [key]: value }; return this; } /** - * @inheritDoc + * Sets the data attribute on the current span + * @param key Data key + * @param value Data value + * @deprecated Use `setAttribute()` instead. */ // eslint-disable-next-line @typescript-eslint/no-explicit-any public setData(key: string, value: any): this { + // eslint-disable-next-line deprecation/deprecation this.data = { ...this.data, [key]: value }; return this; } @@ -316,9 +344,9 @@ export class Span implements SpanInterface { public setAttribute(key: string, value: SpanAttributeValue | undefined): void { if (value === undefined) { // eslint-disable-next-line @typescript-eslint/no-dynamic-delete - delete this.attributes[key]; + delete this._attributes[key]; } else { - this.attributes[key] = value; + this._attributes[key] = value; } } @@ -339,7 +367,9 @@ export class Span implements SpanInterface { * @inheritDoc */ public setHttpStatus(httpStatus: number): this { + // eslint-disable-next-line deprecation/deprecation this.setTag('http.status_code', String(httpStatus)); + // eslint-disable-next-line deprecation/deprecation this.setData('http.response.status_code', httpStatus); const spanStatus = spanStatusfromHttpCode(httpStatus); if (spanStatus !== 'unknown_error') { @@ -415,6 +445,7 @@ export class Span implements SpanInterface { spanId: this._spanId, startTimestamp: this.startTimestamp, status: this.status, + // eslint-disable-next-line deprecation/deprecation tags: this.tags, traceId: this._traceId, }); @@ -424,6 +455,7 @@ export class Span implements SpanInterface { * @inheritDoc */ public updateWithContext(spanContext: SpanContext): this { + // eslint-disable-next-line deprecation/deprecation this.data = spanContext.data || {}; // eslint-disable-next-line deprecation/deprecation this._name = spanContext.name || spanContext.description; @@ -434,6 +466,7 @@ export class Span implements SpanInterface { this._spanId = spanContext.spanId || this._spanId; this.startTimestamp = spanContext.startTimestamp || this.startTimestamp; this.status = spanContext.status; + // eslint-disable-next-line deprecation/deprecation this.tags = spanContext.tags || {}; this._traceId = spanContext.traceId || this._traceId; @@ -459,6 +492,7 @@ export class Span implements SpanInterface { span_id: this._spanId, start_timestamp: this.startTimestamp, status: this.status, + // eslint-disable-next-line deprecation/deprecation tags: Object.keys(this.tags).length > 0 ? this.tags : undefined, timestamp: this.endTimestamp, trace_id: this._traceId, @@ -490,7 +524,8 @@ export class Span implements SpanInterface { [key: string]: any; } | undefined { - const { data, attributes } = this; + // eslint-disable-next-line deprecation/deprecation + const { data, _attributes: attributes } = this; const hasData = Object.keys(data).length > 0; const hasAttributes = Object.keys(attributes).length > 0; diff --git a/packages/core/src/tracing/transaction.ts b/packages/core/src/tracing/transaction.ts index 7d13038de6e0..c68d9e6fbeeb 100644 --- a/packages/core/src/tracing/transaction.ts +++ b/packages/core/src/tracing/transaction.ts @@ -110,11 +110,11 @@ export class Transaction extends SpanClass implements TransactionInterface { ...this._metadata, // From attributes - ...(this.attributes[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE] && { - source: this.attributes[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE] as TransactionMetadata['source'], + ...(this._attributes[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE] && { + source: this._attributes[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE] as TransactionMetadata['source'], }), - ...(this.attributes[SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE] && { - sampleRate: this.attributes[SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE] as TransactionMetadata['sampleRate'], + ...(this._attributes[SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE] && { + sampleRate: this._attributes[SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE] as TransactionMetadata['sampleRate'], }), }; } @@ -157,7 +157,8 @@ export class Transaction extends SpanClass implements TransactionInterface { } /** - * @inheritDoc + * Set the context of a transaction event. + * @deprecated Use either `.setAttribute()`, or set the context on the scope before creating the transaction. */ public setContext(key: string, context: Context | null): void { if (context === null) { @@ -334,6 +335,7 @@ export class Transaction extends SpanClass implements TransactionInterface { // TODO: Pass spans serialized via `spanToJSON()` here instead in v8. spans: finishedSpans, start_timestamp: this.startTimestamp, + // eslint-disable-next-line deprecation/deprecation tags: this.tags, timestamp: this.endTimestamp, transaction: this._name, diff --git a/packages/core/src/utils/prepareEvent.ts b/packages/core/src/utils/prepareEvent.ts index 6f448df8496d..17be8c5354de 100644 --- a/packages/core/src/utils/prepareEvent.ts +++ b/packages/core/src/utils/prepareEvent.ts @@ -15,6 +15,7 @@ import { DEFAULT_ENVIRONMENT } from '../constants'; import { getGlobalEventProcessors, notifyEventProcessors } from '../eventProcessors'; import { Scope, getGlobalScope } from '../scope'; import { applyScopeDataToEvent, mergeScopeData } from './applyScopeDataToEvent'; +import { spanToJSON } from './spanUtils'; /** * This type makes sure that we get either a CaptureContext, OR an EventHint. @@ -326,10 +327,14 @@ function normalizeEvent(event: Event | null, depth: number, maxBreadth: number): // event.spans[].data may contain circular/dangerous data so we need to normalize it if (event.spans) { normalized.spans = event.spans.map(span => { - // We cannot use the spread operator here because `toJSON` on `span` is non-enumerable - if (span.data) { - span.data = normalize(span.data, depth, maxBreadth); + const data = spanToJSON(span).data; + + if (data) { + // This is a bit weird, as we generally have `Span` instances here, but to be safe we do not assume so + // eslint-disable-next-line deprecation/deprecation + span.data = normalize(data, depth, maxBreadth); } + return span; }); } diff --git a/packages/core/test/lib/tracing/span.test.ts b/packages/core/test/lib/tracing/span.test.ts index 3b48cb3d8640..1adff93123ac 100644 --- a/packages/core/test/lib/tracing/span.test.ts +++ b/packages/core/test/lib/tracing/span.test.ts @@ -74,7 +74,7 @@ describe('span', () => { span.setAttribute('boolArray', [true, false]); span.setAttribute('arrayWithUndefined', [1, undefined, 2]); - expect(span.attributes).toEqual({ + expect(span['_attributes']).toEqual({ str: 'bar', num: 1, zero: 0, @@ -92,11 +92,11 @@ describe('span', () => { span.setAttribute('str', 'bar'); - expect(Object.keys(span.attributes).length).toEqual(1); + expect(Object.keys(span['_attributes']).length).toEqual(1); span.setAttribute('str', undefined); - expect(Object.keys(span.attributes).length).toEqual(0); + expect(Object.keys(span['_attributes']).length).toEqual(0); }); it('disallows invalid attribute types', () => { @@ -117,7 +117,7 @@ describe('span', () => { it('allows to set attributes', () => { const span = new Span(); - const initialAttributes = span.attributes; + const initialAttributes = span['_attributes']; expect(initialAttributes).toEqual({}); @@ -135,7 +135,7 @@ describe('span', () => { }; span.setAttributes(newAttributes); - expect(span.attributes).toEqual({ + expect(span['_attributes']).toEqual({ str: 'bar', num: 1, zero: 0, @@ -147,14 +147,14 @@ describe('span', () => { arrayWithUndefined: [1, undefined, 2], }); - expect(span.attributes).not.toBe(newAttributes); + expect(span['_attributes']).not.toBe(newAttributes); span.setAttributes({ num: 2, numArray: [3, 4], }); - expect(span.attributes).toEqual({ + expect(span['_attributes']).toEqual({ str: 'bar', num: 2, zero: 0, @@ -172,11 +172,11 @@ describe('span', () => { span.setAttribute('str', 'bar'); - expect(Object.keys(span.attributes).length).toEqual(1); + expect(Object.keys(span['_attributes']).length).toEqual(1); span.setAttributes({ str: undefined }); - expect(Object.keys(span.attributes).length).toEqual(0); + expect(Object.keys(span['_attributes']).length).toEqual(0); }); }); @@ -270,9 +270,11 @@ describe('span', () => { it('works with data only', () => { const span = new Span(); + // eslint-disable-next-line deprecation/deprecation span.setData('foo', 'bar'); expect(span['_getData']()).toEqual({ foo: 'bar' }); + // eslint-disable-next-line deprecation/deprecation expect(span['_getData']()).toBe(span.data); }); @@ -281,6 +283,7 @@ describe('span', () => { span.setAttribute('foo', 'bar'); expect(span['_getData']()).toEqual({ foo: 'bar' }); + // eslint-disable-next-line deprecation/deprecation expect(span['_getData']()).toBe(span.attributes); }); @@ -288,11 +291,15 @@ describe('span', () => { const span = new Span(); span.setAttribute('foo', 'foo'); span.setAttribute('bar', 'bar'); + // eslint-disable-next-line deprecation/deprecation span.setData('foo', 'foo2'); + // eslint-disable-next-line deprecation/deprecation span.setData('baz', 'baz'); expect(span['_getData']()).toEqual({ foo: 'foo', bar: 'bar', baz: 'baz' }); + // eslint-disable-next-line deprecation/deprecation expect(span['_getData']()).not.toBe(span.attributes); + // eslint-disable-next-line deprecation/deprecation expect(span['_getData']()).not.toBe(span.data); }); }); diff --git a/packages/nextjs/src/common/withServerActionInstrumentation.ts b/packages/nextjs/src/common/withServerActionInstrumentation.ts index 85872ac7d703..01e1c75d6f3f 100644 --- a/packages/nextjs/src/common/withServerActionInstrumentation.ts +++ b/packages/nextjs/src/common/withServerActionInstrumentation.ts @@ -104,19 +104,16 @@ async function withServerActionInstrumentationImplementation = {}; options.formData.forEach((value, key) => { - if (typeof value === 'string') { - formDataObject[key] = value; - } else { - formDataObject[key] = '[non-string value]'; - } + span?.setAttribute( + `server_action_form_data.${key}`, + typeof value === 'string' ? value : '[non-string value]', + ); }); - span?.setData('server_action_form_data', formDataObject); } return result; diff --git a/packages/node/src/handlers.ts b/packages/node/src/handlers.ts index 3a4738d8d2f3..892aabd2dd84 100644 --- a/packages/node/src/handlers.ts +++ b/packages/node/src/handlers.ts @@ -351,6 +351,8 @@ export function trpcMiddleware(options: SentryTrpcMiddlewareOptions = {}) { trpcContext.input = normalize(rawInput); } + // TODO: Can we rewrite this to an attribute? Or set this on the scope? + // eslint-disable-next-line deprecation/deprecation sentryTransaction.setContext('trpc', trpcContext); } diff --git a/packages/node/src/integrations/hapi/types.ts b/packages/node/src/integrations/hapi/types.ts index d74c171ef441..a650667fe362 100644 --- a/packages/node/src/integrations/hapi/types.ts +++ b/packages/node/src/integrations/hapi/types.ts @@ -3,6 +3,7 @@ /* eslint-disable @typescript-eslint/unified-signatures */ /* eslint-disable @typescript-eslint/no-empty-interface */ /* eslint-disable @typescript-eslint/no-namespace */ +/* eslint-disable @typescript-eslint/no-explicit-any */ // Vendored and simplified from: // - @types/hapi__hapi diff --git a/packages/node/test/handlers.test.ts b/packages/node/test/handlers.test.ts index 6a25a1bcc4b0..888991de439c 100644 --- a/packages/node/test/handlers.test.ts +++ b/packages/node/test/handlers.test.ts @@ -372,8 +372,11 @@ describe('tracingHandler', () => { setImmediate(() => { expect(finishTransaction).toHaveBeenCalled(); expect(transaction.status).toBe('ok'); + // eslint-disable-next-line deprecation/deprecation expect(transaction.tags).toEqual(expect.objectContaining({ 'http.status_code': '200' })); - expect(transaction.data).toEqual(expect.objectContaining({ 'http.response.status_code': 200 })); + expect(sentryCore.spanToJSON(transaction).data).toEqual( + expect.objectContaining({ 'http.response.status_code': 200 }), + ); done(); }); }); diff --git a/packages/node/test/integrations/http.test.ts b/packages/node/test/integrations/http.test.ts index 5a1603988ee2..0f1ad687684d 100644 --- a/packages/node/test/integrations/http.test.ts +++ b/packages/node/test/integrations/http.test.ts @@ -300,10 +300,13 @@ describe('tracing', () => { // our span is at index 1 because the transaction itself is at index 0 expect(sentryCore.spanToJSON(spans[1]).description).toEqual('GET http://dogs.are.great/spaniel'); expect(spans[1].op).toEqual('http.client'); - expect(spans[1].data['http.method']).toEqual('GET'); - expect(spans[1].data.url).toEqual('http://dogs.are.great/spaniel'); - expect(spans[1].data['http.query']).toEqual('tail=wag&cute=true'); - expect(spans[1].data['http.fragment']).toEqual('learn-more'); + + const spanAttributes = sentryCore.spanToJSON(spans[1]).data || {}; + + expect(spanAttributes['http.method']).toEqual('GET'); + expect(spanAttributes.url).toEqual('http://dogs.are.great/spaniel'); + expect(spanAttributes['http.query']).toEqual('tail=wag&cute=true'); + expect(spanAttributes['http.fragment']).toEqual('learn-more'); }); it('fills in span data from http.RequestOptions object', () => { @@ -316,13 +319,15 @@ describe('tracing', () => { expect(spans.length).toEqual(2); + const spanAttributes = sentryCore.spanToJSON(spans[1]).data || {}; + // our span is at index 1 because the transaction itself is at index 0 expect(sentryCore.spanToJSON(spans[1]).description).toEqual('GET http://dogs.are.great/spaniel'); expect(spans[1].op).toEqual('http.client'); - expect(spans[1].data['http.method']).toEqual('GET'); - expect(spans[1].data.url).toEqual('http://dogs.are.great/spaniel'); - expect(spans[1].data['http.query']).toEqual('tail=wag&cute=true'); - expect(spans[1].data['http.fragment']).toEqual('learn-more'); + expect(spanAttributes['http.method']).toEqual('GET'); + expect(spanAttributes.url).toEqual('http://dogs.are.great/spaniel'); + expect(spanAttributes['http.query']).toEqual('tail=wag&cute=true'); + expect(spanAttributes['http.fragment']).toEqual('learn-more'); }); it.each([ diff --git a/packages/opentelemetry-node/src/spanprocessor.ts b/packages/opentelemetry-node/src/spanprocessor.ts index b6ca1fe53254..dcec2ebeef43 100644 --- a/packages/opentelemetry-node/src/spanprocessor.ts +++ b/packages/opentelemetry-node/src/spanprocessor.ts @@ -194,14 +194,13 @@ function updateSpanWithOtelData(sentrySpan: SentrySpan, otelSpan: OtelSpan): voi const { op, description, data } = parseOtelSpanDescription(otelSpan); sentrySpan.setStatus(mapOtelStatus(otelSpan)); - sentrySpan.setData('otel.kind', SpanKind[kind]); - const allData = { ...attributes, ...data }; - - Object.keys(allData).forEach(prop => { - const value = allData[prop]; - sentrySpan.setData(prop, value); - }); + const allData = { + ...attributes, + ...data, + 'otel.kind': SpanKind[kind], + }; + sentrySpan.setAttributes(allData); sentrySpan.op = op; sentrySpan.updateName(description); @@ -210,17 +209,14 @@ function updateSpanWithOtelData(sentrySpan: SentrySpan, otelSpan: OtelSpan): voi function updateTransactionWithOtelData(transaction: Transaction, otelSpan: OtelSpan): void { const { op, description, source, data } = parseOtelSpanDescription(otelSpan); + // eslint-disable-next-line deprecation/deprecation transaction.setContext('otel', { attributes: otelSpan.attributes, resource: otelSpan.resource.attributes, }); const allData = data || {}; - - Object.keys(allData).forEach(prop => { - const value = allData[prop]; - transaction.setData(prop, value); - }); + transaction.setAttributes(allData); transaction.setStatus(mapOtelStatus(otelSpan)); diff --git a/packages/opentelemetry-node/test/spanprocessor.test.ts b/packages/opentelemetry-node/test/spanprocessor.test.ts index c1d3c209ecca..f4bc26041ceb 100644 --- a/packages/opentelemetry-node/test/spanprocessor.test.ts +++ b/packages/opentelemetry-node/test/spanprocessor.test.ts @@ -320,11 +320,11 @@ describe('SentrySpanProcessor', () => { const sentrySpan = getSpanForOtelSpan(child); - expect(sentrySpan?.data).toEqual({}); + expect(spanToJSON(sentrySpan!).data).toEqual(undefined); child.end(); - expect(sentrySpan?.data).toEqual({ + expect(spanToJSON(sentrySpan!).data).toEqual({ 'otel.kind': 'INTERNAL', 'test-attribute': 'test-value', 'test-attribute-2': [1, 2, 3], @@ -539,8 +539,10 @@ describe('SentrySpanProcessor', () => { child.end(); - expect(sentrySpan ? spanToJSON(sentrySpan).description : undefined).toBe('GET /my/route/{id}'); - expect(sentrySpan?.data).toEqual({ + const { description, data } = spanToJSON(sentrySpan!); + + expect(description).toBe('GET /my/route/{id}'); + expect(data).toEqual({ 'http.method': 'GET', 'http.route': '/my/route/{id}', 'http.target': '/my/route/123', @@ -567,10 +569,10 @@ describe('SentrySpanProcessor', () => { child.end(); - expect(sentrySpan ? spanToJSON(sentrySpan).description : undefined).toBe( - 'GET http://example.com/my/route/123', - ); - expect(sentrySpan?.data).toEqual({ + const { description, data } = spanToJSON(sentrySpan!); + + expect(description).toBe('GET http://example.com/my/route/123'); + expect(data).toEqual({ 'http.method': 'GET', 'http.target': '/my/route/123', 'http.url': 'http://example.com/my/route/123', @@ -596,10 +598,10 @@ describe('SentrySpanProcessor', () => { child.end(); - expect(sentrySpan ? spanToJSON(sentrySpan).description : undefined).toBe( - 'GET http://example.com/my/route/123', - ); - expect(sentrySpan?.data).toEqual({ + const { description, data } = spanToJSON(sentrySpan!); + + expect(description).toBe('GET http://example.com/my/route/123'); + expect(data).toEqual({ 'http.method': 'GET', 'http.target': '/my/route/123', 'http.url': 'http://example.com/my/route/123?what=123#myHash', diff --git a/packages/opentelemetry/src/spanExporter.ts b/packages/opentelemetry/src/spanExporter.ts index 78557e50d33c..7394e413e7ad 100644 --- a/packages/opentelemetry/src/spanExporter.ts +++ b/packages/opentelemetry/src/spanExporter.ts @@ -3,8 +3,8 @@ import type { ExportResult } from '@opentelemetry/core'; import { ExportResultCode } from '@opentelemetry/core'; import type { ReadableSpan, SpanExporter } from '@opentelemetry/sdk-trace-base'; import { SemanticAttributes } from '@opentelemetry/semantic-conventions'; -import { flush } from '@sentry/core'; -import type { DynamicSamplingContext, Span as SentrySpan, SpanOrigin, TransactionSource } from '@sentry/types'; +import { flush, getCurrentScope } from '@sentry/core'; +import type { DynamicSamplingContext, Scope, Span as SentrySpan, SpanOrigin, TransactionSource } from '@sentry/types'; import { logger } from '@sentry/utils'; import { getCurrentHub } from './custom/hub'; @@ -110,7 +110,7 @@ function maybeSend(spans: ReadableSpan[]): ReadableSpan[] { // Now finish the transaction, which will send it together with all the spans // We make sure to use the finish scope - const scope = getSpanFinishScope(span); + const scope = getScopeForTransactionFinish(span); transaction.finishWithScope(convertOtelTimeToSeconds(span.endTime), scope); }); @@ -119,6 +119,17 @@ function maybeSend(spans: ReadableSpan[]): ReadableSpan[] { .filter((span): span is ReadableSpan => !!span); } +function getScopeForTransactionFinish(span: ReadableSpan): Scope { + // The finish scope should normally always be there (and it is already a clone), + // but for the sake of type safety we fall back to a clone of the current scope + const scope = getSpanFinishScope(span) || getCurrentScope().clone(); + scope.setContext('otel', { + attributes: removeSentryAttributes(span.attributes), + resource: span.resource.attributes, + }); + return scope; +} + function getCompletedRootNodes(nodes: SpanNode[]): SpanNodeCompleted[] { return nodes.filter((node): node is SpanNodeCompleted => !!node.span && !node.parentNode); } @@ -176,11 +187,6 @@ function createTransactionForOtelSpan(span: ReadableSpan): OpenTelemetryTransact sampled: true, }) as OpenTelemetryTransaction; - transaction.setContext('otel', { - attributes: removeSentryAttributes(span.attributes), - resource: span.resource.attributes, - }); - return transaction; } diff --git a/packages/sveltekit/src/client/router.ts b/packages/sveltekit/src/client/router.ts index f6601eb940b2..2b36d4adb4f2 100644 --- a/packages/sveltekit/src/client/router.ts +++ b/packages/sveltekit/src/client/router.ts @@ -124,6 +124,7 @@ function instrumentNavigations(startTransactionFn: (context: TransactionContext) description: 'SvelteKit Route Change', origin: 'auto.ui.sveltekit', }); + // eslint-disable-next-line deprecation/deprecation activeTransaction.setTag('from', parameterizedRouteOrigin); } }); diff --git a/packages/sveltekit/test/client/router.test.ts b/packages/sveltekit/test/client/router.test.ts index 8c4c9187a7ff..29037c28461f 100644 --- a/packages/sveltekit/test/client/router.test.ts +++ b/packages/sveltekit/test/client/router.test.ts @@ -123,6 +123,7 @@ describe('sveltekitRoutingInstrumentation', () => { description: 'SvelteKit Route Change', }); + // eslint-disable-next-line deprecation/deprecation expect(returnedTransaction?.setTag).toHaveBeenCalledWith('from', '/users'); // We emit `null` here to simulate the end of the navigation lifecycle @@ -173,6 +174,7 @@ describe('sveltekitRoutingInstrumentation', () => { description: 'SvelteKit Route Change', }); + // eslint-disable-next-line deprecation/deprecation expect(returnedTransaction?.setTag).toHaveBeenCalledWith('from', '/users/[id]'); }); diff --git a/packages/tracing-internal/src/browser/backgroundtab.ts b/packages/tracing-internal/src/browser/backgroundtab.ts index 67385b665be4..849f20ad20de 100644 --- a/packages/tracing-internal/src/browser/backgroundtab.ts +++ b/packages/tracing-internal/src/browser/backgroundtab.ts @@ -26,6 +26,8 @@ export function registerBackgroundTabDetection(): void { if (!activeTransaction.status) { activeTransaction.setStatus(statusType); } + // TODO: Can we rewrite this to an attribute? + // eslint-disable-next-line deprecation/deprecation activeTransaction.setTag('visibilitychange', 'document.hidden'); activeTransaction.end(); } diff --git a/packages/tracing-internal/src/browser/browsertracing.ts b/packages/tracing-internal/src/browser/browsertracing.ts index 1c8723a9705b..083e76b08ecf 100644 --- a/packages/tracing-internal/src/browser/browsertracing.ts +++ b/packages/tracing-internal/src/browser/browsertracing.ts @@ -343,6 +343,7 @@ export class BrowserTracing implements Integration { this._latestRouteName = finalContext.name; + // eslint-disable-next-line deprecation/deprecation const sourceFromData = context.data && context.data[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]; // eslint-disable-next-line deprecation/deprecation const sourceFromMetadata = finalContext.metadata && finalContext.metadata.source; diff --git a/packages/tracing-internal/src/browser/metrics/index.ts b/packages/tracing-internal/src/browser/metrics/index.ts index 72cce4f7aad6..df7cde3e1703 100644 --- a/packages/tracing-internal/src/browser/metrics/index.ts +++ b/packages/tracing-internal/src/browser/metrics/index.ts @@ -115,7 +115,7 @@ export function startTrackingInteractions(): void { const componentName = getComponentName(entry.target); if (componentName) { - span.data = { 'ui.component_name': componentName }; + span.attributes = { 'ui.component_name': componentName }; } // eslint-disable-next-line deprecation/deprecation @@ -448,10 +448,14 @@ function _trackNavigator(transaction: Transaction): void { const connection = navigator.connection; if (connection) { if (connection.effectiveType) { + // TODO: Can we rewrite this to an attribute? + // eslint-disable-next-line deprecation/deprecation transaction.setTag('effectiveConnectionType', connection.effectiveType); } if (connection.type) { + // TODO: Can we rewrite this to an attribute? + // eslint-disable-next-line deprecation/deprecation transaction.setTag('connectionType', connection.type); } @@ -461,10 +465,14 @@ function _trackNavigator(transaction: Transaction): void { } if (isMeasurementValue(navigator.deviceMemory)) { + // TODO: Can we rewrite this to an attribute? + // eslint-disable-next-line deprecation/deprecation transaction.setTag('deviceMemory', `${navigator.deviceMemory} GB`); } if (isMeasurementValue(navigator.hardwareConcurrency)) { + // TODO: Can we rewrite this to an attribute? + // eslint-disable-next-line deprecation/deprecation transaction.setTag('hardwareConcurrency', String(navigator.hardwareConcurrency)); } } @@ -477,18 +485,26 @@ function _tagMetricInfo(transaction: Transaction): void { // Capture Properties of the LCP element that contributes to the LCP. if (_lcpEntry.element) { + // TODO: Can we rewrite this to an attribute? + // eslint-disable-next-line deprecation/deprecation transaction.setTag('lcp.element', htmlTreeAsString(_lcpEntry.element)); } if (_lcpEntry.id) { + // TODO: Can we rewrite this to an attribute? + // eslint-disable-next-line deprecation/deprecation transaction.setTag('lcp.id', _lcpEntry.id); } if (_lcpEntry.url) { // Trim URL to the first 200 characters. + // TODO: Can we rewrite this to an attribute? + // eslint-disable-next-line deprecation/deprecation transaction.setTag('lcp.url', _lcpEntry.url.trim().slice(0, 200)); } + // TODO: Can we rewrite this to an attribute? + // eslint-disable-next-line deprecation/deprecation transaction.setTag('lcp.size', _lcpEntry.size); } @@ -496,6 +512,8 @@ function _tagMetricInfo(transaction: Transaction): void { if (_clsEntry && _clsEntry.sources) { DEBUG_BUILD && logger.log('[Measurements] Adding CLS Data'); _clsEntry.sources.forEach((source, index) => + // TODO: Can we rewrite this to an attribute? + // eslint-disable-next-line deprecation/deprecation transaction.setTag(`cls.source.${index + 1}`, htmlTreeAsString(source.node)), ); } diff --git a/packages/tracing-internal/src/browser/request.ts b/packages/tracing-internal/src/browser/request.ts index d7c4c7892189..9fb5a5cc3439 100644 --- a/packages/tracing-internal/src/browser/request.ts +++ b/packages/tracing-internal/src/browser/request.ts @@ -5,6 +5,7 @@ import { getCurrentScope, getDynamicSamplingContextFromClient, hasTracingEnabled, + spanToJSON, spanToTraceHeader, } from '@sentry/core'; import type { HandlerDataXhr, SentryWrappedXMLHttpRequest, Span } from '@sentry/types'; @@ -146,9 +147,9 @@ function isPerformanceResourceTiming(entry: PerformanceEntry): entry is Performa * @param span A span that has yet to be finished, must contain `url` on data. */ function addHTTPTimings(span: Span): void { - const url = span.data.url; + const { url } = spanToJSON(span).data || {}; - if (!url) { + if (!url || typeof url !== 'string') { return; } @@ -156,7 +157,7 @@ function addHTTPTimings(span: Span): void { entries.forEach(entry => { if (isPerformanceResourceTiming(entry) && entry.name.endsWith(url)) { const spanData = resourceTimingEntryToSpanData(entry); - spanData.forEach(data => span.setData(...data)); + spanData.forEach(data => span.setAttribute(...data)); // In the next tick, clean this handler up // We have to wait here because otherwise this cleans itself up before it is fully done setTimeout(cleanup); diff --git a/packages/tracing-internal/src/common/fetch.ts b/packages/tracing-internal/src/common/fetch.ts index cc04885e4436..d6cf469cadc8 100644 --- a/packages/tracing-internal/src/common/fetch.ts +++ b/packages/tracing-internal/src/common/fetch.ts @@ -58,7 +58,7 @@ export function instrumentFetchRequest( if (contentLength) { const contentLengthNum = parseInt(contentLength); if (contentLengthNum > 0) { - span.setData('http.response_content_length', contentLengthNum); + span.setAttribute('http.response_content_length', contentLengthNum); } } } else if (handlerData.error) { diff --git a/packages/tracing-internal/src/node/integrations/mysql.ts b/packages/tracing-internal/src/node/integrations/mysql.ts index 16753c4a9e08..9756b200e06b 100644 --- a/packages/tracing-internal/src/node/integrations/mysql.ts +++ b/packages/tracing-internal/src/node/integrations/mysql.ts @@ -73,7 +73,7 @@ export class Mysql implements LazyLoadedIntegration { DEBUG_BUILD && logger.error('Mysql Integration was unable to instrument `mysql` config.'); } - function spanDataFromConfig(): Record { + function spanDataFromConfig(): Record { if (!mySqlConfig) { return {}; } @@ -91,7 +91,7 @@ export class Mysql implements LazyLoadedIntegration { const data = spanDataFromConfig(); Object.keys(data).forEach(key => { - span.setData(key, data[key]); + span.setAttribute(key, data[key]); }); span.end(); diff --git a/packages/tracing-internal/test/browser/backgroundtab.test.ts b/packages/tracing-internal/test/browser/backgroundtab.test.ts index dcb423fd094a..215a9c5d6583 100644 --- a/packages/tracing-internal/test/browser/backgroundtab.test.ts +++ b/packages/tracing-internal/test/browser/backgroundtab.test.ts @@ -55,6 +55,7 @@ conditionalTest({ min: 10 })('registerBackgroundTabDetection', () => { events.visibilitychange(); expect(span?.status).toBe('cancelled'); + // eslint-disable-next-line deprecation/deprecation expect(span?.tags.visibilitychange).toBe('document.hidden'); expect(span?.endTimestamp).toBeDefined(); }); diff --git a/packages/tracing-internal/test/browser/request.test.ts b/packages/tracing-internal/test/browser/request.test.ts index 0f3ce191278a..3dabe104c8f6 100644 --- a/packages/tracing-internal/test/browser/request.test.ts +++ b/packages/tracing-internal/test/browser/request.test.ts @@ -256,7 +256,7 @@ describe('callbacks', () => { expect(finishedSpan).toBeDefined(); expect(finishedSpan).toBeInstanceOf(Span); - expect(finishedSpan.data).toEqual({ + expect(sentryCore.spanToJSON(finishedSpan).data).toEqual({ 'http.response_content_length': 123, 'http.method': 'GET', 'http.response.status_code': 404, diff --git a/packages/types/src/span.ts b/packages/types/src/span.ts index 1538fc343e52..3f54322886bb 100644 --- a/packages/types/src/span.ts +++ b/packages/types/src/span.ts @@ -129,11 +129,13 @@ export interface SpanContext { /** * Tags of the Span. + * @deprecated Pass `attributes` instead. */ tags?: { [key: string]: Primitive }; /** * Data of the Span. + * @deprecated Pass `attributes` instead. */ data?: { [key: string]: any }; @@ -195,17 +197,20 @@ export interface Span extends SpanContext { startTimestamp: number; /** - * @inheritDoc + * Tags for the span. + * @deprecated Use `getSpanAttributes(span)` instead. */ tags: { [key: string]: Primitive }; /** - * @inheritDoc + * Data for the span. + * @deprecated Use `getSpanAttributes(span)` instead. */ data: { [key: string]: any }; /** - * @inheritDoc + * Attributes for the span. + * @deprecated Use `getSpanAttributes(span)` instead. */ attributes: SpanAttributes; @@ -243,6 +248,7 @@ export interface Span extends SpanContext { * * @param key Tag key * @param value Tag value + * @deprecated Use `setAttribute()` instead. */ setTag(key: string, value: Primitive): this; @@ -250,6 +256,7 @@ export interface Span extends SpanContext { * Sets the data attribute on the current span * @param key Data key * @param value Data value + * @deprecated Use `setAttribute()` instead. */ setData(key: string, value: any): this; diff --git a/packages/types/src/transaction.ts b/packages/types/src/transaction.ts index d07c5ce435c1..586523625710 100644 --- a/packages/types/src/transaction.ts +++ b/packages/types/src/transaction.ts @@ -74,17 +74,20 @@ export interface Transaction extends TransactionContext, Omit Date: Wed, 10 Jan 2024 11:26:18 +0100 Subject: [PATCH 39/43] feat(core): Deprecate `new Transaction()` (#10125) No more transaction class! This should not really affect users, as they should only use `startTransaction()` anyhow. --- packages/core/src/tracing/hubextensions.ts | 2 ++ packages/core/src/tracing/idletransaction.ts | 3 +++ packages/core/src/tracing/transaction.ts | 2 ++ packages/node/test/handlers.test.ts | 3 +++ packages/opentelemetry-node/test/propagator.test.ts | 1 + packages/opentelemetry/src/custom/transaction.ts | 1 + packages/opentelemetry/test/custom/transaction.test.ts | 3 +++ packages/tracing-internal/test/browser/metrics/index.test.ts | 2 ++ packages/tracing-internal/test/browser/metrics/utils.test.ts | 3 +++ packages/tracing/test/idletransaction.test.ts | 2 ++ 10 files changed, 22 insertions(+) diff --git a/packages/core/src/tracing/hubextensions.ts b/packages/core/src/tracing/hubextensions.ts index c33f80db150d..c63e5710b6f2 100644 --- a/packages/core/src/tracing/hubextensions.ts +++ b/packages/core/src/tracing/hubextensions.ts @@ -60,6 +60,7 @@ The transaction will not be sampled. Please use the ${configInstrumenter} instru transactionContext.sampled = false; } + // eslint-disable-next-line deprecation/deprecation let transaction = new Transaction(transactionContext, this); transaction = sampleTransaction(transaction, options, { parentSampled: transactionContext.parentSampled, @@ -90,6 +91,7 @@ export function startIdleTransaction( const client = hub.getClient(); const options: Partial = (client && client.getOptions()) || {}; + // eslint-disable-next-line deprecation/deprecation let transaction = new IdleTransaction(transactionContext, hub, idleTimeout, finalTimeout, heartbeatInterval, onScope); transaction = sampleTransaction(transaction, options, { parentSampled: transactionContext.parentSampled, diff --git a/packages/core/src/tracing/idletransaction.ts b/packages/core/src/tracing/idletransaction.ts index 2735146dbbc4..dfab5782e914 100644 --- a/packages/core/src/tracing/idletransaction.ts +++ b/packages/core/src/tracing/idletransaction.ts @@ -95,6 +95,9 @@ export class IdleTransaction extends Transaction { private _finishReason: (typeof IDLE_TRANSACTION_FINISH_REASONS)[number]; + /** + * @deprecated Transactions will be removed in v8. Use spans instead. + */ public constructor( transactionContext: TransactionContext, private readonly _idleHub: Hub, diff --git a/packages/core/src/tracing/transaction.ts b/packages/core/src/tracing/transaction.ts index c68d9e6fbeeb..76166003fb34 100644 --- a/packages/core/src/tracing/transaction.ts +++ b/packages/core/src/tracing/transaction.ts @@ -45,6 +45,8 @@ export class Transaction extends SpanClass implements TransactionInterface { * @internal * @hideconstructor * @hidden + * + * @deprecated Transactions will be removed in v8. Use spans instead. */ public constructor(transactionContext: TransactionContext, hub?: Hub) { super(transactionContext); diff --git a/packages/node/test/handlers.test.ts b/packages/node/test/handlers.test.ts index 888991de439c..46683206e6fe 100644 --- a/packages/node/test/handlers.test.ts +++ b/packages/node/test/handlers.test.ts @@ -361,6 +361,7 @@ describe('tracingHandler', () => { }); it('pulls status code from the response', done => { + // eslint-disable-next-line deprecation/deprecation const transaction = new Transaction({ name: 'mockTransaction' }); jest.spyOn(sentryCore, 'startTransaction').mockReturnValue(transaction as Transaction); const finishTransaction = jest.spyOn(transaction, 'end'); @@ -412,6 +413,7 @@ describe('tracingHandler', () => { }); it('closes the transaction when request processing is done', done => { + // eslint-disable-next-line deprecation/deprecation const transaction = new Transaction({ name: 'mockTransaction' }); jest.spyOn(sentryCore, 'startTransaction').mockReturnValue(transaction as Transaction); const finishTransaction = jest.spyOn(transaction, 'end'); @@ -426,6 +428,7 @@ describe('tracingHandler', () => { }); it('waits to finish transaction until all spans are finished, even though `transaction.end()` is registered on `res.finish` event first', done => { + // eslint-disable-next-line deprecation/deprecation const transaction = new Transaction({ name: 'mockTransaction', sampled: true }); transaction.initSpanRecorder(); // eslint-disable-next-line deprecation/deprecation diff --git a/packages/opentelemetry-node/test/propagator.test.ts b/packages/opentelemetry-node/test/propagator.test.ts index 8e24d7564992..22c686320e76 100644 --- a/packages/opentelemetry-node/test/propagator.test.ts +++ b/packages/opentelemetry-node/test/propagator.test.ts @@ -60,6 +60,7 @@ describe('SentryPropagator', () => { } function createTransactionAndMaybeSpan(type: PerfType, transactionContext: TransactionContext) { + // eslint-disable-next-line deprecation/deprecation const transaction = new Transaction(transactionContext, hub); setSentrySpan(transaction.spanContext().spanId, transaction); if (type === PerfType.Span) { diff --git a/packages/opentelemetry/src/custom/transaction.ts b/packages/opentelemetry/src/custom/transaction.ts index b1f84d01aec7..161901c5348e 100644 --- a/packages/opentelemetry/src/custom/transaction.ts +++ b/packages/opentelemetry/src/custom/transaction.ts @@ -11,6 +11,7 @@ export function startTransaction(hub: HubInterface, transactionContext: Transact const client = hub.getClient(); const options: Partial = (client && client.getOptions()) || {}; + // eslint-disable-next-line deprecation/deprecation const transaction = new OpenTelemetryTransaction(transactionContext, hub as Hub); // Since we do not do sampling here, we assume that this is _always_ sampled // Any sampling decision happens in OpenTelemetry's sampler diff --git a/packages/opentelemetry/test/custom/transaction.test.ts b/packages/opentelemetry/test/custom/transaction.test.ts index 108e85f598a8..5b1ccc5b8044 100644 --- a/packages/opentelemetry/test/custom/transaction.test.ts +++ b/packages/opentelemetry/test/custom/transaction.test.ts @@ -17,6 +17,7 @@ describe('NodeExperimentalTransaction', () => { const hub = getCurrentHub(); hub.bindClient(client); + // eslint-disable-next-line deprecation/deprecation const transaction = new OpenTelemetryTransaction({ name: 'test', sampled: true }, hub); const res = transaction.finishWithScope(); @@ -63,6 +64,7 @@ describe('NodeExperimentalTransaction', () => { const hub = getCurrentHub(); hub.bindClient(client); + // eslint-disable-next-line deprecation/deprecation const transaction = new OpenTelemetryTransaction({ name: 'test', startTimestamp: 123456, sampled: true }, hub); const res = transaction.finishWithScope(1234567); @@ -87,6 +89,7 @@ describe('NodeExperimentalTransaction', () => { const hub = getCurrentHub(); hub.bindClient(client); + // eslint-disable-next-line deprecation/deprecation const transaction = new OpenTelemetryTransaction({ name: 'test', startTimestamp: 123456, sampled: true }, hub); const scope = new OpenTelemetryScope(); diff --git a/packages/tracing-internal/test/browser/metrics/index.test.ts b/packages/tracing-internal/test/browser/metrics/index.test.ts index cfb0500fb68a..f24b6ce4b45a 100644 --- a/packages/tracing-internal/test/browser/metrics/index.test.ts +++ b/packages/tracing-internal/test/browser/metrics/index.test.ts @@ -3,6 +3,7 @@ import type { ResourceEntry } from '../../../src/browser/metrics'; import { _addMeasureSpans, _addResourceSpans } from '../../../src/browser/metrics'; describe('_addMeasureSpans', () => { + // eslint-disable-next-line deprecation/deprecation const transaction = new Transaction({ op: 'pageload', name: '/' }); beforeEach(() => { // eslint-disable-next-line deprecation/deprecation @@ -39,6 +40,7 @@ describe('_addMeasureSpans', () => { }); describe('_addResourceSpans', () => { + // eslint-disable-next-line deprecation/deprecation const transaction = new Transaction({ op: 'pageload', name: '/' }); beforeEach(() => { // eslint-disable-next-line deprecation/deprecation diff --git a/packages/tracing-internal/test/browser/metrics/utils.test.ts b/packages/tracing-internal/test/browser/metrics/utils.test.ts index 120eb6cf8076..ae614abc41a6 100644 --- a/packages/tracing-internal/test/browser/metrics/utils.test.ts +++ b/packages/tracing-internal/test/browser/metrics/utils.test.ts @@ -4,6 +4,7 @@ import { _startChild } from '../../../src/browser/metrics/utils'; describe('_startChild()', () => { it('creates a span with given properties', () => { + // eslint-disable-next-line deprecation/deprecation const transaction = new Transaction({ name: 'test' }); const span = _startChild(transaction, { description: 'evaluation', @@ -16,6 +17,7 @@ describe('_startChild()', () => { }); it('adjusts the start timestamp if child span starts before transaction', () => { + // eslint-disable-next-line deprecation/deprecation const transaction = new Transaction({ name: 'test', startTimestamp: 123 }); const span = _startChild(transaction, { description: 'script.js', @@ -28,6 +30,7 @@ describe('_startChild()', () => { }); it('does not adjust start timestamp if child span starts after transaction', () => { + // eslint-disable-next-line deprecation/deprecation const transaction = new Transaction({ name: 'test', startTimestamp: 123 }); const span = _startChild(transaction, { description: 'script.js', diff --git a/packages/tracing/test/idletransaction.test.ts b/packages/tracing/test/idletransaction.test.ts index 17a4400542a6..f5f7e92595c5 100644 --- a/packages/tracing/test/idletransaction.test.ts +++ b/packages/tracing/test/idletransaction.test.ts @@ -1,3 +1,4 @@ +/* eslint-disable deprecation/deprecation */ import { BrowserClient } from '@sentry/browser'; import { TRACING_DEFAULTS, @@ -96,6 +97,7 @@ describe('IdleTransaction', () => { transaction.initSpanRecorder(10); // @ts-expect-error need to pass in hub + // eslint-disable-next-line deprecation/deprecation const otherTransaction = new Transaction({ name: 'bar' }, hub); // eslint-disable-next-line deprecation/deprecation hub.getScope().setSpan(otherTransaction); From 6faec4238c248e4295d0e827c355f834ef041966 Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Wed, 10 Jan 2024 11:55:36 +0100 Subject: [PATCH 40/43] feat(core): Deprecate `Transaction.getDynamicSamplingContext` in favour of `getDynamicSamplingContextFromSpan` (#10094) Deprecate `Transaction.getDynamicSamplingContext` and introduce its direct replacement, a top-level utility function. Note that this only is an intermediate step we should take to rework how we generate and handle the DSC creation. More details in #10095 --- MIGRATION.md | 1 + .../rollup-utils/plugins/bundlePlugins.mjs | 3 + packages/astro/src/server/meta.ts | 8 +- packages/astro/test/server/meta.test.ts | 4 + packages/core/src/server-runtime-client.ts | 8 +- .../src/tracing/dynamicSamplingContext.ts | 65 ++++++++- packages/core/src/tracing/index.ts | 2 +- packages/core/src/tracing/transaction.ts | 43 +----- .../core/src/utils/applyScopeDataToEvent.ts | 3 +- .../tracing/dynamicSamplingContext.test.ts | 124 ++++++++++++++++++ .../wrapAppGetInitialPropsWithSentry.ts | 10 +- .../wrapErrorGetInitialPropsWithSentry.ts | 10 +- .../common/wrapGetInitialPropsWithSentry.ts | 10 +- .../wrapGetServerSidePropsWithSentry.ts | 10 +- packages/node/src/integrations/hapi/index.ts | 3 +- packages/node/src/integrations/http.ts | 3 +- .../node/src/integrations/undici/index.ts | 3 +- packages/opentelemetry-node/src/propagator.ts | 4 +- packages/remix/src/utils/instrumentServer.ts | 3 +- packages/sveltekit/src/server/handle.ts | 4 +- .../tracing-internal/src/browser/request.ts | 3 +- packages/tracing-internal/src/common/fetch.ts | 3 +- packages/tracing/test/span.test.ts | 18 +-- packages/types/src/transaction.ts | 6 +- 24 files changed, 276 insertions(+), 75 deletions(-) create mode 100644 packages/core/test/lib/tracing/dynamicSamplingContext.test.ts diff --git a/MIGRATION.md b/MIGRATION.md index 57fd79bcb18e..7d8645fd4e16 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -98,6 +98,7 @@ In v8, the Span class is heavily reworked. The following properties & methods ar * `span.traceId`: Use `span.spanContext().traceId` instead. * `span.name`: Use `spanToJSON(span).description` instead. * `span.description`: Use `spanToJSON(span).description` instead. +* `span.getDynamicSamplingContext`: Use `getDynamicSamplingContextFromSpan` utility function instead. * `transaction.setMetadata()`: Use attributes instead, or set data on the scope. * `transaction.metadata`: Use attributes instead, or set data on the scope. * `span.tags`: Set tags on the surrounding scope instead, or use attributes. diff --git a/dev-packages/rollup-utils/plugins/bundlePlugins.mjs b/dev-packages/rollup-utils/plugins/bundlePlugins.mjs index 66f8e8c78228..9aef97dc828f 100644 --- a/dev-packages/rollup-utils/plugins/bundlePlugins.mjs +++ b/dev-packages/rollup-utils/plugins/bundlePlugins.mjs @@ -135,6 +135,9 @@ export function makeTerserPlugin() { // These are used by instrument.ts in utils for identifying HTML elements & events '_sentryCaptured', '_sentryId', + // For v7 backwards-compatibility we need to access txn._frozenDynamicSamplingContext + // TODO (v8): Remove this reserved word + '_frozenDynamicSamplingContext', ], }, }, diff --git a/packages/astro/src/server/meta.ts b/packages/astro/src/server/meta.ts index bdc0d89d4f80..1a908acc9ceb 100644 --- a/packages/astro/src/server/meta.ts +++ b/packages/astro/src/server/meta.ts @@ -1,4 +1,8 @@ -import { getDynamicSamplingContextFromClient, spanToTraceHeader } from '@sentry/core'; +import { + getDynamicSamplingContextFromClient, + getDynamicSamplingContextFromSpan, + spanToTraceHeader, +} from '@sentry/core'; import type { Client, Scope, Span } from '@sentry/types'; import { TRACEPARENT_REGEXP, @@ -33,7 +37,7 @@ export function getTracingMetaTags( const sentryTrace = span ? spanToTraceHeader(span) : generateSentryTraceHeader(traceId, undefined, sampled); const dynamicSamplingContext = transaction - ? transaction.getDynamicSamplingContext() + ? getDynamicSamplingContextFromSpan(transaction) : dsc ? dsc : client diff --git a/packages/astro/test/server/meta.test.ts b/packages/astro/test/server/meta.test.ts index 1aa9e9405be5..f235ad34d7ca 100644 --- a/packages/astro/test/server/meta.test.ts +++ b/packages/astro/test/server/meta.test.ts @@ -32,6 +32,10 @@ const mockedScope = { describe('getTracingMetaTags', () => { it('returns the tracing tags from the span, if it is provided', () => { { + vi.spyOn(SentryCore, 'getDynamicSamplingContextFromSpan').mockReturnValueOnce({ + environment: 'production', + }); + const tags = getTracingMetaTags(mockedSpan, mockedScope, mockedClient); expect(tags).toEqual({ diff --git a/packages/core/src/server-runtime-client.ts b/packages/core/src/server-runtime-client.ts index b21e162c5b1c..00f23be2dc0a 100644 --- a/packages/core/src/server-runtime-client.ts +++ b/packages/core/src/server-runtime-client.ts @@ -21,7 +21,11 @@ import { getClient } from './exports'; import { MetricsAggregator } from './metrics/aggregator'; import type { Scope } from './scope'; import { SessionFlusher } from './sessionflusher'; -import { addTracingExtensions, getDynamicSamplingContextFromClient } from './tracing'; +import { + addTracingExtensions, + getDynamicSamplingContextFromClient, + getDynamicSamplingContextFromSpan, +} from './tracing'; import { spanToTraceContext } from './utils/spanUtils'; export interface ServerRuntimeClientOptions extends ClientOptions { @@ -258,7 +262,7 @@ export class ServerRuntimeClient< // eslint-disable-next-line deprecation/deprecation const span = scope.getSpan(); if (span) { - const samplingContext = span.transaction ? span.transaction.getDynamicSamplingContext() : undefined; + const samplingContext = span.transaction ? getDynamicSamplingContextFromSpan(span) : undefined; return [samplingContext, spanToTraceContext(span)]; } diff --git a/packages/core/src/tracing/dynamicSamplingContext.ts b/packages/core/src/tracing/dynamicSamplingContext.ts index f8e8cd107c87..318c232eb43b 100644 --- a/packages/core/src/tracing/dynamicSamplingContext.ts +++ b/packages/core/src/tracing/dynamicSamplingContext.ts @@ -1,12 +1,14 @@ -import type { Client, DynamicSamplingContext, Scope } from '@sentry/types'; +import type { Client, DynamicSamplingContext, Scope, Span, Transaction } from '@sentry/types'; import { dropUndefinedKeys } from '@sentry/utils'; import { DEFAULT_ENVIRONMENT } from '../constants'; +import { getClient, getCurrentScope } from '../exports'; +import { spanIsSampled, spanToJSON } from '../utils/spanUtils'; /** * Creates a dynamic sampling context from a client. * - * Dispatchs the `createDsc` lifecycle hook as a side effect. + * Dispatches the `createDsc` lifecycle hook as a side effect. */ export function getDynamicSamplingContextFromClient( trace_id: string, @@ -30,3 +32,62 @@ export function getDynamicSamplingContextFromClient( return dsc; } + +/** + * A Span with a frozen dynamic sampling context. + */ +type TransactionWithV7FrozenDsc = Transaction & { _frozenDynamicSamplingContext?: DynamicSamplingContext }; + +/** + * Creates a dynamic sampling context from a span (and client and scope) + * + * @param span the span from which a few values like the root span name and sample rate are extracted. + * + * @returns a dynamic sampling context + */ +export function getDynamicSamplingContextFromSpan(span: Span): Readonly> { + const client = getClient(); + if (!client) { + return {}; + } + + // passing emit=false here to only emit later once the DSC is actually populated + const dsc = getDynamicSamplingContextFromClient(spanToJSON(span).trace_id || '', client, getCurrentScope()); + + // As long as we use `Transaction`s internally, this should be fine. + // TODO: We need to replace this with a `getRootSpan(span)` function though + const txn = span.transaction as TransactionWithV7FrozenDsc | undefined; + if (!txn) { + return dsc; + } + + // TODO (v8): Remove v7FrozenDsc as a Transaction will no longer have _frozenDynamicSamplingContext + // For now we need to avoid breaking users who directly created a txn with a DSC, where this field is still set. + // @see Transaction class constructor + const v7FrozenDsc = txn && txn._frozenDynamicSamplingContext; + if (v7FrozenDsc) { + return v7FrozenDsc; + } + + // TODO (v8): Replace txn.metadata with txn.attributes[] + // We can't do this yet because attributes aren't always set yet. + // eslint-disable-next-line deprecation/deprecation + const { sampleRate: maybeSampleRate, source } = txn.metadata; + if (maybeSampleRate != null) { + dsc.sample_rate = `${maybeSampleRate}`; + } + + // We don't want to have a transaction name in the DSC if the source is "url" because URLs might contain PII + const jsonSpan = spanToJSON(txn); + + // after JSON conversion, txn.name becomes jsonSpan.description + if (source && source !== 'url') { + dsc.transaction = jsonSpan.description; + } + + dsc.sampled = String(spanIsSampled(txn)); + + client.emit && client.emit('createDsc', dsc); + + return dsc; +} diff --git a/packages/core/src/tracing/index.ts b/packages/core/src/tracing/index.ts index 759a42cbdfe0..ecdc5f595095 100644 --- a/packages/core/src/tracing/index.ts +++ b/packages/core/src/tracing/index.ts @@ -19,5 +19,5 @@ export { startSpanManual, continueTrace, } from './trace'; -export { getDynamicSamplingContextFromClient } from './dynamicSamplingContext'; +export { getDynamicSamplingContextFromClient, getDynamicSamplingContextFromSpan } from './dynamicSamplingContext'; export { setMeasurement } from './measurement'; diff --git a/packages/core/src/tracing/transaction.ts b/packages/core/src/tracing/transaction.ts index 76166003fb34..6e9e3e62f9d9 100644 --- a/packages/core/src/tracing/transaction.ts +++ b/packages/core/src/tracing/transaction.ts @@ -17,7 +17,7 @@ import type { Hub } from '../hub'; import { getCurrentHub } from '../hub'; import { SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE, SEMANTIC_ATTRIBUTE_SENTRY_SOURCE } from '../semanticAttributes'; import { spanTimeInputToSeconds, spanToTraceContext } from '../utils/spanUtils'; -import { getDynamicSamplingContextFromClient } from './dynamicSamplingContext'; +import { getDynamicSamplingContextFromSpan } from './dynamicSamplingContext'; import { Span as SpanClass, SpanRecorder } from './span'; /** JSDoc */ @@ -35,6 +35,7 @@ export class Transaction extends SpanClass implements TransactionInterface { private _trimEnd?: boolean; + // DO NOT yet remove this property, it is used in a hack for v7 backwards compatibility. private _frozenDynamicSamplingContext: Readonly> | undefined; private _metadata: Partial; @@ -230,43 +231,11 @@ export class Transaction extends SpanClass implements TransactionInterface { * @inheritdoc * * @experimental + * + * @deprecated Use top-level `getDynamicSamplingContextFromSpan` instead. */ public getDynamicSamplingContext(): Readonly> { - if (this._frozenDynamicSamplingContext) { - return this._frozenDynamicSamplingContext; - } - - const hub = this._hub || getCurrentHub(); - const client = hub.getClient(); - - if (!client) return {}; - - const { _traceId: traceId, _sampled: sampled } = this; - - const scope = hub.getScope(); - const dsc = getDynamicSamplingContextFromClient(traceId, client, scope); - - // eslint-disable-next-line deprecation/deprecation - const maybeSampleRate = this.metadata.sampleRate; - if (maybeSampleRate !== undefined) { - dsc.sample_rate = `${maybeSampleRate}`; - } - - // We don't want to have a transaction name in the DSC if the source is "url" because URLs might contain PII - // eslint-disable-next-line deprecation/deprecation - const source = this.metadata.source; - if (source && source !== 'url') { - dsc.transaction = this._name; - } - - if (sampled !== undefined) { - dsc.sampled = String(sampled); - } - - // Uncomment if we want to make DSC immutable - // this._frozenDynamicSamplingContext = dsc; - - return dsc; + return getDynamicSamplingContextFromSpan(this); } /** @@ -344,7 +313,7 @@ export class Transaction extends SpanClass implements TransactionInterface { type: 'transaction', sdkProcessingMetadata: { ...metadata, - dynamicSamplingContext: this.getDynamicSamplingContext(), + dynamicSamplingContext: getDynamicSamplingContextFromSpan(this), }, ...(source && { transaction_info: { diff --git a/packages/core/src/utils/applyScopeDataToEvent.ts b/packages/core/src/utils/applyScopeDataToEvent.ts index ef9796680cb9..f834776ac9ba 100644 --- a/packages/core/src/utils/applyScopeDataToEvent.ts +++ b/packages/core/src/utils/applyScopeDataToEvent.ts @@ -1,5 +1,6 @@ import type { Breadcrumb, Event, PropagationContext, ScopeData, Span } from '@sentry/types'; import { arrayify } from '@sentry/utils'; +import { getDynamicSamplingContextFromSpan } from '../tracing/dynamicSamplingContext'; import { spanToJSON, spanToTraceContext } from './spanUtils'; /** @@ -176,7 +177,7 @@ function applySpanToEvent(event: Event, span: Span): void { const transaction = span.transaction; if (transaction) { event.sdkProcessingMetadata = { - dynamicSamplingContext: transaction.getDynamicSamplingContext(), + dynamicSamplingContext: getDynamicSamplingContextFromSpan(span), ...event.sdkProcessingMetadata, }; const transactionName = spanToJSON(transaction).description; diff --git a/packages/core/test/lib/tracing/dynamicSamplingContext.test.ts b/packages/core/test/lib/tracing/dynamicSamplingContext.test.ts new file mode 100644 index 000000000000..06a061612eda --- /dev/null +++ b/packages/core/test/lib/tracing/dynamicSamplingContext.test.ts @@ -0,0 +1,124 @@ +import type { TransactionSource } from '@sentry/types'; +import { Hub, SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE, SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, makeMain } from '../../../src'; +import { Transaction, getDynamicSamplingContextFromSpan, startInactiveSpan } from '../../../src/tracing'; +import { addTracingExtensions } from '../../../src/tracing'; +import { TestClient, getDefaultTestClientOptions } from '../../mocks/client'; + +describe('getDynamicSamplingContextFromSpan', () => { + let hub: Hub; + beforeEach(() => { + const options = getDefaultTestClientOptions({ tracesSampleRate: 1.0, release: '1.0.1' }); + const client = new TestClient(options); + hub = new Hub(client); + hub.bindClient(client); + makeMain(hub); + addTracingExtensions(); + }); + + afterEach(() => { + jest.resetAllMocks(); + }); + + test('returns the DSC provided during transaction creation', () => { + const transaction = new Transaction({ + name: 'tx', + metadata: { dynamicSamplingContext: { environment: 'myEnv' } }, + }); + + const dynamicSamplingContext = getDynamicSamplingContextFromSpan(transaction); + + expect(dynamicSamplingContext).toStrictEqual({ environment: 'myEnv' }); + }); + + test('returns a new DSC, if no DSC was provided during transaction creation (via attributes)', () => { + const transaction = startInactiveSpan({ name: 'tx' }); + + // Setting the attribute should overwrite the computed values + transaction?.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE, 0.56); + transaction?.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, 'route'); + + const dynamicSamplingContext = getDynamicSamplingContextFromSpan(transaction!); + + expect(dynamicSamplingContext).toStrictEqual({ + release: '1.0.1', + environment: 'production', + sampled: 'true', + sample_rate: '0.56', + trace_id: expect.any(String), + transaction: 'tx', + }); + }); + + test('returns a new DSC, if no DSC was provided during transaction creation (via deprecated metadata)', () => { + const transaction = startInactiveSpan({ + name: 'tx', + }); + + const dynamicSamplingContext = getDynamicSamplingContextFromSpan(transaction!); + + expect(dynamicSamplingContext).toStrictEqual({ + release: '1.0.1', + environment: 'production', + sampled: 'true', + sample_rate: '1', + trace_id: expect.any(String), + transaction: 'tx', + }); + }); + + test('returns a new DSC, if no DSC was provided during transaction creation (via new Txn and deprecated metadata)', () => { + const transaction = new Transaction({ + name: 'tx', + metadata: { + sampleRate: 0.56, + source: 'route', + }, + sampled: true, + }); + + const dynamicSamplingContext = getDynamicSamplingContextFromSpan(transaction!); + + expect(dynamicSamplingContext).toStrictEqual({ + release: '1.0.1', + environment: 'production', + sampled: 'true', + sample_rate: '0.56', + trace_id: expect.any(String), + transaction: 'tx', + }); + }); + + describe('Including transaction name in DSC', () => { + test('is not included if transaction source is url', () => { + const transaction = new Transaction({ + name: 'tx', + metadata: { + source: 'url', + sampleRate: 0.56, + }, + }); + + const dsc = getDynamicSamplingContextFromSpan(transaction); + expect(dsc.transaction).toBeUndefined(); + }); + + test.each([ + ['is included if transaction source is parameterized route/url', 'route'], + ['is included if transaction source is a custom name', 'custom'], + ])('%s', (_: string, source) => { + const transaction = new Transaction({ + name: 'tx', + metadata: { + ...(source && { source: source as TransactionSource }), + }, + }); + + // Only setting the attribute manually because we're directly calling new Transaction() + transaction?.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, source); + + const dsc = getDynamicSamplingContextFromSpan(transaction); + + expect(dsc.transaction).toEqual('tx'); + }); + }); +}); diff --git a/packages/nextjs/src/common/wrapAppGetInitialPropsWithSentry.ts b/packages/nextjs/src/common/wrapAppGetInitialPropsWithSentry.ts index c965cacb3c32..df18b2ad952d 100644 --- a/packages/nextjs/src/common/wrapAppGetInitialPropsWithSentry.ts +++ b/packages/nextjs/src/common/wrapAppGetInitialPropsWithSentry.ts @@ -1,4 +1,10 @@ -import { addTracingExtensions, getClient, getCurrentScope, spanToTraceHeader } from '@sentry/core'; +import { + addTracingExtensions, + getClient, + getCurrentScope, + getDynamicSamplingContextFromSpan, + spanToTraceHeader, +} from '@sentry/core'; import { dynamicSamplingContextToSentryBaggageHeader } from '@sentry/utils'; import type App from 'next/app'; @@ -66,7 +72,7 @@ export function wrapAppGetInitialPropsWithSentry(origAppGetInitialProps: AppGetI if (requestTransaction) { appGetInitialProps.pageProps._sentryTraceData = spanToTraceHeader(requestTransaction); - const dynamicSamplingContext = requestTransaction.getDynamicSamplingContext(); + const dynamicSamplingContext = getDynamicSamplingContextFromSpan(requestTransaction); appGetInitialProps.pageProps._sentryBaggage = dynamicSamplingContextToSentryBaggageHeader(dynamicSamplingContext); } diff --git a/packages/nextjs/src/common/wrapErrorGetInitialPropsWithSentry.ts b/packages/nextjs/src/common/wrapErrorGetInitialPropsWithSentry.ts index 413c350ef14f..44a171d8e6d5 100644 --- a/packages/nextjs/src/common/wrapErrorGetInitialPropsWithSentry.ts +++ b/packages/nextjs/src/common/wrapErrorGetInitialPropsWithSentry.ts @@ -1,4 +1,10 @@ -import { addTracingExtensions, getClient, getCurrentScope, spanToTraceHeader } from '@sentry/core'; +import { + addTracingExtensions, + getClient, + getCurrentScope, + getDynamicSamplingContextFromSpan, + spanToTraceHeader, +} from '@sentry/core'; import { dynamicSamplingContextToSentryBaggageHeader } from '@sentry/utils'; import type { NextPageContext } from 'next'; import type { ErrorProps } from 'next/error'; @@ -58,7 +64,7 @@ export function wrapErrorGetInitialPropsWithSentry( if (requestTransaction) { errorGetInitialProps._sentryTraceData = spanToTraceHeader(requestTransaction); - const dynamicSamplingContext = requestTransaction.getDynamicSamplingContext(); + const dynamicSamplingContext = getDynamicSamplingContextFromSpan(requestTransaction); errorGetInitialProps._sentryBaggage = dynamicSamplingContextToSentryBaggageHeader(dynamicSamplingContext); } diff --git a/packages/nextjs/src/common/wrapGetInitialPropsWithSentry.ts b/packages/nextjs/src/common/wrapGetInitialPropsWithSentry.ts index 8263f00c3dcb..1a6743765cd6 100644 --- a/packages/nextjs/src/common/wrapGetInitialPropsWithSentry.ts +++ b/packages/nextjs/src/common/wrapGetInitialPropsWithSentry.ts @@ -1,4 +1,10 @@ -import { addTracingExtensions, getClient, getCurrentScope, spanToTraceHeader } from '@sentry/core'; +import { + addTracingExtensions, + getClient, + getCurrentScope, + getDynamicSamplingContextFromSpan, + spanToTraceHeader, +} from '@sentry/core'; import { dynamicSamplingContextToSentryBaggageHeader } from '@sentry/utils'; import type { NextPage } from 'next'; @@ -54,7 +60,7 @@ export function wrapGetInitialPropsWithSentry(origGetInitialProps: GetInitialPro if (requestTransaction) { initialProps._sentryTraceData = spanToTraceHeader(requestTransaction); - const dynamicSamplingContext = requestTransaction.getDynamicSamplingContext(); + const dynamicSamplingContext = getDynamicSamplingContextFromSpan(requestTransaction); initialProps._sentryBaggage = dynamicSamplingContextToSentryBaggageHeader(dynamicSamplingContext); } diff --git a/packages/nextjs/src/common/wrapGetServerSidePropsWithSentry.ts b/packages/nextjs/src/common/wrapGetServerSidePropsWithSentry.ts index fff92d31f49b..691570f87683 100644 --- a/packages/nextjs/src/common/wrapGetServerSidePropsWithSentry.ts +++ b/packages/nextjs/src/common/wrapGetServerSidePropsWithSentry.ts @@ -1,4 +1,10 @@ -import { addTracingExtensions, getClient, getCurrentScope, spanToTraceHeader } from '@sentry/core'; +import { + addTracingExtensions, + getClient, + getCurrentScope, + getDynamicSamplingContextFromSpan, + spanToTraceHeader, +} from '@sentry/core'; import { dynamicSamplingContextToSentryBaggageHeader } from '@sentry/utils'; import type { GetServerSideProps } from 'next'; @@ -51,7 +57,7 @@ export function wrapGetServerSidePropsWithSentry( if (requestTransaction) { serverSideProps.props._sentryTraceData = spanToTraceHeader(requestTransaction); - const dynamicSamplingContext = requestTransaction.getDynamicSamplingContext(); + const dynamicSamplingContext = getDynamicSamplingContextFromSpan(requestTransaction); serverSideProps.props._sentryBaggage = dynamicSamplingContextToSentryBaggageHeader(dynamicSamplingContext); } } diff --git a/packages/node/src/integrations/hapi/index.ts b/packages/node/src/integrations/hapi/index.ts index 3776cf449ce8..42335f7c4ce5 100644 --- a/packages/node/src/integrations/hapi/index.ts +++ b/packages/node/src/integrations/hapi/index.ts @@ -5,6 +5,7 @@ import { convertIntegrationFnToClass, getActiveTransaction, getCurrentScope, + getDynamicSamplingContextFromSpan, spanToTraceHeader, startTransaction, } from '@sentry/core'; @@ -101,7 +102,7 @@ export const hapiTracingPlugin = { response.header('sentry-trace', spanToTraceHeader(transaction)); const dynamicSamplingContext = dynamicSamplingContextToSentryBaggageHeader( - transaction.getDynamicSamplingContext(), + getDynamicSamplingContextFromSpan(transaction), ); if (dynamicSamplingContext) { diff --git a/packages/node/src/integrations/http.ts b/packages/node/src/integrations/http.ts index 658f914a8155..7c2627b17123 100644 --- a/packages/node/src/integrations/http.ts +++ b/packages/node/src/integrations/http.ts @@ -8,6 +8,7 @@ import { getCurrentHub, getCurrentScope, getDynamicSamplingContextFromClient, + getDynamicSamplingContextFromSpan, isSentryRequestUrl, spanToJSON, spanToTraceHeader, @@ -271,7 +272,7 @@ function _createWrappedRequestMethodFactory( if (shouldAttachTraceData(rawRequestUrl)) { if (requestSpan) { const sentryTraceHeader = spanToTraceHeader(requestSpan); - const dynamicSamplingContext = requestSpan?.transaction?.getDynamicSamplingContext(); + const dynamicSamplingContext = getDynamicSamplingContextFromSpan(requestSpan); addHeadersToRequestOptions(requestOptions, requestUrl, sentryTraceHeader, dynamicSamplingContext); } else { const client = getClient(); diff --git a/packages/node/src/integrations/undici/index.ts b/packages/node/src/integrations/undici/index.ts index 25ef141d6b83..f4aec53aa30f 100644 --- a/packages/node/src/integrations/undici/index.ts +++ b/packages/node/src/integrations/undici/index.ts @@ -5,6 +5,7 @@ import { getCurrentHub, getCurrentScope, getDynamicSamplingContextFromClient, + getDynamicSamplingContextFromSpan, isSentryRequestUrl, spanToTraceHeader, } from '@sentry/core'; @@ -181,7 +182,7 @@ export class Undici implements Integration { if (shouldAttachTraceData(stringUrl)) { if (span) { - const dynamicSamplingContext = span?.transaction?.getDynamicSamplingContext(); + const dynamicSamplingContext = getDynamicSamplingContextFromSpan(span); const sentryBaggageHeader = dynamicSamplingContextToSentryBaggageHeader(dynamicSamplingContext); setHeadersOnRequest(request, spanToTraceHeader(span), sentryBaggageHeader); diff --git a/packages/opentelemetry-node/src/propagator.ts b/packages/opentelemetry-node/src/propagator.ts index ce0f295ce720..9052ede7e966 100644 --- a/packages/opentelemetry-node/src/propagator.ts +++ b/packages/opentelemetry-node/src/propagator.ts @@ -1,7 +1,7 @@ import type { Baggage, Context, TextMapGetter, TextMapSetter } from '@opentelemetry/api'; import { TraceFlags, isSpanContextValid, propagation, trace } from '@opentelemetry/api'; import { W3CBaggagePropagator, isTracingSuppressed } from '@opentelemetry/core'; -import { spanToTraceHeader } from '@sentry/core'; +import { getDynamicSamplingContextFromSpan, spanToTraceHeader } from '@sentry/core'; import { SENTRY_BAGGAGE_KEY_PREFIX, baggageHeaderToDynamicSamplingContext, @@ -36,7 +36,7 @@ export class SentryPropagator extends W3CBaggagePropagator { setter.set(carrier, SENTRY_TRACE_HEADER, spanToTraceHeader(span)); if (span.transaction) { - const dynamicSamplingContext = span.transaction.getDynamicSamplingContext(); + const dynamicSamplingContext = getDynamicSamplingContextFromSpan(span); baggage = Object.entries(dynamicSamplingContext).reduce((b, [dscKey, dscValue]) => { if (dscValue) { return b.setEntry(`${SENTRY_BAGGAGE_KEY_PREFIX}${dscKey}`, { value: dscValue }); diff --git a/packages/remix/src/utils/instrumentServer.ts b/packages/remix/src/utils/instrumentServer.ts index 4b1109549d47..f5202d64570a 100644 --- a/packages/remix/src/utils/instrumentServer.ts +++ b/packages/remix/src/utils/instrumentServer.ts @@ -4,6 +4,7 @@ import { getActiveTransaction, getClient, getCurrentScope, + getDynamicSamplingContextFromSpan, hasTracingEnabled, runWithAsyncContext, spanToJSON, @@ -307,7 +308,7 @@ function getTraceAndBaggage(): { const span = getActiveSpan(); if (span && transaction) { - const dynamicSamplingContext = transaction.getDynamicSamplingContext(); + const dynamicSamplingContext = getDynamicSamplingContextFromSpan(transaction); return { sentryTrace: spanToTraceHeader(span), diff --git a/packages/sveltekit/src/server/handle.ts b/packages/sveltekit/src/server/handle.ts index cf195551ef61..1bb0c485168e 100644 --- a/packages/sveltekit/src/server/handle.ts +++ b/packages/sveltekit/src/server/handle.ts @@ -1,4 +1,4 @@ -import { getActiveSpan, getCurrentScope, spanToTraceHeader } from '@sentry/core'; +import { getActiveSpan, getCurrentScope, getDynamicSamplingContextFromSpan, spanToTraceHeader } from '@sentry/core'; import { getActiveTransaction, runWithAsyncContext, startSpan } from '@sentry/core'; import { captureException } from '@sentry/node'; /* eslint-disable @sentry-internal/sdk/no-optional-chaining */ @@ -100,7 +100,7 @@ export function addSentryCodeToPage(options: SentryHandleOptions): NonNullable diff --git a/packages/tracing-internal/src/browser/request.ts b/packages/tracing-internal/src/browser/request.ts index 9fb5a5cc3439..abde36a88488 100644 --- a/packages/tracing-internal/src/browser/request.ts +++ b/packages/tracing-internal/src/browser/request.ts @@ -4,6 +4,7 @@ import { getClient, getCurrentScope, getDynamicSamplingContextFromClient, + getDynamicSamplingContextFromSpan, hasTracingEnabled, spanToJSON, spanToTraceHeader, @@ -298,7 +299,7 @@ export function xhrCallback( if (xhr.setRequestHeader && shouldAttachHeaders(sentryXhrData.url)) { if (span) { const transaction = span && span.transaction; - const dynamicSamplingContext = transaction && transaction.getDynamicSamplingContext(); + const dynamicSamplingContext = transaction && getDynamicSamplingContextFromSpan(transaction); const sentryBaggageHeader = dynamicSamplingContextToSentryBaggageHeader(dynamicSamplingContext); setHeaderOnXhr(xhr, spanToTraceHeader(span), sentryBaggageHeader); } else { diff --git a/packages/tracing-internal/src/common/fetch.ts b/packages/tracing-internal/src/common/fetch.ts index d6cf469cadc8..14cf242cbf51 100644 --- a/packages/tracing-internal/src/common/fetch.ts +++ b/packages/tracing-internal/src/common/fetch.ts @@ -3,6 +3,7 @@ import { getClient, getCurrentScope, getDynamicSamplingContextFromClient, + getDynamicSamplingContextFromSpan, hasTracingEnabled, spanToTraceHeader, } from '@sentry/core'; @@ -139,7 +140,7 @@ export function addTracingHeadersToFetchRequest( const sentryTraceHeader = span ? spanToTraceHeader(span) : generateSentryTraceHeader(traceId, undefined, sampled); const dynamicSamplingContext = transaction - ? transaction.getDynamicSamplingContext() + ? getDynamicSamplingContextFromSpan(transaction) : dsc ? dsc : getDynamicSamplingContextFromClient(traceId, client, scope); diff --git a/packages/tracing/test/span.test.ts b/packages/tracing/test/span.test.ts index d47919d7acdb..e2ebf335ecb5 100644 --- a/packages/tracing/test/span.test.ts +++ b/packages/tracing/test/span.test.ts @@ -571,24 +571,19 @@ describe('Span', () => { hub, ); - const hubSpy = jest.spyOn(hub.getClient()!, 'getOptions'); - const dynamicSamplingContext = transaction.getDynamicSamplingContext(); - expect(hubSpy).not.toHaveBeenCalled(); expect(dynamicSamplingContext).toStrictEqual({ environment: 'myEnv' }); }); test('should return new DSC, if no DSC was provided during transaction creation', () => { - const transaction = new Transaction( - { - name: 'tx', - metadata: { - sampleRate: 0.56, - }, + const transaction = new Transaction({ + name: 'tx', + metadata: { + sampleRate: 0.56, }, - hub, - ); + sampled: true, + }); const getOptionsSpy = jest.spyOn(hub.getClient()!, 'getOptions'); @@ -598,6 +593,7 @@ describe('Span', () => { expect(dynamicSamplingContext).toStrictEqual({ release: '1.0.1', environment: 'production', + sampled: 'true', sample_rate: '0.56', trace_id: expect.any(String), transaction: 'tx', diff --git a/packages/types/src/transaction.ts b/packages/types/src/transaction.ts index 586523625710..05be3c588c06 100644 --- a/packages/types/src/transaction.ts +++ b/packages/types/src/transaction.ts @@ -140,7 +140,11 @@ export interface Transaction extends TransactionContext, Omit): void; - /** Return the current Dynamic Sampling Context of this transaction */ + /** + * Return the current Dynamic Sampling Context of this transaction + * + * @deprecated Use top-level `getDynamicSamplingContextFromSpan` instead. + */ getDynamicSamplingContext(): Partial; } From 1a7ea9ba5138ca843a13858d6d74a6e1cf2a7893 Mon Sep 17 00:00:00 2001 From: Luca Forstner Date: Wed, 10 Jan 2024 12:54:55 +0100 Subject: [PATCH 41/43] chore: Add migration docs for `Hub` (#10126) Co-authored-by: Lukas Stracke --- MIGRATION.md | 55 +++++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 44 insertions(+), 11 deletions(-) diff --git a/MIGRATION.md b/MIGRATION.md index 7d8645fd4e16..95aa940265f3 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -8,6 +8,50 @@ npx @sentry/migr8@latest This will let you select which updates to run, and automatically update your code. Make sure to still review all code changes! +## Deprecate `Hub` + +The `Hub` has been a very important part of the Sentry SDK API up until now. +Hubs were the SDK's "unit of concurrency" to keep track of data across threads and to scope data to certain parts of your code. +Because it is overly complicated and confusing to power users, it is going to be replaced by a set of new APIs: the "new Scope API". + +`Scope`s have existed before in the SDK but we are now expanding on them because we have found them powerful enough to fully cover the `Hub` API. + +If you are using the `Hub` right now, see the following table on how to migrate to the new API: + +| Old `Hub` API | New `Scope` API | +| --- | --- | +| `new Hub()` | `withScope()`, `withIsolationScope()` or `new Scope()` | +| hub.isOlderThan() | REMOVED - Was used to compare `Hub` instances, which are gonna be removed | +| hub.bindClient() | A combination of `scope.setClient()` and `client.setupIntegrations()` | +| hub.pushScope() | `Sentry.withScope()` | +| hub.popScope() | `Sentry.withScope()` | +| hub.withScope() | `Sentry.withScope()` | +| getClient() | `Sentry.getClient()` | +| getScope() | `Sentry.getCurrentScope()` to get the currently active scope | +| getIsolationScope() | `Sentry.getIsolationScope()` | +| getStack() | REMOVED - The stack used to hold scopes. Scopes are used directly now | +| getStackTop() | REMOVED - The stack used to hold scopes. Scopes are used directly now | +| captureException() | `Sentry.captureException()` | +| captureMessage() | `Sentry.captureMessage()` | +| captureEvent() | `Sentry.captureEvent()` | +| lastEventId() | REMOVED - Use event processors or beforeSend instead | +| addBreadcrumb() | `Sentry.addBreadcrumb()` | +| setUser() | `Sentry.setUser()` | +| setTags() | `Sentry.setTags()` | +| setExtras() | `Sentry.setExtras()` | +| setTag() | `Sentry.setTag()` | +| setExtra() | `Sentry.setExtra()` | +| setContext() | `Sentry.setContext()` | +| configureScope() | REMOVED - Scopes are now the unit of concurrency | +| run() | `Sentry.withScope()` or `Sentry.withIsolationScope()` | +| getIntegration() | `client.getIntegration()` | +| startTransaction() | `Sentry.startSpan()`, `Sentry.startInactiveSpan()` or `Sentry.startSpanManual()` | +| traceHeaders() | REMOVED - The closest equivalent is now `spanToTraceHeader(getActiveSpan())` | +| captureSession() | `Sentry.captureSession()` | +| startSession() | `Sentry.startSession()` | +| endSession() | `Sentry.endSession()` | +| shouldSendDefaultPii() | REMOVED - The closest equivalent is `Sentry.getClient().getOptions().sendDefaultPii` | + ## Deprecate `scope.getSpan()` and `scope.setSpan()` Instead, you can get the currently active span via `Sentry.getActiveSpan()`. @@ -73,17 +117,6 @@ Sentry.init({ }); ``` -## Deprecated fields on `Hub` - -In v8, the Hub class will be removed. The following methods are therefore deprecated: - -* `hub.startTransaction()`: See [Deprecation of `startTransaction`](#deprecate-starttransaction) -* `hub.lastEventId()`: See [Deprecation of `lastEventId`](#deprecate-sentrylasteventid-and-hublasteventid) -* `hub.startSession()`: Use top-level `Sentry.startSession()` instead -* `hub.endSession()`: Use top-level `Sentry.endSession()` instead -* `hub.captureSession()`: Use top-level `Sentry.captureSession()` instead -* `hub.shouldSendDefaultPii()`: Access Sentry client option via `Sentry.getClient().getOptions().sendDefaultPii` instead - ## Deprecated fields on `Span` and `Transaction` In v8, the Span class is heavily reworked. The following properties & methods are thus deprecated: From 1005ba9c4caf7f98739c8d42c5552d264478e305 Mon Sep 17 00:00:00 2001 From: Luca Forstner Date: Wed, 10 Jan 2024 13:03:13 +0100 Subject: [PATCH 42/43] meta: Ignore `Transaction` deprecation lint warnings in DSC tests (#10133) Currently fails develop. --- packages/core/test/lib/tracing/dynamicSamplingContext.test.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/core/test/lib/tracing/dynamicSamplingContext.test.ts b/packages/core/test/lib/tracing/dynamicSamplingContext.test.ts index 06a061612eda..da8bf1595e21 100644 --- a/packages/core/test/lib/tracing/dynamicSamplingContext.test.ts +++ b/packages/core/test/lib/tracing/dynamicSamplingContext.test.ts @@ -20,6 +20,7 @@ describe('getDynamicSamplingContextFromSpan', () => { }); test('returns the DSC provided during transaction creation', () => { + // eslint-disable-next-line deprecation/deprecation const transaction = new Transaction({ name: 'tx', metadata: { dynamicSamplingContext: { environment: 'myEnv' } }, @@ -67,6 +68,7 @@ describe('getDynamicSamplingContextFromSpan', () => { }); test('returns a new DSC, if no DSC was provided during transaction creation (via new Txn and deprecated metadata)', () => { + // eslint-disable-next-line deprecation/deprecation const transaction = new Transaction({ name: 'tx', metadata: { @@ -90,6 +92,7 @@ describe('getDynamicSamplingContextFromSpan', () => { describe('Including transaction name in DSC', () => { test('is not included if transaction source is url', () => { + // eslint-disable-next-line deprecation/deprecation const transaction = new Transaction({ name: 'tx', metadata: { @@ -106,6 +109,7 @@ describe('getDynamicSamplingContextFromSpan', () => { ['is included if transaction source is parameterized route/url', 'route'], ['is included if transaction source is a custom name', 'custom'], ])('%s', (_: string, source) => { + // eslint-disable-next-line deprecation/deprecation const transaction = new Transaction({ name: 'tx', metadata: { From 0fb8b64817dc928ebcd6e6b5aab7ca61a877471b Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Wed, 10 Jan 2024 12:12:02 +0100 Subject: [PATCH 43/43] meta: Update CHANGELOG for 7.93.0 --- CHANGELOG.md | 65 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2bf94ab74c14..0901896acc7a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,71 @@ - "You miss 100 percent of the chances you don't take. — Wayne Gretzky" — Michael Scott +## 7.93.0 + +### Important Changes + +#### Deprecations + +As we're moving closer to the next major version of the SDK, more public APIs were deprecated. + +To get a head start on migrating to the replacement APIs, please take a look at our +[migration guide](https://github.com/getsentry/sentry-javascript/blob/develop/MIGRATION.md). + +- feat(core): Deprecate `getActiveTransaction()` & `scope.getTransaction()` (#10098) +- feat(core): Deprecate `Hub.shouldSendDefaultPii` (#10062) +- feat(core): Deprecate `new Transaction()` (#10125) +- feat(core): Deprecate `scope.getSpan()` & `scope.setSpan()` (#10114) +- feat(core): Deprecate `scope.setTransactionName()` (#10113) +- feat(core): Deprecate `span.startChild()` (#10091) +- feat(core): Deprecate `startTransaction()` (#10073) +- feat(core): Deprecate `Transaction.getDynamicSamplingContext` in favor of `getDynamicSamplingContextFromSpan` (#10094) +- feat(core): Deprecate arguments for `startSpan()` (#10101) +- feat(core): Deprecate hub capture APIs and add them to `Scope` (#10039) +- feat(core): Deprecate session APIs on hub and add global replacements (#10054) +- feat(core): Deprecate span `name` and `description` (#10056) +- feat(core): Deprecate span `tags`, `data`, `context` & setters (#10053) +- feat(core): Deprecate transaction metadata in favor of attributes (#10097) +- feat(core): Deprecate `span.sampled` in favor of `span.isRecording()` (#10034) +- ref(node-experimental): Deprecate `lastEventId` on scope (#10093) + +#### Cron Monitoring Support for `node-schedule` library + +This release adds auto instrumented check-ins for the `node-schedule` library. + +```ts +import * as Sentry from '@sentry/node'; +import * as schedule from 'node-schedule'; + +const scheduleWithCheckIn = Sentry.cron.instrumentNodeSchedule(schedule); + +const job = scheduleWithCheckIn.scheduleJob('my-cron-job', '* * * * *', () => { + console.log('You will see this message every minute'); +}); +``` + +- feat(node): Instrumentation for `node-schedule` library (#10086) + +### Other Changes + +- feat(core): Add `span.spanContext()` (#10037) +- feat(core): Add `spanToJSON()` method to get span properties (#10074) +- feat(core): Allow to pass `scope` to `startSpan` APIs (#10076) +- feat(core): Allow to pass start/end timestamp for spans flexibly (#10060) +- feat(node): Make `getModuleFromFilename` compatible with ESM (#10061) +- feat(replay): Update rrweb to 2.7.3 (#10072) +- feat(utils): Add `parameterize` function (#9145) +- fix(astro): Use correct package name for CF (#10099) +- fix(core): Do not run `setup` for integration on client multiple times (#10116) +- fix(core): Ensure we copy passed in span data/tags/attributes (#10105) +- fix(cron): Make name required for instrumentNodeCron option (#10070) +- fix(nextjs): Don't capture not-found and redirect errors in generation functions (#10057) +- fix(node): `LocalVariables` integration should have correct name (#10084) +- fix(node): Anr events should have an `event_id` (#10068) +- fix(node): Revert to only use sync debugger for `LocalVariables` (#10077) +- fix(node): Update ANR min node version to v16.17.0 (#10107) + + ## 7.92.0 ### Important Changes