Skip to content

Commit d8b879a

Browse files
authored
ref(node-experimental): Patch startTransaction for breadcrumbs (#9010)
This refactors the code for keeping breadcrumbs on transactions in node-experimental to instead patch `startTransaction` on the hub.
1 parent 2356e80 commit d8b879a

File tree

3 files changed

+101
-77
lines changed

3 files changed

+101
-77
lines changed
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import type { startTransaction } from '@sentry/core';
2+
import { addTracingExtensions as _addTracingExtensions, getMainCarrier } from '@sentry/core';
3+
import type { Breadcrumb, Hub, Transaction } from '@sentry/types';
4+
import { dateTimestampInSeconds } from '@sentry/utils';
5+
6+
import type { TransactionWithBreadcrumbs } from '../types';
7+
8+
const DEFAULT_MAX_BREADCRUMBS = 100;
9+
10+
/**
11+
* Add tracing extensions, ensuring a patched `startTransaction` to work with OTEL.
12+
*/
13+
export function addTracingExtensions(): void {
14+
_addTracingExtensions();
15+
16+
const carrier = getMainCarrier();
17+
if (!carrier.__SENTRY__) {
18+
return;
19+
}
20+
21+
carrier.__SENTRY__.extensions = carrier.__SENTRY__.extensions || {};
22+
if (carrier.__SENTRY__.extensions.startTransaction) {
23+
carrier.__SENTRY__.extensions.startTransaction = getPatchedStartTransaction(
24+
carrier.__SENTRY__.extensions.startTransaction as typeof startTransaction,
25+
);
26+
}
27+
}
28+
29+
/**
30+
* We patch the `startTransaction` function to ensure we create a `TransactionWithBreadcrumbs` instead of a regular `Transaction`.
31+
*/
32+
function getPatchedStartTransaction(_startTransaction: typeof startTransaction): typeof startTransaction {
33+
return function (this: Hub, ...args) {
34+
const transaction = _startTransaction.apply(this, args);
35+
36+
return patchTransaction(transaction);
37+
};
38+
}
39+
40+
function patchTransaction(transaction: Transaction): TransactionWithBreadcrumbs {
41+
return new Proxy(transaction as TransactionWithBreadcrumbs, {
42+
get(target, prop, receiver) {
43+
if (prop === 'addBreadcrumb') {
44+
return addBreadcrumb;
45+
}
46+
if (prop === 'getBreadcrumbs') {
47+
return getBreadcrumbs;
48+
}
49+
if (prop === '_breadcrumbs') {
50+
const breadcrumbs = Reflect.get(target, prop, receiver);
51+
return breadcrumbs || [];
52+
}
53+
return Reflect.get(target, prop, receiver);
54+
},
55+
});
56+
}
57+
58+
/** Add a breadcrumb to a transaction. */
59+
function addBreadcrumb(this: TransactionWithBreadcrumbs, breadcrumb: Breadcrumb, maxBreadcrumbs?: number): void {
60+
const maxCrumbs = typeof maxBreadcrumbs === 'number' ? maxBreadcrumbs : DEFAULT_MAX_BREADCRUMBS;
61+
62+
// No data has been changed, so don't notify scope listeners
63+
if (maxCrumbs <= 0) {
64+
return;
65+
}
66+
67+
const mergedBreadcrumb = {
68+
timestamp: dateTimestampInSeconds(),
69+
...breadcrumb,
70+
};
71+
72+
const breadcrumbs = this._breadcrumbs;
73+
breadcrumbs.push(mergedBreadcrumb);
74+
this._breadcrumbs = breadcrumbs.length > maxCrumbs ? breadcrumbs.slice(-maxCrumbs) : breadcrumbs;
75+
}
76+
77+
/** Get all breadcrumbs from a transaction. */
78+
function getBreadcrumbs(this: TransactionWithBreadcrumbs): Breadcrumb[] {
79+
return this._breadcrumbs;
80+
}
Lines changed: 5 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,9 @@
11
import { Scope } from '@sentry/core';
2-
import type { Breadcrumb, Transaction } from '@sentry/types';
3-
import { dateTimestampInSeconds } from '@sentry/utils';
2+
import type { Breadcrumb } from '@sentry/types';
43

4+
import type { TransactionWithBreadcrumbs } from '../types';
55
import { getActiveSpan } from './trace';
66

7-
const DEFAULT_MAX_BREADCRUMBS = 100;
8-
9-
/**
10-
* This is a fork of the base Transaction with OTEL specific stuff added.
11-
* Note that we do not solve this via an actual subclass, but by wrapping this in a proxy when we need it -
12-
* as we can't easily control all the places a transaction may be created.
13-
*/
14-
interface TransactionWithBreadcrumbs extends Transaction {
15-
_breadcrumbs: Breadcrumb[];
16-
17-
/** Get all breadcrumbs added to this transaction. */
18-
getBreadcrumbs(): Breadcrumb[];
19-
20-
/** Add a breadcrumb to this transaction. */
21-
addBreadcrumb(breadcrumb: Breadcrumb, maxBreadcrumbs?: number): void;
22-
}
23-
247
/** A fork of the classic scope with some otel specific stuff. */
258
export class OtelScope extends Scope {
269
/**
@@ -54,7 +37,7 @@ export class OtelScope extends Scope {
5437
public addBreadcrumb(breadcrumb: Breadcrumb, maxBreadcrumbs?: number): this {
5538
const transaction = getActiveTransaction();
5639

57-
if (transaction) {
40+
if (transaction && transaction.addBreadcrumb) {
5841
transaction.addBreadcrumb(breadcrumb, maxBreadcrumbs);
5942
return this;
6043
}
@@ -67,7 +50,7 @@ export class OtelScope extends Scope {
6750
*/
6851
protected _getBreadcrumbs(): Breadcrumb[] {
6952
const transaction = getActiveTransaction();
70-
const transactionBreadcrumbs = transaction ? transaction.getBreadcrumbs() : [];
53+
const transactionBreadcrumbs = transaction && transaction.getBreadcrumbs ? transaction.getBreadcrumbs() : [];
7154

7255
return this._breadcrumbs.concat(transactionBreadcrumbs);
7356
}
@@ -79,60 +62,5 @@ export class OtelScope extends Scope {
7962
*/
8063
function getActiveTransaction(): TransactionWithBreadcrumbs | undefined {
8164
const activeSpan = getActiveSpan();
82-
const transaction = activeSpan && activeSpan.transaction;
83-
84-
if (!transaction) {
85-
return undefined;
86-
}
87-
88-
if (transactionHasBreadcrumbs(transaction)) {
89-
return transaction;
90-
}
91-
92-
return new Proxy(transaction as TransactionWithBreadcrumbs, {
93-
get(target, prop, receiver) {
94-
if (prop === 'addBreadcrumb') {
95-
return addBreadcrumb;
96-
}
97-
if (prop === 'getBreadcrumbs') {
98-
return getBreadcrumbs;
99-
}
100-
if (prop === '_breadcrumbs') {
101-
const breadcrumbs = Reflect.get(target, prop, receiver);
102-
return breadcrumbs || [];
103-
}
104-
return Reflect.get(target, prop, receiver);
105-
},
106-
});
107-
}
108-
109-
function transactionHasBreadcrumbs(transaction: Transaction): transaction is TransactionWithBreadcrumbs {
110-
return (
111-
typeof (transaction as TransactionWithBreadcrumbs).getBreadcrumbs === 'function' &&
112-
typeof (transaction as TransactionWithBreadcrumbs).addBreadcrumb === 'function'
113-
);
114-
}
115-
116-
/** Add a breadcrumb to a transaction. */
117-
function addBreadcrumb(this: TransactionWithBreadcrumbs, breadcrumb: Breadcrumb, maxBreadcrumbs?: number): void {
118-
const maxCrumbs = typeof maxBreadcrumbs === 'number' ? maxBreadcrumbs : DEFAULT_MAX_BREADCRUMBS;
119-
120-
// No data has been changed, so don't notify scope listeners
121-
if (maxCrumbs <= 0) {
122-
return;
123-
}
124-
125-
const mergedBreadcrumb = {
126-
timestamp: dateTimestampInSeconds(),
127-
...breadcrumb,
128-
};
129-
130-
const breadcrumbs = this._breadcrumbs;
131-
breadcrumbs.push(mergedBreadcrumb);
132-
this._breadcrumbs = breadcrumbs.length > maxCrumbs ? breadcrumbs.slice(-maxCrumbs) : breadcrumbs;
133-
}
134-
135-
/** Get all breadcrumbs from a transaction. */
136-
function getBreadcrumbs(this: TransactionWithBreadcrumbs): Breadcrumb[] {
137-
return this._breadcrumbs;
65+
return activeSpan && (activeSpan.transaction as TransactionWithBreadcrumbs | undefined);
13866
}

packages/node-experimental/src/types.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import type { Tracer } from '@opentelemetry/api';
22
import type { NodeClient, NodeOptions } from '@sentry/node';
3+
import type { Breadcrumb, Transaction } from '@sentry/types';
34

45
export type NodeExperimentalOptions = NodeOptions;
56
export type NodeExperimentalClientOptions = ConstructorParameters<typeof NodeClient>[0];
@@ -8,3 +9,18 @@ export interface NodeExperimentalClient extends NodeClient {
89
tracer: Tracer;
910
getOptions(): NodeExperimentalClientOptions;
1011
}
12+
13+
/**
14+
* This is a fork of the base Transaction with OTEL specific stuff added.
15+
* Note that we do not solve this via an actual subclass, but by wrapping this in a proxy when we need it -
16+
* as we can't easily control all the places a transaction may be created.
17+
*/
18+
export interface TransactionWithBreadcrumbs extends Transaction {
19+
_breadcrumbs: Breadcrumb[];
20+
21+
/** Get all breadcrumbs added to this transaction. */
22+
getBreadcrumbs(): Breadcrumb[];
23+
24+
/** Add a breadcrumb to this transaction. */
25+
addBreadcrumb(breadcrumb: Breadcrumb, maxBreadcrumbs?: number): void;
26+
}

0 commit comments

Comments
 (0)