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
2 changes: 2 additions & 0 deletions packages/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,8 @@ export { getTraceData } from './utils/traceData';
export { shouldPropagateTraceForUrl } from './utils/tracePropagationTargets';
export { getTraceMetaTags } from './utils/meta';
export { debounce } from './utils/debounce';
export { makeWeakRef, derefWeakRef } from './utils/weakRef';
export type { MaybeWeakRef } from './utils/weakRef';
export { shouldIgnoreSpan } from './utils/should-ignore-span';
export {
winterCGHeadersToDict,
Expand Down
44 changes: 4 additions & 40 deletions packages/core/src/tracing/utils.ts
Original file line number Diff line number Diff line change
@@ -1,56 +1,20 @@
import type { Scope } from '../scope';
import type { Span } from '../types-hoist/span';
import { addNonEnumerableProperty } from '../utils/object';
import { GLOBAL_OBJ } from '../utils/worldwide';
import { derefWeakRef, makeWeakRef, type MaybeWeakRef } from '../utils/weakRef';

const SCOPE_ON_START_SPAN_FIELD = '_sentryScope';
const ISOLATION_SCOPE_ON_START_SPAN_FIELD = '_sentryIsolationScope';

type ScopeWeakRef = { deref(): Scope | undefined } | Scope;

type SpanWithScopes = Span & {
[SCOPE_ON_START_SPAN_FIELD]?: Scope;
[ISOLATION_SCOPE_ON_START_SPAN_FIELD]?: ScopeWeakRef;
[ISOLATION_SCOPE_ON_START_SPAN_FIELD]?: MaybeWeakRef<Scope>;
};

/** Wrap a scope with a WeakRef if available, falling back to a direct scope. */
function wrapScopeWithWeakRef(scope: Scope): ScopeWeakRef {
try {
// @ts-expect-error - WeakRef is not available in all environments
const WeakRefClass = GLOBAL_OBJ.WeakRef;
if (typeof WeakRefClass === 'function') {
return new WeakRefClass(scope);
}
} catch {
// WeakRef not available or failed to create
// We'll fall back to a direct scope
}

return scope;
}

/** Try to unwrap a scope from a potential WeakRef wrapper. */
function unwrapScopeFromWeakRef(scopeRef: ScopeWeakRef | undefined): Scope | undefined {
if (!scopeRef) {
return undefined;
}

if (typeof scopeRef === 'object' && 'deref' in scopeRef && typeof scopeRef.deref === 'function') {
try {
return scopeRef.deref();
} catch {
return undefined;
}
}

// Fallback to a direct scope
return scopeRef as Scope;
}

/** Store the scope & isolation scope for a span, which can the be used when it is finished. */
export function setCapturedScopesOnSpan(span: Span | undefined, scope: Scope, isolationScope: Scope): void {
if (span) {
addNonEnumerableProperty(span, ISOLATION_SCOPE_ON_START_SPAN_FIELD, wrapScopeWithWeakRef(isolationScope));
addNonEnumerableProperty(span, ISOLATION_SCOPE_ON_START_SPAN_FIELD, makeWeakRef(isolationScope));
// We don't wrap the scope with a WeakRef here because webkit aggressively garbage collects
// and scopes are not held in memory for long periods of time.
addNonEnumerableProperty(span, SCOPE_ON_START_SPAN_FIELD, scope);
Expand All @@ -66,6 +30,6 @@ export function getCapturedScopesOnSpan(span: Span): { scope?: Scope; isolationS

return {
scope: spanWithScopes[SCOPE_ON_START_SPAN_FIELD],
isolationScope: unwrapScopeFromWeakRef(spanWithScopes[ISOLATION_SCOPE_ON_START_SPAN_FIELD]),
isolationScope: derefWeakRef(spanWithScopes[ISOLATION_SCOPE_ON_START_SPAN_FIELD]),
};
}
65 changes: 65 additions & 0 deletions packages/core/src/utils/weakRef.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { GLOBAL_OBJ } from './worldwide';

/**
* Interface representing a weak reference to an object.
* This matches the standard WeakRef interface but is defined here
* because WeakRef is not available in ES2020 type definitions.
*/
interface WeakRefLike<T extends object> {
deref(): T | undefined;
}

/**
* A wrapper type that represents either a WeakRef-like object or a direct reference.
* Used for optional weak referencing in environments where WeakRef may not be available.
*/
export type MaybeWeakRef<T extends object> = WeakRefLike<T> | T;

/**
* Creates a weak reference to an object if WeakRef is available,
* otherwise returns the object directly.
*
* This is useful for breaking circular references while maintaining
* compatibility with environments that don't support WeakRef (e.g., older browsers).
*
* @param value - The object to create a weak reference to
* @returns A WeakRef wrapper if available, or the original object as fallback
*/
export function makeWeakRef<T extends object>(value: T): MaybeWeakRef<T> {
try {
// @ts-expect-error - WeakRef may not be in the type definitions for older TS targets
const WeakRefImpl = GLOBAL_OBJ.WeakRef;
if (typeof WeakRefImpl === 'function') {
return new WeakRefImpl(value);
}
} catch {
// WeakRef not available or construction failed
}
return value;
}

/**
* Resolves a potentially weak reference, returning the underlying object
* or undefined if the reference has been garbage collected.
*
* @param ref - A MaybeWeakRef or undefined
* @returns The referenced object, or undefined if GC'd or ref was undefined
*/
export function derefWeakRef<T extends object>(ref: MaybeWeakRef<T> | undefined): T | undefined {
if (!ref) {
return undefined;
}

// Check if this is a WeakRef (has deref method)
if (typeof ref === 'object' && 'deref' in ref && typeof ref.deref === 'function') {
try {
return ref.deref();
} catch {
// deref() failed - treat as GC'd
return undefined;
}
}

// Direct reference fallback
return ref as T;
}
178 changes: 178 additions & 0 deletions packages/core/test/lib/utils/weakRef.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { derefWeakRef, makeWeakRef, type MaybeWeakRef } from '../../../src/utils/weakRef';

describe('Unit | util | weakRef', () => {
describe('makeWeakRef', () => {
it('creates a WeakRef when available', () => {
const obj = { foo: 'bar' };
const ref = makeWeakRef(obj);

// Should be a WeakRef, not the direct object
expect(ref).toBeInstanceOf(WeakRef);
expect((ref as WeakRef<typeof obj>).deref()).toBe(obj);
});

it('returns the object directly when WeakRef is not available', () => {
const originalWeakRef = globalThis.WeakRef;
(globalThis as any).WeakRef = undefined;

try {
const obj = { foo: 'bar' };
const ref = makeWeakRef(obj);

// Should be the direct object
expect(ref).toBe(obj);
} finally {
(globalThis as any).WeakRef = originalWeakRef;
}
});

it('returns the object directly when WeakRef constructor throws', () => {
const originalWeakRef = globalThis.WeakRef;
(globalThis as any).WeakRef = function () {
throw new Error('WeakRef not supported');
};

try {
const obj = { foo: 'bar' };
const ref = makeWeakRef(obj);

// Should fall back to the direct object
expect(ref).toBe(obj);
} finally {
(globalThis as any).WeakRef = originalWeakRef;
}
});

it('works with different object types', () => {
const plainObject = { key: 'value' };
const array = [1, 2, 3];
const func = () => 'test';
const date = new Date();

expect(derefWeakRef(makeWeakRef(plainObject))).toBe(plainObject);
expect(derefWeakRef(makeWeakRef(array))).toBe(array);
expect(derefWeakRef(makeWeakRef(func))).toBe(func);
expect(derefWeakRef(makeWeakRef(date))).toBe(date);
});
});

describe('derefWeakRef', () => {
it('returns undefined for undefined input', () => {
expect(derefWeakRef(undefined)).toBeUndefined();
});

it('correctly dereferences a WeakRef', () => {
const obj = { foo: 'bar' };
const weakRef = new WeakRef(obj);

expect(derefWeakRef(weakRef)).toBe(obj);
});

it('returns the direct object when not a WeakRef', () => {
const obj = { foo: 'bar' };

// Passing a direct object (fallback case)
expect(derefWeakRef(obj as MaybeWeakRef<typeof obj>)).toBe(obj);
});

it('returns undefined when WeakRef.deref() returns undefined (simulating GC)', () => {
const mockWeakRef = {
deref: vi.fn().mockReturnValue(undefined),
};

expect(derefWeakRef(mockWeakRef as MaybeWeakRef<object>)).toBeUndefined();
expect(mockWeakRef.deref).toHaveBeenCalled();
});

it('returns undefined when WeakRef.deref() throws an error', () => {
const mockWeakRef = {
deref: vi.fn().mockImplementation(() => {
throw new Error('deref failed');
}),
};

expect(derefWeakRef(mockWeakRef as MaybeWeakRef<object>)).toBeUndefined();
expect(mockWeakRef.deref).toHaveBeenCalled();
});

it('handles objects with a non-function deref property', () => {
const objWithDerefProperty = {
deref: 'not a function',
actualData: 'test',
};

// Should treat it as a direct object since deref is not a function
expect(derefWeakRef(objWithDerefProperty as unknown as MaybeWeakRef<object>)).toBe(objWithDerefProperty);
});
});

describe('roundtrip (makeWeakRef + derefWeakRef)', () => {
it('preserves object identity with WeakRef available', () => {
const obj = { nested: { data: [1, 2, 3] } };
const ref = makeWeakRef(obj);
const retrieved = derefWeakRef(ref);

expect(retrieved).toBe(obj);
expect(retrieved?.nested.data).toEqual([1, 2, 3]);
});

it('preserves object identity with WeakRef unavailable', () => {
const originalWeakRef = globalThis.WeakRef;
(globalThis as any).WeakRef = undefined;

try {
const obj = { nested: { data: [1, 2, 3] } };
const ref = makeWeakRef(obj);
const retrieved = derefWeakRef(ref);

expect(retrieved).toBe(obj);
expect(retrieved?.nested.data).toEqual([1, 2, 3]);
} finally {
(globalThis as any).WeakRef = originalWeakRef;
}
});

it('allows multiple refs to the same object', () => {
const obj = { id: 'shared' };
const ref1 = makeWeakRef(obj);
const ref2 = makeWeakRef(obj);

expect(derefWeakRef(ref1)).toBe(obj);
expect(derefWeakRef(ref2)).toBe(obj);
expect(derefWeakRef(ref1)).toBe(derefWeakRef(ref2));
});
});

describe('type safety', () => {
it('preserves generic type information', () => {
interface TestInterface {
id: number;
name: string;
}

const obj: TestInterface = { id: 1, name: 'test' };
const ref: MaybeWeakRef<TestInterface> = makeWeakRef(obj);
const retrieved: TestInterface | undefined = derefWeakRef(ref);

expect(retrieved?.id).toBe(1);
expect(retrieved?.name).toBe('test');
});

it('works with class instances', () => {
class TestClass {
constructor(public value: string) {}
getValue(): string {
return this.value;
}
}

const instance = new TestClass('hello');
const ref = makeWeakRef(instance);
const retrieved = derefWeakRef(ref);

expect(retrieved).toBeInstanceOf(TestClass);
expect(retrieved?.getValue()).toBe('hello');
});
});
});
17 changes: 14 additions & 3 deletions packages/opentelemetry/src/utils/contextData.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
import type { Context } from '@opentelemetry/api';
import type { Scope } from '@sentry/core';
import { addNonEnumerableProperty } from '@sentry/core';
import { addNonEnumerableProperty, derefWeakRef, makeWeakRef, type MaybeWeakRef } from '@sentry/core';
import { SENTRY_SCOPES_CONTEXT_KEY } from '../constants';
import type { CurrentScopes } from '../types';

const SCOPE_CONTEXT_FIELD = '_scopeContext';

type ScopeWithContext = Scope & {
[SCOPE_CONTEXT_FIELD]?: MaybeWeakRef<Context>;
};

/**
* Try to get the current scopes from the given OTEL context.
* This requires a Context Manager that was wrapped with getWrappedContextManager.
Expand All @@ -25,14 +29,21 @@ export function setScopesOnContext(context: Context, scopes: CurrentScopes): Con
/**
* Set the context on the scope so we can later look it up.
* We need this to get the context from the scope in the `trace` functions.
*
* We use WeakRef to avoid a circular reference between the scope and the context.
* The context holds scopes (via SENTRY_SCOPES_CONTEXT_KEY), and if the scope held
* a strong reference back to the context, neither could be garbage collected even
* when the context is no longer reachable from application code (e.g., after a
* request completes but pooled connections retain patched callbacks).
*/
export function setContextOnScope(scope: Scope, context: Context): void {
addNonEnumerableProperty(scope, SCOPE_CONTEXT_FIELD, context);
addNonEnumerableProperty(scope, SCOPE_CONTEXT_FIELD, makeWeakRef(context));
}

/**
* Get the context related to a scope.
* Returns undefined if the context has been garbage collected (when WeakRef is used).
*/
export function getContextFromScope(scope: Scope): Context | undefined {
return (scope as { [SCOPE_CONTEXT_FIELD]?: Context })[SCOPE_CONTEXT_FIELD];
return derefWeakRef((scope as ScopeWithContext)[SCOPE_CONTEXT_FIELD]);
}
Loading
Loading