-
-
Notifications
You must be signed in to change notification settings - Fork 1.5k
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
feat(node-experimental): Keep breadcrumbs on transaction #8967
Merged
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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; | ||
} |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
l: we can proxy patch
startTransaction
(hub extension) so that we don't have to rely ongetActiveTransaction
being used all the time.Can skip this for now just to get it merged.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good idea, wrote something up here: #9010