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, },