diff --git a/packages/node-experimental/src/sdk/hubextensions.ts b/packages/node-experimental/src/sdk/hubextensions.ts new file mode 100644 index 000000000000..4971226fee01 --- /dev/null +++ b/packages/node-experimental/src/sdk/hubextensions.ts @@ -0,0 +1,80 @@ +import type { startTransaction } from '@sentry/core'; +import { addTracingExtensions as _addTracingExtensions, getMainCarrier } from '@sentry/core'; +import type { Breadcrumb, Hub, Transaction } from '@sentry/types'; +import { dateTimestampInSeconds } from '@sentry/utils'; + +import type { TransactionWithBreadcrumbs } from '../types'; + +const DEFAULT_MAX_BREADCRUMBS = 100; + +/** + * Add tracing extensions, ensuring a patched `startTransaction` to work with OTEL. + */ +export function addTracingExtensions(): void { + _addTracingExtensions(); + + const carrier = getMainCarrier(); + if (!carrier.__SENTRY__) { + return; + } + + carrier.__SENTRY__.extensions = carrier.__SENTRY__.extensions || {}; + if (carrier.__SENTRY__.extensions.startTransaction) { + carrier.__SENTRY__.extensions.startTransaction = getPatchedStartTransaction( + carrier.__SENTRY__.extensions.startTransaction as typeof startTransaction, + ); + } +} + +/** + * We patch the `startTransaction` function to ensure we create a `TransactionWithBreadcrumbs` instead of a regular `Transaction`. + */ +function getPatchedStartTransaction(_startTransaction: typeof startTransaction): typeof startTransaction { + return function (this: Hub, ...args) { + const transaction = _startTransaction.apply(this, args); + + return patchTransaction(transaction); + }; +} + +function patchTransaction(transaction: Transaction): TransactionWithBreadcrumbs { + return new Proxy(transaction as TransactionWithBreadcrumbs, { + get(target, prop, receiver) { + if (prop === 'addBreadcrumb') { + return addBreadcrumb; + } + if (prop === 'getBreadcrumbs') { + return getBreadcrumbs; + } + if (prop === '_breadcrumbs') { + const breadcrumbs = Reflect.get(target, prop, receiver); + return breadcrumbs || []; + } + return Reflect.get(target, prop, receiver); + }, + }); +} + +/** Add a breadcrumb to a transaction. */ +function addBreadcrumb(this: TransactionWithBreadcrumbs, breadcrumb: Breadcrumb, maxBreadcrumbs?: number): void { + const maxCrumbs = typeof maxBreadcrumbs === 'number' ? maxBreadcrumbs : DEFAULT_MAX_BREADCRUMBS; + + // No data has been changed, so don't notify scope listeners + if (maxCrumbs <= 0) { + return; + } + + const mergedBreadcrumb = { + timestamp: dateTimestampInSeconds(), + ...breadcrumb, + }; + + const breadcrumbs = this._breadcrumbs; + breadcrumbs.push(mergedBreadcrumb); + this._breadcrumbs = breadcrumbs.length > maxCrumbs ? breadcrumbs.slice(-maxCrumbs) : breadcrumbs; +} + +/** Get all breadcrumbs from a transaction. */ +function getBreadcrumbs(this: TransactionWithBreadcrumbs): Breadcrumb[] { + return this._breadcrumbs; +} diff --git a/packages/node-experimental/src/sdk/scope.ts b/packages/node-experimental/src/sdk/scope.ts index ab255f1d20bc..12fcc6862904 100644 --- a/packages/node-experimental/src/sdk/scope.ts +++ b/packages/node-experimental/src/sdk/scope.ts @@ -1,26 +1,9 @@ import { Scope } from '@sentry/core'; -import type { Breadcrumb, Transaction } from '@sentry/types'; -import { dateTimestampInSeconds } from '@sentry/utils'; +import type { Breadcrumb } from '@sentry/types'; +import type { TransactionWithBreadcrumbs } from '../types'; import { getActiveSpan } from './trace'; -const DEFAULT_MAX_BREADCRUMBS = 100; - -/** - * This is a fork of the base Transaction with OTEL specific stuff added. - * Note that we do not solve this via an actual subclass, but by wrapping this in a proxy when we need it - - * as we can't easily control all the places a transaction may be created. - */ -interface TransactionWithBreadcrumbs extends Transaction { - _breadcrumbs: Breadcrumb[]; - - /** Get all breadcrumbs added to this transaction. */ - getBreadcrumbs(): Breadcrumb[]; - - /** Add a breadcrumb to this transaction. */ - addBreadcrumb(breadcrumb: Breadcrumb, maxBreadcrumbs?: number): void; -} - /** A fork of the classic scope with some otel specific stuff. */ export class OtelScope extends Scope { /** @@ -54,7 +37,7 @@ export class OtelScope extends Scope { public addBreadcrumb(breadcrumb: Breadcrumb, maxBreadcrumbs?: number): this { const transaction = getActiveTransaction(); - if (transaction) { + if (transaction && transaction.addBreadcrumb) { transaction.addBreadcrumb(breadcrumb, maxBreadcrumbs); return this; } @@ -67,7 +50,7 @@ export class OtelScope extends Scope { */ protected _getBreadcrumbs(): Breadcrumb[] { const transaction = getActiveTransaction(); - const transactionBreadcrumbs = transaction ? transaction.getBreadcrumbs() : []; + const transactionBreadcrumbs = transaction && transaction.getBreadcrumbs ? transaction.getBreadcrumbs() : []; return this._breadcrumbs.concat(transactionBreadcrumbs); } @@ -79,60 +62,5 @@ export class OtelScope extends Scope { */ function getActiveTransaction(): TransactionWithBreadcrumbs | undefined { const activeSpan = getActiveSpan(); - const transaction = activeSpan && activeSpan.transaction; - - if (!transaction) { - return undefined; - } - - if (transactionHasBreadcrumbs(transaction)) { - return transaction; - } - - return new Proxy(transaction as TransactionWithBreadcrumbs, { - get(target, prop, receiver) { - if (prop === 'addBreadcrumb') { - return addBreadcrumb; - } - if (prop === 'getBreadcrumbs') { - return getBreadcrumbs; - } - if (prop === '_breadcrumbs') { - const breadcrumbs = Reflect.get(target, prop, receiver); - return breadcrumbs || []; - } - return Reflect.get(target, prop, receiver); - }, - }); -} - -function transactionHasBreadcrumbs(transaction: Transaction): transaction is TransactionWithBreadcrumbs { - return ( - typeof (transaction as TransactionWithBreadcrumbs).getBreadcrumbs === 'function' && - typeof (transaction as TransactionWithBreadcrumbs).addBreadcrumb === 'function' - ); -} - -/** Add a breadcrumb to a transaction. */ -function addBreadcrumb(this: TransactionWithBreadcrumbs, breadcrumb: Breadcrumb, maxBreadcrumbs?: number): void { - const maxCrumbs = typeof maxBreadcrumbs === 'number' ? maxBreadcrumbs : DEFAULT_MAX_BREADCRUMBS; - - // No data has been changed, so don't notify scope listeners - if (maxCrumbs <= 0) { - return; - } - - const mergedBreadcrumb = { - timestamp: dateTimestampInSeconds(), - ...breadcrumb, - }; - - const breadcrumbs = this._breadcrumbs; - breadcrumbs.push(mergedBreadcrumb); - this._breadcrumbs = breadcrumbs.length > maxCrumbs ? breadcrumbs.slice(-maxCrumbs) : breadcrumbs; -} - -/** Get all breadcrumbs from a transaction. */ -function getBreadcrumbs(this: TransactionWithBreadcrumbs): Breadcrumb[] { - return this._breadcrumbs; + return activeSpan && (activeSpan.transaction as TransactionWithBreadcrumbs | undefined); } diff --git a/packages/node-experimental/src/types.ts b/packages/node-experimental/src/types.ts index 107cfdb37266..65e3d905be72 100644 --- a/packages/node-experimental/src/types.ts +++ b/packages/node-experimental/src/types.ts @@ -1,5 +1,6 @@ import type { Tracer } from '@opentelemetry/api'; import type { NodeClient, NodeOptions } from '@sentry/node'; +import type { Breadcrumb, Transaction } from '@sentry/types'; export type NodeExperimentalOptions = NodeOptions; export type NodeExperimentalClientOptions = ConstructorParameters[0]; @@ -8,3 +9,18 @@ export interface NodeExperimentalClient extends NodeClient { tracer: Tracer; getOptions(): NodeExperimentalClientOptions; } + +/** + * This is a fork of the base Transaction with OTEL specific stuff added. + * Note that we do not solve this via an actual subclass, but by wrapping this in a proxy when we need it - + * as we can't easily control all the places a transaction may be created. + */ +export interface TransactionWithBreadcrumbs extends Transaction { + _breadcrumbs: Breadcrumb[]; + + /** Get all breadcrumbs added to this transaction. */ + getBreadcrumbs(): Breadcrumb[]; + + /** Add a breadcrumb to this transaction. */ + addBreadcrumb(breadcrumb: Breadcrumb, maxBreadcrumbs?: number): void; +}