Skip to content

ref(node-experimental): Patch startTransaction for breadcrumbs #9010

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Sep 15, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 80 additions & 0 deletions packages/node-experimental/src/sdk/hubextensions.ts
Original file line number Diff line number Diff line change
@@ -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;
}
82 changes: 5 additions & 77 deletions packages/node-experimental/src/sdk/scope.ts
Original file line number Diff line number Diff line change
@@ -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 {
/**
Expand Down Expand Up @@ -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;
}
Expand All @@ -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);
}
Expand All @@ -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);
}
16 changes: 16 additions & 0 deletions packages/node-experimental/src/types.ts
Original file line number Diff line number Diff line change
@@ -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<typeof NodeClient>[0];
Expand All @@ -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;
}