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
3 changes: 1 addition & 2 deletions packages/node-experimental/src/index.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -12,6 +12,7 @@ export { INTEGRATIONS as Integrations };
export { getAutoPerformanceIntegrations } from './integrations/getAutoPerformanceIntegrations';
export * as Handlers from './sdk/handlers';
export * from './sdk/trace';
export { getCurrentHub, getHubFromCarrier } from './sdk/hub';

export {
makeNodeTransport,
Expand All@@ -33,8 +34,6 @@ export {
extractTraceparentData,
flush,
getActiveTransaction,
getHubFromCarrier,
getCurrentHub,
Hub,
lastEventId,
makeMain,
Expand Down
2 changes: 1 addition & 1 deletion packages/node-experimental/src/integrations/http.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -10,7 +10,7 @@ import { _INTERNAL_getSentrySpan } from '@sentry/opentelemetry-node';
import type { EventProcessor, Hub, Integration } from '@sentry/types';
import type { ClientRequest, IncomingMessage, ServerResponse } from 'http';

import type { NodeExperimentalClient } from '../sdk/client';
import type { NodeExperimentalClient } from '../types';
import { getRequestSpanData } from '../utils/getRequestSpanData';

interface TracingOptions {
Expand Down
26 changes: 24 additions & 2 deletions packages/node-experimental/src/sdk/client.ts
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,19 @@
import type { Tracer } from '@opentelemetry/api';
import { trace } from '@opentelemetry/api';
import type { EventHint, Scope } from '@sentry/node';
import { NodeClient, SDK_VERSION } from '@sentry/node';
import type { Event } from '@sentry/types';

import type { NodeExperimentalClientOptions } from '../types';
import type {
NodeExperimentalClient as NodeExperimentalClientInterface,
NodeExperimentalClientOptions,
} from '../types';
import { OtelScope } from './scope';

/**
* A client built on top of the NodeClient, which provides some otel-specific things on top.
*/
export class NodeExperimentalClient extends NodeClient {
export class NodeExperimentalClient extends NodeClient implements NodeExperimentalClientInterface {
private _tracer: Tracer | undefined;

public constructor(options: ConstructorParameters<typeof NodeClient>[0]) {
Expand DownExpand Up@@ -47,4 +53,20 @@ export class NodeExperimentalClient extends NodeClient {
// Just a type-cast, basically
return super.getOptions();
}

/**
* Extends the base `_prepareEvent` so that we can properly handle `captureContext`.
* This uses `Scope.clone()`, which we need to replace with `OtelScope.clone()` for this client.
*/
protected _prepareEvent(event: Event, hint: EventHint, scope?: Scope): PromiseLike<Event | null> {
let actualScope = scope;

// Remove `captureContext` hint and instead clone already here
if (hint && hint.captureContext) {
actualScope = OtelScope.clone(scope);
delete hint.captureContext;
}

return super._prepareEvent(event, hint, actualScope);
}
}
140 changes: 140 additions & 0 deletions packages/node-experimental/src/sdk/hub.ts
Original file line numberDiff line numberDiff 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;
}
2 changes: 1 addition & 1 deletion packages/node-experimental/src/sdk/initOtel.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,7 +4,7 @@ import { getCurrentHub } from '@sentry/core';
import { SentryPropagator, SentrySpanProcessor } from '@sentry/opentelemetry-node';
import { logger } from '@sentry/utils';

import type { NodeExperimentalClient } from './client';
import type { NodeExperimentalClient } from '../types';
import { SentryContextManager } from './otelContextManager';

/**
Expand Down
3 changes: 2 additions & 1 deletion packages/node-experimental/src/sdk/otelContextManager.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -2,7 +2,8 @@ import type { Context } from '@opentelemetry/api';
import * as api from '@opentelemetry/api';
import { AsyncLocalStorageContextManager } from '@opentelemetry/context-async-hooks';
import type { Carrier, Hub } from '@sentry/core';
import { ensureHubOnCarrier, getCurrentHub, getHubFromCarrier } from '@sentry/core';

import { ensureHubOnCarrier, getCurrentHub, getHubFromCarrier } from './hub';

export const OTEL_CONTEXT_HUB_KEY = api.createContextKey('sentry_hub');

Expand Down
138 changes: 138 additions & 0 deletions packages/node-experimental/src/sdk/scope.ts
Original file line numberDiff line numberDiff 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, {

Copy link
Copy Markdown
Contributor

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 on getActiveTransaction being used all the time.

Can skip this for now just to get it merged.

Copy link
Copy Markdown
MemberAuthor

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

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;
}
2 changes: 1 addition & 1 deletion packages/node-experimental/src/sdk/trace.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,7 +5,7 @@ import { _INTERNAL_getSentrySpan } from '@sentry/opentelemetry-node';
import type { Span, TransactionContext } from '@sentry/types';
import { isThenable } from '@sentry/utils';

import type { NodeExperimentalClient } from './client';
import type { NodeExperimentalClient } from '../types';

/**
* Wraps a function with a transaction/span and finishes the span after the function is done.
Expand Down
6 changes: 6 additions & 0 deletions packages/node-experimental/src/types.ts
Original file line numberDiff line numberDiff 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;
}
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content
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
3 changes: 1 addition & 2 deletions packages/node-experimental/src/index.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -12,6 +12,7 @@ export { INTEGRATIONS as Integrations };
export { getAutoPerformanceIntegrations } from './integrations/getAutoPerformanceIntegrations';
export * as Handlers from './sdk/handlers';
export * from './sdk/trace';
export { getCurrentHub, getHubFromCarrier } from './sdk/hub';

export {
makeNodeTransport,
Expand All@@ -33,8 +34,6 @@ export {
extractTraceparentData,
flush,
getActiveTransaction,
getHubFromCarrier,
getCurrentHub,
Hub,
lastEventId,
makeMain,
Expand Down
2 changes: 1 addition & 1 deletion packages/node-experimental/src/integrations/http.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -10,7 +10,7 @@ import { _INTERNAL_getSentrySpan } from '@sentry/opentelemetry-node';
import type { EventProcessor, Hub, Integration } from '@sentry/types';
import type { ClientRequest, IncomingMessage, ServerResponse } from 'http';

import type { NodeExperimentalClient } from '../sdk/client';
import type { NodeExperimentalClient } from '../types';
import { getRequestSpanData } from '../utils/getRequestSpanData';

interface TracingOptions {
Expand Down
26 changes: 24 additions & 2 deletions packages/node-experimental/src/sdk/client.ts
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,19 @@
import type { Tracer } from '@opentelemetry/api';
import { trace } from '@opentelemetry/api';
import type { EventHint, Scope } from '@sentry/node';
import { NodeClient, SDK_VERSION } from '@sentry/node';
import type { Event } from '@sentry/types';

import type { NodeExperimentalClientOptions } from '../types';
import type {
NodeExperimentalClient as NodeExperimentalClientInterface,
NodeExperimentalClientOptions,
} from '../types';
import { OtelScope } from './scope';

/**
* A client built on top of the NodeClient, which provides some otel-specific things on top.
*/
export class NodeExperimentalClient extends NodeClient {
export class NodeExperimentalClient extends NodeClient implements NodeExperimentalClientInterface {
private _tracer: Tracer | undefined;

public constructor(options: ConstructorParameters<typeof NodeClient>[0]) {
Expand DownExpand Up@@ -47,4 +53,20 @@ export class NodeExperimentalClient extends NodeClient {
// Just a type-cast, basically
return super.getOptions();
}

/**
* Extends the base `_prepareEvent` so that we can properly handle `captureContext`.
* This uses `Scope.clone()`, which we need to replace with `OtelScope.clone()` for this client.
*/
protected _prepareEvent(event: Event, hint: EventHint, scope?: Scope): PromiseLike<Event | null> {
let actualScope = scope;

// Remove `captureContext` hint and instead clone already here
if (hint && hint.captureContext) {
actualScope = OtelScope.clone(scope);
delete hint.captureContext;
}

return super._prepareEvent(event, hint, actualScope);
}
}
140 changes: 140 additions & 0 deletions packages/node-experimental/src/sdk/hub.ts
Original file line numberDiff line numberDiff 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;
}
2 changes: 1 addition & 1 deletion packages/node-experimental/src/sdk/initOtel.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,7 +4,7 @@ import { getCurrentHub } from '@sentry/core';
import { SentryPropagator, SentrySpanProcessor } from '@sentry/opentelemetry-node';
import { logger } from '@sentry/utils';

import type { NodeExperimentalClient } from './client';
import type { NodeExperimentalClient } from '../types';
import { SentryContextManager } from './otelContextManager';

/**
Expand Down
3 changes: 2 additions & 1 deletion packages/node-experimental/src/sdk/otelContextManager.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -2,7 +2,8 @@ import type { Context } from '@opentelemetry/api';
import * as api from '@opentelemetry/api';
import { AsyncLocalStorageContextManager } from '@opentelemetry/context-async-hooks';
import type { Carrier, Hub } from '@sentry/core';
import { ensureHubOnCarrier, getCurrentHub, getHubFromCarrier } from '@sentry/core';

import { ensureHubOnCarrier, getCurrentHub, getHubFromCarrier } from './hub';

export const OTEL_CONTEXT_HUB_KEY = api.createContextKey('sentry_hub');

Expand Down
138 changes: 138 additions & 0 deletions packages/node-experimental/src/sdk/scope.ts
Original file line numberDiff line numberDiff 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, {

Copy link
Copy Markdown
Contributor

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 on getActiveTransaction being used all the time.

Can skip this for now just to get it merged.

Copy link
Copy Markdown
MemberAuthor

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

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;
}
2 changes: 1 addition & 1 deletion packages/node-experimental/src/sdk/trace.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,7 +5,7 @@ import { _INTERNAL_getSentrySpan } from '@sentry/opentelemetry-node';
import type { Span, TransactionContext } from '@sentry/types';
import { isThenable } from '@sentry/utils';

import type { NodeExperimentalClient } from './client';
import type { NodeExperimentalClient } from '../types';

/**
* Wraps a function with a transaction/span and finishes the span after the function is done.
Expand Down
6 changes: 6 additions & 0 deletions packages/node-experimental/src/types.ts
Original file line numberDiff line numberDiff 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;
}
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
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
3 changes: 1 addition & 2 deletions packages/node-experimental/src/index.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -12,6 +12,7 @@ export { INTEGRATIONS as Integrations };
export { getAutoPerformanceIntegrations } from './integrations/getAutoPerformanceIntegrations';
export * as Handlers from './sdk/handlers';
export * from './sdk/trace';
export { getCurrentHub, getHubFromCarrier } from './sdk/hub';

export {
makeNodeTransport,
Expand All@@ -33,8 +34,6 @@ export {
extractTraceparentData,
flush,
getActiveTransaction,
getHubFromCarrier,
getCurrentHub,
Hub,
lastEventId,
makeMain,
Expand Down
2 changes: 1 addition & 1 deletion packages/node-experimental/src/integrations/http.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -10,7 +10,7 @@ import { _INTERNAL_getSentrySpan } from '@sentry/opentelemetry-node';
import type { EventProcessor, Hub, Integration } from '@sentry/types';
import type { ClientRequest, IncomingMessage, ServerResponse } from 'http';

import type { NodeExperimentalClient } from '../sdk/client';
import type { NodeExperimentalClient } from '../types';
import { getRequestSpanData } from '../utils/getRequestSpanData';

interface TracingOptions {
Expand Down
26 changes: 24 additions & 2 deletions packages/node-experimental/src/sdk/client.ts
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,19 @@
import type { Tracer } from '@opentelemetry/api';
import { trace } from '@opentelemetry/api';
import type { EventHint, Scope } from '@sentry/node';
import { NodeClient, SDK_VERSION } from '@sentry/node';
import type { Event } from '@sentry/types';

import type { NodeExperimentalClientOptions } from '../types';
import type {
NodeExperimentalClient as NodeExperimentalClientInterface,
NodeExperimentalClientOptions,
} from '../types';
import { OtelScope } from './scope';

/**
* A client built on top of the NodeClient, which provides some otel-specific things on top.
*/
export class NodeExperimentalClient extends NodeClient {
export class NodeExperimentalClient extends NodeClient implements NodeExperimentalClientInterface {
private _tracer: Tracer | undefined;

public constructor(options: ConstructorParameters<typeof NodeClient>[0]) {
Expand DownExpand Up@@ -47,4 +53,20 @@ export class NodeExperimentalClient extends NodeClient {
// Just a type-cast, basically
return super.getOptions();
}

/**
* Extends the base `_prepareEvent` so that we can properly handle `captureContext`.
* This uses `Scope.clone()`, which we need to replace with `OtelScope.clone()` for this client.
*/
protected _prepareEvent(event: Event, hint: EventHint, scope?: Scope): PromiseLike<Event | null> {
let actualScope = scope;

// Remove `captureContext` hint and instead clone already here
if (hint && hint.captureContext) {
actualScope = OtelScope.clone(scope);
delete hint.captureContext;
}

return super._prepareEvent(event, hint, actualScope);
}
}
140 changes: 140 additions & 0 deletions packages/node-experimental/src/sdk/hub.ts
Original file line numberDiff line numberDiff 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;
}
2 changes: 1 addition & 1 deletion packages/node-experimental/src/sdk/initOtel.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,7 +4,7 @@ import { getCurrentHub } from '@sentry/core';
import { SentryPropagator, SentrySpanProcessor } from '@sentry/opentelemetry-node';
import { logger } from '@sentry/utils';

import type { NodeExperimentalClient } from './client';
import type { NodeExperimentalClient } from '../types';
import { SentryContextManager } from './otelContextManager';

/**
Expand Down
3 changes: 2 additions & 1 deletion packages/node-experimental/src/sdk/otelContextManager.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -2,7 +2,8 @@ import type { Context } from '@opentelemetry/api';
import * as api from '@opentelemetry/api';
import { AsyncLocalStorageContextManager } from '@opentelemetry/context-async-hooks';
import type { Carrier, Hub } from '@sentry/core';
import { ensureHubOnCarrier, getCurrentHub, getHubFromCarrier } from '@sentry/core';

import { ensureHubOnCarrier, getCurrentHub, getHubFromCarrier } from './hub';

export const OTEL_CONTEXT_HUB_KEY = api.createContextKey('sentry_hub');

Expand Down
138 changes: 138 additions & 0 deletions packages/node-experimental/src/sdk/scope.ts
Original file line numberDiff line numberDiff 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, {

Copy link
Copy Markdown
Contributor

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 on getActiveTransaction being used all the time.

Can skip this for now just to get it merged.

Copy link
Copy Markdown
MemberAuthor

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

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;
}
2 changes: 1 addition & 1 deletion packages/node-experimental/src/sdk/trace.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,7 +5,7 @@ import { _INTERNAL_getSentrySpan } from '@sentry/opentelemetry-node';
import type { Span, TransactionContext } from '@sentry/types';
import { isThenable } from '@sentry/utils';

import type { NodeExperimentalClient } from './client';
import type { NodeExperimentalClient } from '../types';

/**
* Wraps a function with a transaction/span and finishes the span after the function is done.
Expand Down
6 changes: 6 additions & 0 deletions packages/node-experimental/src/types.ts
Original file line numberDiff line numberDiff 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;
}
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
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
3 changes: 1 addition & 2 deletions packages/node-experimental/src/index.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -12,6 +12,7 @@ export { INTEGRATIONS as Integrations };
export { getAutoPerformanceIntegrations } from './integrations/getAutoPerformanceIntegrations';
export * as Handlers from './sdk/handlers';
export * from './sdk/trace';
export { getCurrentHub, getHubFromCarrier } from './sdk/hub';

export {
makeNodeTransport,
Expand All@@ -33,8 +34,6 @@ export {
extractTraceparentData,
flush,
getActiveTransaction,
getHubFromCarrier,
getCurrentHub,
Hub,
lastEventId,
makeMain,
Expand Down
2 changes: 1 addition & 1 deletion packages/node-experimental/src/integrations/http.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -10,7 +10,7 @@ import { _INTERNAL_getSentrySpan } from '@sentry/opentelemetry-node';
import type { EventProcessor, Hub, Integration } from '@sentry/types';
import type { ClientRequest, IncomingMessage, ServerResponse } from 'http';

import type { NodeExperimentalClient } from '../sdk/client';
import type { NodeExperimentalClient } from '../types';
import { getRequestSpanData } from '../utils/getRequestSpanData';

interface TracingOptions {
Expand Down
26 changes: 24 additions & 2 deletions packages/node-experimental/src/sdk/client.ts
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,19 @@
import type { Tracer } from '@opentelemetry/api';
import { trace } from '@opentelemetry/api';
import type { EventHint, Scope } from '@sentry/node';
import { NodeClient, SDK_VERSION } from '@sentry/node';
import type { Event } from '@sentry/types';

import type { NodeExperimentalClientOptions } from '../types';
import type {
NodeExperimentalClient as NodeExperimentalClientInterface,
NodeExperimentalClientOptions,
} from '../types';
import { OtelScope } from './scope';

/**
* A client built on top of the NodeClient, which provides some otel-specific things on top.
*/
export class NodeExperimentalClient extends NodeClient {
export class NodeExperimentalClient extends NodeClient implements NodeExperimentalClientInterface {
private _tracer: Tracer | undefined;

public constructor(options: ConstructorParameters<typeof NodeClient>[0]) {
Expand DownExpand Up@@ -47,4 +53,20 @@ export class NodeExperimentalClient extends NodeClient {
// Just a type-cast, basically
return super.getOptions();
}

/**
* Extends the base `_prepareEvent` so that we can properly handle `captureContext`.
* This uses `Scope.clone()`, which we need to replace with `OtelScope.clone()` for this client.
*/
protected _prepareEvent(event: Event, hint: EventHint, scope?: Scope): PromiseLike<Event | null> {
let actualScope = scope;

// Remove `captureContext` hint and instead clone already here
if (hint && hint.captureContext) {
actualScope = OtelScope.clone(scope);
delete hint.captureContext;
}

return super._prepareEvent(event, hint, actualScope);
}
}
140 changes: 140 additions & 0 deletions packages/node-experimental/src/sdk/hub.ts
Original file line numberDiff line numberDiff 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;
}
2 changes: 1 addition & 1 deletion packages/node-experimental/src/sdk/initOtel.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,7 +4,7 @@ import { getCurrentHub } from '@sentry/core';
import { SentryPropagator, SentrySpanProcessor } from '@sentry/opentelemetry-node';
import { logger } from '@sentry/utils';

import type { NodeExperimentalClient } from './client';
import type { NodeExperimentalClient } from '../types';
import { SentryContextManager } from './otelContextManager';

/**
Expand Down
3 changes: 2 additions & 1 deletion packages/node-experimental/src/sdk/otelContextManager.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -2,7 +2,8 @@ import type { Context } from '@opentelemetry/api';
import * as api from '@opentelemetry/api';
import { AsyncLocalStorageContextManager } from '@opentelemetry/context-async-hooks';
import type { Carrier, Hub } from '@sentry/core';
import { ensureHubOnCarrier, getCurrentHub, getHubFromCarrier } from '@sentry/core';

import { ensureHubOnCarrier, getCurrentHub, getHubFromCarrier } from './hub';

export const OTEL_CONTEXT_HUB_KEY = api.createContextKey('sentry_hub');

Expand Down
138 changes: 138 additions & 0 deletions packages/node-experimental/src/sdk/scope.ts
Original file line numberDiff line numberDiff 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, {

Copy link
Copy Markdown
Contributor

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 on getActiveTransaction being used all the time.

Can skip this for now just to get it merged.

Copy link
Copy Markdown
MemberAuthor

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

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;
}
2 changes: 1 addition & 1 deletion packages/node-experimental/src/sdk/trace.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,7 +5,7 @@ import { _INTERNAL_getSentrySpan } from '@sentry/opentelemetry-node';
import type { Span, TransactionContext } from '@sentry/types';
import { isThenable } from '@sentry/utils';

import type { NodeExperimentalClient } from './client';
import type { NodeExperimentalClient } from '../types';

/**
* Wraps a function with a transaction/span and finishes the span after the function is done.
Expand Down
6 changes: 6 additions & 0 deletions packages/node-experimental/src/types.ts
Original file line numberDiff line numberDiff 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;
}
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content
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
3 changes: 1 addition & 2 deletions packages/node-experimental/src/index.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -12,6 +12,7 @@ export { INTEGRATIONS as Integrations };
export { getAutoPerformanceIntegrations } from './integrations/getAutoPerformanceIntegrations';
export * as Handlers from './sdk/handlers';
export * from './sdk/trace';
export { getCurrentHub, getHubFromCarrier } from './sdk/hub';

export {
makeNodeTransport,
Expand All@@ -33,8 +34,6 @@ export {
extractTraceparentData,
flush,
getActiveTransaction,
getHubFromCarrier,
getCurrentHub,
Hub,
lastEventId,
makeMain,
Expand Down
2 changes: 1 addition & 1 deletion packages/node-experimental/src/integrations/http.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -10,7 +10,7 @@ import { _INTERNAL_getSentrySpan } from '@sentry/opentelemetry-node';
import type { EventProcessor, Hub, Integration } from '@sentry/types';
import type { ClientRequest, IncomingMessage, ServerResponse } from 'http';

import type { NodeExperimentalClient } from '../sdk/client';
import type { NodeExperimentalClient } from '../types';
import { getRequestSpanData } from '../utils/getRequestSpanData';

interface TracingOptions {
Expand Down
26 changes: 24 additions & 2 deletions packages/node-experimental/src/sdk/client.ts
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,19 @@
import type { Tracer } from '@opentelemetry/api';
import { trace } from '@opentelemetry/api';
import type { EventHint, Scope } from '@sentry/node';
import { NodeClient, SDK_VERSION } from '@sentry/node';
import type { Event } from '@sentry/types';

import type { NodeExperimentalClientOptions } from '../types';
import type {
NodeExperimentalClient as NodeExperimentalClientInterface,
NodeExperimentalClientOptions,
} from '../types';
import { OtelScope } from './scope';

/**
* A client built on top of the NodeClient, which provides some otel-specific things on top.
*/
export class NodeExperimentalClient extends NodeClient {
export class NodeExperimentalClient extends NodeClient implements NodeExperimentalClientInterface {
private _tracer: Tracer | undefined;

public constructor(options: ConstructorParameters<typeof NodeClient>[0]) {
Expand DownExpand Up@@ -47,4 +53,20 @@ export class NodeExperimentalClient extends NodeClient {
// Just a type-cast, basically
return super.getOptions();
}

/**
* Extends the base `_prepareEvent` so that we can properly handle `captureContext`.
* This uses `Scope.clone()`, which we need to replace with `OtelScope.clone()` for this client.
*/
protected _prepareEvent(event: Event, hint: EventHint, scope?: Scope): PromiseLike<Event | null> {
let actualScope = scope;

// Remove `captureContext` hint and instead clone already here
if (hint && hint.captureContext) {
actualScope = OtelScope.clone(scope);
delete hint.captureContext;
}

return super._prepareEvent(event, hint, actualScope);
}
}
140 changes: 140 additions & 0 deletions packages/node-experimental/src/sdk/hub.ts
Original file line numberDiff line numberDiff 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;
}
2 changes: 1 addition & 1 deletion packages/node-experimental/src/sdk/initOtel.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,7 +4,7 @@ import { getCurrentHub } from '@sentry/core';
import { SentryPropagator, SentrySpanProcessor } from '@sentry/opentelemetry-node';
import { logger } from '@sentry/utils';

import type { NodeExperimentalClient } from './client';
import type { NodeExperimentalClient } from '../types';
import { SentryContextManager } from './otelContextManager';

/**
Expand Down
3 changes: 2 additions & 1 deletion packages/node-experimental/src/sdk/otelContextManager.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -2,7 +2,8 @@ import type { Context } from '@opentelemetry/api';
import * as api from '@opentelemetry/api';
import { AsyncLocalStorageContextManager } from '@opentelemetry/context-async-hooks';
import type { Carrier, Hub } from '@sentry/core';
import { ensureHubOnCarrier, getCurrentHub, getHubFromCarrier } from '@sentry/core';

import { ensureHubOnCarrier, getCurrentHub, getHubFromCarrier } from './hub';

export const OTEL_CONTEXT_HUB_KEY = api.createContextKey('sentry_hub');

Expand Down
138 changes: 138 additions & 0 deletions packages/node-experimental/src/sdk/scope.ts
Original file line numberDiff line numberDiff 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, {

Copy link
Copy Markdown
Contributor

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 on getActiveTransaction being used all the time.

Can skip this for now just to get it merged.

Copy link
Copy Markdown
MemberAuthor

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

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;
}
2 changes: 1 addition & 1 deletion packages/node-experimental/src/sdk/trace.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,7 +5,7 @@ import { _INTERNAL_getSentrySpan } from '@sentry/opentelemetry-node';
import type { Span, TransactionContext } from '@sentry/types';
import { isThenable } from '@sentry/utils';

import type { NodeExperimentalClient } from './client';
import type { NodeExperimentalClient } from '../types';

/**
* Wraps a function with a transaction/span and finishes the span after the function is done.
Expand Down
6 changes: 6 additions & 0 deletions packages/node-experimental/src/types.ts
Original file line numberDiff line numberDiff 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;
}
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
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
3 changes: 1 addition & 2 deletions packages/node-experimental/src/index.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -12,6 +12,7 @@ export { INTEGRATIONS as Integrations };
export { getAutoPerformanceIntegrations } from './integrations/getAutoPerformanceIntegrations';
export * as Handlers from './sdk/handlers';
export * from './sdk/trace';
export { getCurrentHub, getHubFromCarrier } from './sdk/hub';

export {
makeNodeTransport,
Expand All@@ -33,8 +34,6 @@ export {
extractTraceparentData,
flush,
getActiveTransaction,
getHubFromCarrier,
getCurrentHub,
Hub,
lastEventId,
makeMain,
Expand Down
2 changes: 1 addition & 1 deletion packages/node-experimental/src/integrations/http.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -10,7 +10,7 @@ import { _INTERNAL_getSentrySpan } from '@sentry/opentelemetry-node';
import type { EventProcessor, Hub, Integration } from '@sentry/types';
import type { ClientRequest, IncomingMessage, ServerResponse } from 'http';

import type { NodeExperimentalClient } from '../sdk/client';
import type { NodeExperimentalClient } from '../types';
import { getRequestSpanData } from '../utils/getRequestSpanData';

interface TracingOptions {
Expand Down
26 changes: 24 additions & 2 deletions packages/node-experimental/src/sdk/client.ts
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,19 @@
import type { Tracer } from '@opentelemetry/api';
import { trace } from '@opentelemetry/api';
import type { EventHint, Scope } from '@sentry/node';
import { NodeClient, SDK_VERSION } from '@sentry/node';
import type { Event } from '@sentry/types';

import type { NodeExperimentalClientOptions } from '../types';
import type {
NodeExperimentalClient as NodeExperimentalClientInterface,
NodeExperimentalClientOptions,
} from '../types';
import { OtelScope } from './scope';

/**
* A client built on top of the NodeClient, which provides some otel-specific things on top.
*/
export class NodeExperimentalClient extends NodeClient {
export class NodeExperimentalClient extends NodeClient implements NodeExperimentalClientInterface {
private _tracer: Tracer | undefined;

public constructor(options: ConstructorParameters<typeof NodeClient>[0]) {
Expand DownExpand Up@@ -47,4 +53,20 @@ export class NodeExperimentalClient extends NodeClient {
// Just a type-cast, basically
return super.getOptions();
}

/**
* Extends the base `_prepareEvent` so that we can properly handle `captureContext`.
* This uses `Scope.clone()`, which we need to replace with `OtelScope.clone()` for this client.
*/
protected _prepareEvent(event: Event, hint: EventHint, scope?: Scope): PromiseLike<Event | null> {
let actualScope = scope;

// Remove `captureContext` hint and instead clone already here
if (hint && hint.captureContext) {
actualScope = OtelScope.clone(scope);
delete hint.captureContext;
}

return super._prepareEvent(event, hint, actualScope);
}
}
140 changes: 140 additions & 0 deletions packages/node-experimental/src/sdk/hub.ts
Original file line numberDiff line numberDiff 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;
}
2 changes: 1 addition & 1 deletion packages/node-experimental/src/sdk/initOtel.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,7 +4,7 @@ import { getCurrentHub } from '@sentry/core';
import { SentryPropagator, SentrySpanProcessor } from '@sentry/opentelemetry-node';
import { logger } from '@sentry/utils';

import type { NodeExperimentalClient } from './client';
import type { NodeExperimentalClient } from '../types';
import { SentryContextManager } from './otelContextManager';

/**
Expand Down
3 changes: 2 additions & 1 deletion packages/node-experimental/src/sdk/otelContextManager.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -2,7 +2,8 @@ import type { Context } from '@opentelemetry/api';
import * as api from '@opentelemetry/api';
import { AsyncLocalStorageContextManager } from '@opentelemetry/context-async-hooks';
import type { Carrier, Hub } from '@sentry/core';
import { ensureHubOnCarrier, getCurrentHub, getHubFromCarrier } from '@sentry/core';

import { ensureHubOnCarrier, getCurrentHub, getHubFromCarrier } from './hub';

export const OTEL_CONTEXT_HUB_KEY = api.createContextKey('sentry_hub');

Expand Down
138 changes: 138 additions & 0 deletions packages/node-experimental/src/sdk/scope.ts
Original file line numberDiff line numberDiff 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, {

Copy link
Copy Markdown
Contributor

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 on getActiveTransaction being used all the time.

Can skip this for now just to get it merged.

Copy link
Copy Markdown
MemberAuthor

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

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;
}
2 changes: 1 addition & 1 deletion packages/node-experimental/src/sdk/trace.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,7 +5,7 @@ import { _INTERNAL_getSentrySpan } from '@sentry/opentelemetry-node';
import type { Span, TransactionContext } from '@sentry/types';
import { isThenable } from '@sentry/utils';

import type { NodeExperimentalClient } from './client';
import type { NodeExperimentalClient } from '../types';

/**
* Wraps a function with a transaction/span and finishes the span after the function is done.
Expand Down
6 changes: 6 additions & 0 deletions packages/node-experimental/src/types.ts
Original file line numberDiff line numberDiff 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;
}
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
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
3 changes: 1 addition & 2 deletions packages/node-experimental/src/index.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -12,6 +12,7 @@ export { INTEGRATIONS as Integrations };
export { getAutoPerformanceIntegrations } from './integrations/getAutoPerformanceIntegrations';
export * as Handlers from './sdk/handlers';
export * from './sdk/trace';
export { getCurrentHub, getHubFromCarrier } from './sdk/hub';

export {
makeNodeTransport,
Expand All@@ -33,8 +34,6 @@ export {
extractTraceparentData,
flush,
getActiveTransaction,
getHubFromCarrier,
getCurrentHub,
Hub,
lastEventId,
makeMain,
Expand Down
2 changes: 1 addition & 1 deletion packages/node-experimental/src/integrations/http.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -10,7 +10,7 @@ import { _INTERNAL_getSentrySpan } from '@sentry/opentelemetry-node';
import type { EventProcessor, Hub, Integration } from '@sentry/types';
import type { ClientRequest, IncomingMessage, ServerResponse } from 'http';

import type { NodeExperimentalClient } from '../sdk/client';
import type { NodeExperimentalClient } from '../types';
import { getRequestSpanData } from '../utils/getRequestSpanData';

interface TracingOptions {
Expand Down
26 changes: 24 additions & 2 deletions packages/node-experimental/src/sdk/client.ts
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,19 @@
import type { Tracer } from '@opentelemetry/api';
import { trace } from '@opentelemetry/api';
import type { EventHint, Scope } from '@sentry/node';
import { NodeClient, SDK_VERSION } from '@sentry/node';
import type { Event } from '@sentry/types';

import type { NodeExperimentalClientOptions } from '../types';
import type {
NodeExperimentalClient as NodeExperimentalClientInterface,
NodeExperimentalClientOptions,
} from '../types';
import { OtelScope } from './scope';

/**
* A client built on top of the NodeClient, which provides some otel-specific things on top.
*/
export class NodeExperimentalClient extends NodeClient {
export class NodeExperimentalClient extends NodeClient implements NodeExperimentalClientInterface {
private _tracer: Tracer | undefined;

public constructor(options: ConstructorParameters<typeof NodeClient>[0]) {
Expand DownExpand Up@@ -47,4 +53,20 @@ export class NodeExperimentalClient extends NodeClient {
// Just a type-cast, basically
return super.getOptions();
}

/**
* Extends the base `_prepareEvent` so that we can properly handle `captureContext`.
* This uses `Scope.clone()`, which we need to replace with `OtelScope.clone()` for this client.
*/
protected _prepareEvent(event: Event, hint: EventHint, scope?: Scope): PromiseLike<Event | null> {
let actualScope = scope;

// Remove `captureContext` hint and instead clone already here
if (hint && hint.captureContext) {
actualScope = OtelScope.clone(scope);
delete hint.captureContext;
}

return super._prepareEvent(event, hint, actualScope);
}
}
140 changes: 140 additions & 0 deletions packages/node-experimental/src/sdk/hub.ts
Original file line numberDiff line numberDiff 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;
}
2 changes: 1 addition & 1 deletion packages/node-experimental/src/sdk/initOtel.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,7 +4,7 @@ import { getCurrentHub } from '@sentry/core';
import { SentryPropagator, SentrySpanProcessor } from '@sentry/opentelemetry-node';
import { logger } from '@sentry/utils';

import type { NodeExperimentalClient } from './client';
import type { NodeExperimentalClient } from '../types';
import { SentryContextManager } from './otelContextManager';

/**
Expand Down
3 changes: 2 additions & 1 deletion packages/node-experimental/src/sdk/otelContextManager.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -2,7 +2,8 @@ import type { Context } from '@opentelemetry/api';
import * as api from '@opentelemetry/api';
import { AsyncLocalStorageContextManager } from '@opentelemetry/context-async-hooks';
import type { Carrier, Hub } from '@sentry/core';
import { ensureHubOnCarrier, getCurrentHub, getHubFromCarrier } from '@sentry/core';

import { ensureHubOnCarrier, getCurrentHub, getHubFromCarrier } from './hub';

export const OTEL_CONTEXT_HUB_KEY = api.createContextKey('sentry_hub');

Expand Down
138 changes: 138 additions & 0 deletions packages/node-experimental/src/sdk/scope.ts
Original file line numberDiff line numberDiff 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, {

Copy link
Copy Markdown
Contributor

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 on getActiveTransaction being used all the time.

Can skip this for now just to get it merged.

Copy link
Copy Markdown
MemberAuthor

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

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;
}
2 changes: 1 addition & 1 deletion packages/node-experimental/src/sdk/trace.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,7 +5,7 @@ import { _INTERNAL_getSentrySpan } from '@sentry/opentelemetry-node';
import type { Span, TransactionContext } from '@sentry/types';
import { isThenable } from '@sentry/utils';

import type { NodeExperimentalClient } from './client';
import type { NodeExperimentalClient } from '../types';

/**
* Wraps a function with a transaction/span and finishes the span after the function is done.
Expand Down
6 changes: 6 additions & 0 deletions packages/node-experimental/src/types.ts
Original file line numberDiff line numberDiff 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;
}
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content
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
3 changes: 1 addition & 2 deletions packages/node-experimental/src/index.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -12,6 +12,7 @@ export { INTEGRATIONS as Integrations };
export { getAutoPerformanceIntegrations } from './integrations/getAutoPerformanceIntegrations';
export * as Handlers from './sdk/handlers';
export * from './sdk/trace';
export { getCurrentHub, getHubFromCarrier } from './sdk/hub';

export {
makeNodeTransport,
Expand All@@ -33,8 +34,6 @@ export {
extractTraceparentData,
flush,
getActiveTransaction,
getHubFromCarrier,
getCurrentHub,
Hub,
lastEventId,
makeMain,
Expand Down
2 changes: 1 addition & 1 deletion packages/node-experimental/src/integrations/http.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -10,7 +10,7 @@ import { _INTERNAL_getSentrySpan } from '@sentry/opentelemetry-node';
import type { EventProcessor, Hub, Integration } from '@sentry/types';
import type { ClientRequest, IncomingMessage, ServerResponse } from 'http';

import type { NodeExperimentalClient } from '../sdk/client';
import type { NodeExperimentalClient } from '../types';
import { getRequestSpanData } from '../utils/getRequestSpanData';

interface TracingOptions {
Expand Down
26 changes: 24 additions & 2 deletions packages/node-experimental/src/sdk/client.ts
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,19 @@
import type { Tracer } from '@opentelemetry/api';
import { trace } from '@opentelemetry/api';
import type { EventHint, Scope } from '@sentry/node';
import { NodeClient, SDK_VERSION } from '@sentry/node';
import type { Event } from '@sentry/types';

import type { NodeExperimentalClientOptions } from '../types';
import type {
NodeExperimentalClient as NodeExperimentalClientInterface,
NodeExperimentalClientOptions,
} from '../types';
import { OtelScope } from './scope';

/**
* A client built on top of the NodeClient, which provides some otel-specific things on top.
*/
export class NodeExperimentalClient extends NodeClient {
export class NodeExperimentalClient extends NodeClient implements NodeExperimentalClientInterface {
private _tracer: Tracer | undefined;

public constructor(options: ConstructorParameters<typeof NodeClient>[0]) {
Expand DownExpand Up@@ -47,4 +53,20 @@ export class NodeExperimentalClient extends NodeClient {
// Just a type-cast, basically
return super.getOptions();
}

/**
* Extends the base `_prepareEvent` so that we can properly handle `captureContext`.
* This uses `Scope.clone()`, which we need to replace with `OtelScope.clone()` for this client.
*/
protected _prepareEvent(event: Event, hint: EventHint, scope?: Scope): PromiseLike<Event | null> {
let actualScope = scope;

// Remove `captureContext` hint and instead clone already here
if (hint && hint.captureContext) {
actualScope = OtelScope.clone(scope);
delete hint.captureContext;
}

return super._prepareEvent(event, hint, actualScope);
}
}
140 changes: 140 additions & 0 deletions packages/node-experimental/src/sdk/hub.ts
Original file line numberDiff line numberDiff 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;
}
2 changes: 1 addition & 1 deletion packages/node-experimental/src/sdk/initOtel.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,7 +4,7 @@ import { getCurrentHub } from '@sentry/core';
import { SentryPropagator, SentrySpanProcessor } from '@sentry/opentelemetry-node';
import { logger } from '@sentry/utils';

import type { NodeExperimentalClient } from './client';
import type { NodeExperimentalClient } from '../types';
import { SentryContextManager } from './otelContextManager';

/**
Expand Down
3 changes: 2 additions & 1 deletion packages/node-experimental/src/sdk/otelContextManager.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -2,7 +2,8 @@ import type { Context } from '@opentelemetry/api';
import * as api from '@opentelemetry/api';
import { AsyncLocalStorageContextManager } from '@opentelemetry/context-async-hooks';
import type { Carrier, Hub } from '@sentry/core';
import { ensureHubOnCarrier, getCurrentHub, getHubFromCarrier } from '@sentry/core';

import { ensureHubOnCarrier, getCurrentHub, getHubFromCarrier } from './hub';

export const OTEL_CONTEXT_HUB_KEY = api.createContextKey('sentry_hub');

Expand Down
138 changes: 138 additions & 0 deletions packages/node-experimental/src/sdk/scope.ts
Original file line numberDiff line numberDiff 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, {

Copy link
Copy Markdown
Contributor

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 on getActiveTransaction being used all the time.

Can skip this for now just to get it merged.

Copy link
Copy Markdown
MemberAuthor

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

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;
}
2 changes: 1 addition & 1 deletion packages/node-experimental/src/sdk/trace.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,7 +5,7 @@ import { _INTERNAL_getSentrySpan } from '@sentry/opentelemetry-node';
import type { Span, TransactionContext } from '@sentry/types';
import { isThenable } from '@sentry/utils';

import type { NodeExperimentalClient } from './client';
import type { NodeExperimentalClient } from '../types';

/**
* Wraps a function with a transaction/span and finishes the span after the function is done.
Expand Down
6 changes: 6 additions & 0 deletions packages/node-experimental/src/types.ts
Original file line numberDiff line numberDiff 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;
}