-
-
Notifications
You must be signed in to change notification settings - Fork 1.5k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(node-experimental): Keep breadcrumbs on transaction (#8967)
- Loading branch information
Showing
9 changed files
with
314 additions
and
8 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,140 @@ | ||
import type { Carrier, Scope } from '@sentry/core'; | ||
import { Hub } from '@sentry/core'; | ||
import type { Client } from '@sentry/types'; | ||
import { getGlobalSingleton, GLOBAL_OBJ } from '@sentry/utils'; | ||
|
||
import { OtelScope } from './scope'; | ||
|
||
/** A custom hub that ensures we always creat an OTEL scope. */ | ||
|
||
class OtelHub extends Hub { | ||
public constructor(client?: Client, scope: Scope = new OtelScope()) { | ||
super(client, scope); | ||
} | ||
|
||
/** | ||
* @inheritDoc | ||
*/ | ||
public pushScope(): Scope { | ||
// We want to clone the content of prev scope | ||
const scope = OtelScope.clone(this.getScope()); | ||
this.getStack().push({ | ||
client: this.getClient(), | ||
scope, | ||
}); | ||
return scope; | ||
} | ||
} | ||
|
||
/** | ||
* ******************************************************************************* | ||
* Everything below here is a copy of the stuff from core's hub.ts, | ||
* only that we make sure to create our custom OtelScope instead of the default Scope. | ||
* This is necessary to get the correct breadcrumbs behavior. | ||
* | ||
* Basically, this overwrites all places that do `new Scope()` with `new OtelScope()`. | ||
* Which in turn means overwriting all places that do `new Hub()` and make sure to pass in a OtelScope instead. | ||
* ******************************************************************************* | ||
*/ | ||
|
||
/** | ||
* API compatibility version of this hub. | ||
* | ||
* WARNING: This number should only be increased when the global interface | ||
* changes and new methods are introduced. | ||
* | ||
* @hidden | ||
*/ | ||
const API_VERSION = 4; | ||
|
||
/** | ||
* Returns the default hub instance. | ||
* | ||
* If a hub is already registered in the global carrier but this module | ||
* contains a more recent version, it replaces the registered version. | ||
* Otherwise, the currently registered hub will be returned. | ||
*/ | ||
export function getCurrentHub(): Hub { | ||
// Get main carrier (global for every environment) | ||
const registry = getMainCarrier(); | ||
|
||
if (registry.__SENTRY__ && registry.__SENTRY__.acs) { | ||
const hub = registry.__SENTRY__.acs.getCurrentHub(); | ||
|
||
if (hub) { | ||
return hub; | ||
} | ||
} | ||
|
||
// Return hub that lives on a global object | ||
return getGlobalHub(registry); | ||
} | ||
|
||
/** | ||
* This will create a new {@link Hub} and add to the passed object on | ||
* __SENTRY__.hub. | ||
* @param carrier object | ||
* @hidden | ||
*/ | ||
export function getHubFromCarrier(carrier: Carrier): Hub { | ||
return getGlobalSingleton<Hub>('hub', () => new OtelHub(), carrier); | ||
} | ||
|
||
/** | ||
* @private Private API with no semver guarantees! | ||
* | ||
* If the carrier does not contain a hub, a new hub is created with the global hub client and scope. | ||
*/ | ||
export function ensureHubOnCarrier(carrier: Carrier, parent: Hub = getGlobalHub()): void { | ||
// If there's no hub on current domain, or it's an old API, assign a new one | ||
if (!hasHubOnCarrier(carrier) || getHubFromCarrier(carrier).isOlderThan(API_VERSION)) { | ||
const globalHubTopStack = parent.getStackTop(); | ||
setHubOnCarrier(carrier, new OtelHub(globalHubTopStack.client, OtelScope.clone(globalHubTopStack.scope))); | ||
} | ||
} | ||
|
||
function getGlobalHub(registry: Carrier = getMainCarrier()): Hub { | ||
// If there's no hub, or its an old API, assign a new one | ||
if (!hasHubOnCarrier(registry) || getHubFromCarrier(registry).isOlderThan(API_VERSION)) { | ||
setHubOnCarrier(registry, new OtelHub()); | ||
} | ||
|
||
// Return hub that lives on a global object | ||
return getHubFromCarrier(registry); | ||
} | ||
|
||
/** | ||
* This will tell whether a carrier has a hub on it or not | ||
* @param carrier object | ||
*/ | ||
function hasHubOnCarrier(carrier: Carrier): boolean { | ||
return !!(carrier && carrier.__SENTRY__ && carrier.__SENTRY__.hub); | ||
} | ||
|
||
/** | ||
* Returns the global shim registry. | ||
* | ||
* FIXME: This function is problematic, because despite always returning a valid Carrier, | ||
* it has an optional `__SENTRY__` property, which then in turn requires us to always perform an unnecessary check | ||
* at the call-site. We always access the carrier through this function, so we can guarantee that `__SENTRY__` is there. | ||
**/ | ||
function getMainCarrier(): Carrier { | ||
GLOBAL_OBJ.__SENTRY__ = GLOBAL_OBJ.__SENTRY__ || { | ||
extensions: {}, | ||
hub: undefined, | ||
}; | ||
return GLOBAL_OBJ; | ||
} | ||
|
||
/** | ||
* This will set passed {@link Hub} on the passed object's __SENTRY__.hub attribute | ||
* @param carrier object | ||
* @param hub Hub | ||
* @returns A boolean indicating success or failure | ||
*/ | ||
function setHubOnCarrier(carrier: Carrier, hub: Hub): boolean { | ||
if (!carrier) return false; | ||
const __SENTRY__ = (carrier.__SENTRY__ = carrier.__SENTRY__ || {}); | ||
__SENTRY__.hub = hub; | ||
return true; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,138 @@ | ||
import { Scope } from '@sentry/core'; | ||
import type { Breadcrumb, Transaction } from '@sentry/types'; | ||
import { dateTimestampInSeconds } from '@sentry/utils'; | ||
|
||
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 { | ||
/** | ||
* @inheritDoc | ||
*/ | ||
public static clone(scope?: Scope): Scope { | ||
const newScope = new OtelScope(); | ||
if (scope) { | ||
newScope._breadcrumbs = [...scope['_breadcrumbs']]; | ||
newScope._tags = { ...scope['_tags'] }; | ||
newScope._extra = { ...scope['_extra'] }; | ||
newScope._contexts = { ...scope['_contexts'] }; | ||
newScope._user = scope['_user']; | ||
newScope._level = scope['_level']; | ||
newScope._span = scope['_span']; | ||
newScope._session = scope['_session']; | ||
newScope._transactionName = scope['_transactionName']; | ||
newScope._fingerprint = scope['_fingerprint']; | ||
newScope._eventProcessors = [...scope['_eventProcessors']]; | ||
newScope._requestSession = scope['_requestSession']; | ||
newScope._attachments = [...scope['_attachments']]; | ||
newScope._sdkProcessingMetadata = { ...scope['_sdkProcessingMetadata'] }; | ||
newScope._propagationContext = { ...scope['_propagationContext'] }; | ||
} | ||
return newScope; | ||
} | ||
|
||
/** | ||
* @inheritDoc | ||
*/ | ||
public addBreadcrumb(breadcrumb: Breadcrumb, maxBreadcrumbs?: number): this { | ||
const transaction = getActiveTransaction(); | ||
|
||
if (transaction) { | ||
transaction.addBreadcrumb(breadcrumb, maxBreadcrumbs); | ||
return this; | ||
} | ||
|
||
return super.addBreadcrumb(breadcrumb, maxBreadcrumbs); | ||
} | ||
|
||
/** | ||
* @inheritDoc | ||
*/ | ||
protected _getBreadcrumbs(): Breadcrumb[] { | ||
const transaction = getActiveTransaction(); | ||
const transactionBreadcrumbs = transaction ? transaction.getBreadcrumbs() : []; | ||
|
||
return this._breadcrumbs.concat(transactionBreadcrumbs); | ||
} | ||
} | ||
|
||
/** | ||
* This gets the currently active transaction, | ||
* and ensures to wrap it so that we can store breadcrumbs on it. | ||
*/ | ||
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; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,10 @@ | ||
import type { Tracer } from '@opentelemetry/api'; | ||
import type { NodeClient, NodeOptions } from '@sentry/node'; | ||
|
||
export type NodeExperimentalOptions = NodeOptions; | ||
export type NodeExperimentalClientOptions = ConstructorParameters<typeof NodeClient>[0]; | ||
|
||
export interface NodeExperimentalClient extends NodeClient { | ||
tracer: Tracer; | ||
getOptions(): NodeExperimentalClientOptions; | ||
} |