diff --git a/packages/core/src/baseclient.ts b/packages/core/src/baseclient.ts index 6d246106048c..9a3413e51c9b 100644 --- a/packages/core/src/baseclient.ts +++ b/packages/core/src/baseclient.ts @@ -35,6 +35,7 @@ import { SyncPromise, truncate, uuid4, + timestampInSeconds, } from '@sentry/utils'; import { getEnvelopeEndpointWithUrlEncodedAuth } from './api'; @@ -653,6 +654,29 @@ export abstract class BaseClient implements Client { throw new SentryError('`beforeSend` returned `null`, will not send event.', 'log'); } + const transactionInfo = processedEvent.transaction_info; + if ( + processedEvent.type === 'transaction' && + transactionInfo && + processedEvent.transaction && + processedEvent.transaction !== event.transaction + ) { + const source = 'custom'; + event.transaction_info = { + ...transactionInfo, + source, + name_changes: [ + ...transactionInfo.name_changes, + { + name: processedEvent.transaction, + source, + timestamp: timestampInSeconds(), + propagations: transactionInfo.propagations, + }, + ], + }; + } + const session = scope && scope.getSession(); if (!isTransaction && session) { this._updateSessionFromEvent(session, processedEvent); diff --git a/packages/node/src/integrations/http.ts b/packages/node/src/integrations/http.ts index 9edd5d8c3d34..d2a81170d692 100644 --- a/packages/node/src/integrations/http.ts +++ b/packages/node/src/integrations/http.ts @@ -185,6 +185,11 @@ function _createWrappedRequestMethodFactory( addRequestBreadcrumb('response', requestUrl, req, res); } if (tracingEnabled && span) { + const transaction = span.transaction; + if (transaction) { + transaction.metadata.propagations += 1; + } + if (res.statusCode) { span.setHttpStatus(res.statusCode); } diff --git a/packages/tracing/src/browser/request.ts b/packages/tracing/src/browser/request.ts index ae67d5b6ae19..31322c9c1daf 100644 --- a/packages/tracing/src/browser/request.ts +++ b/packages/tracing/src/browser/request.ts @@ -199,6 +199,7 @@ export function fetchCallback( // eslint-disable-next-line @typescript-eslint/no-explicit-any const options = (handlerData.args[1] = (handlerData.args[1] as { [key: string]: any }) || {}); options.headers = addTracingHeaders(request, activeTransaction.getBaggage(), span, options); + activeTransaction.metadata.propagations += 1; } } @@ -304,6 +305,7 @@ export function xhrCallback( BAGGAGE_HEADER_NAME, mergeAndSerializeBaggage(activeTransaction.getBaggage(), headerBaggageString), ); + activeTransaction.metadata.propagations += 1; } catch (_) { // Error: InvalidStateError: Failed to execute 'setRequestHeader' on 'XMLHttpRequest': The object's state must be OPENED. } diff --git a/packages/tracing/src/index.ts b/packages/tracing/src/index.ts index 14d80fb7b4cc..551280a2e8dd 100644 --- a/packages/tracing/src/index.ts +++ b/packages/tracing/src/index.ts @@ -27,7 +27,7 @@ export { BrowserTracing, BROWSER_TRACING_INTEGRATION_ID } from './browser'; export { Span, spanStatusfromHttpCode } from './span'; // eslint-disable-next-line deprecation/deprecation export { SpanStatus } from './spanstatus'; -export { Transaction } from './transaction'; +export { Transaction, generateTransactionNameChange } from './transaction'; export { instrumentOutgoingRequests, defaultRequestInstrumentationOptions } from './browser'; export { IdleTransaction } from './idletransaction'; export { startIdleTransaction } from './hubextensions'; diff --git a/packages/tracing/src/transaction.ts b/packages/tracing/src/transaction.ts index 2b509763743d..19cc9466fd8d 100644 --- a/packages/tracing/src/transaction.ts +++ b/packages/tracing/src/transaction.ts @@ -8,8 +8,17 @@ import { Transaction as TransactionInterface, TransactionContext, TransactionMetadata, + TransactionNameChange, + TransactionSource, } from '@sentry/types'; -import { createBaggage, dropUndefinedKeys, getSentryBaggageItems, isBaggageMutable, logger } from '@sentry/utils'; +import { + createBaggage, + dropUndefinedKeys, + getSentryBaggageItems, + isBaggageMutable, + logger, + timestampWithMs, +} from '@sentry/utils'; import { Span as SpanClass, SpanRecorder } from './span'; @@ -45,6 +54,8 @@ export class Transaction extends SpanClass implements TransactionInterface { this.metadata = { ...transactionContext.metadata, spanMetadata: {}, + nameChanges: [], + propagations: 0, }; this._trimEnd = transactionContext.trimEnd; @@ -61,7 +72,9 @@ export class Transaction extends SpanClass implements TransactionInterface { /** Setter for `name` property, which also sets `source` */ public set name(newName: string) { this._name = newName; - this.metadata.source = 'custom'; + const source = 'custom'; + this.metadata.source = source; + this.metadata.nameChanges.push(generateTransactionNameChange(source, this.metadata.propagations)); } /** @@ -70,6 +83,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.metadata.nameChanges.push(generateTransactionNameChange(source, this.metadata.propagations)); } /** @@ -156,6 +170,8 @@ export class Transaction extends SpanClass implements TransactionInterface { ...(metadata.source && { transaction_info: { source: metadata.source, + name_changes: metadata.nameChanges, + propagations: metadata.propagations, }, }), }; @@ -273,3 +289,12 @@ export class Transaction extends SpanClass implements TransactionInterface { ); } } + +/** Generate objects representing a transaction name change */ +export function generateTransactionNameChange(source: TransactionSource, propagations: number): TransactionNameChange { + return { + source, + timestamp: timestampWithMs(), + propagations, + }; +} diff --git a/packages/types/src/event.ts b/packages/types/src/event.ts index 0d3897734956..65a315c42456 100644 --- a/packages/types/src/event.ts +++ b/packages/types/src/event.ts @@ -11,7 +11,7 @@ import { CaptureContext } from './scope'; import { SdkInfo } from './sdkinfo'; import { Severity, SeverityLevel } from './severity'; import { Span } from './span'; -import { TransactionSource } from './transaction'; +import { TransactionNameChange, TransactionSource } from './transaction'; import { User } from './user'; /** JSDoc */ @@ -49,6 +49,9 @@ export interface Event { sdkProcessingMetadata?: { [key: string]: any }; transaction_info?: { source: TransactionSource; + name_changes: TransactionNameChange[]; + // The total number of propagations that happened + propagations: number; }; } diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index c43bb34c8f8e..20718b53830f 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -73,6 +73,7 @@ export type { TransactionMetadata, TransactionSamplingMethod, TransactionSource, + TransactionNameChange, } from './transaction'; export type { DurationUnit, diff --git a/packages/types/src/transaction.ts b/packages/types/src/transaction.ts index d8029c2184cb..bc3da939387a 100644 --- a/packages/types/src/transaction.ts +++ b/packages/types/src/transaction.ts @@ -150,6 +150,12 @@ export interface TransactionMetadata { /** Metadata for the transaction's spans, keyed by spanId */ spanMetadata: { [spanId: string]: { [key: string]: unknown } }; + + /** Metadata representing information about transaction name changes */ + nameChanges: TransactionNameChange[]; + + /** The total number of propagations that happened */ + propagations: number; } /** @@ -169,3 +175,15 @@ export type TransactionSource = | 'component' /** Name of a background task (e.g. a Celery task) */ | 'task'; + +/** + * Object representing metadata about when a transaction name was changed. + */ +export interface TransactionNameChange { + // unix timestamp when the name was changed + timestamp: number; + // new source + source: TransactionSource; + // number of propagations since start of transaction. + propagations: number; +}