From 9dba73601e722dcfd6ee5d2ed152b62f62a6e455 Mon Sep 17 00:00:00 2001 From: Andrei Borza Date: Fri, 19 Sep 2025 16:18:54 +0200 Subject: [PATCH 1/5] ref(core): Wrap scopes in `WeakRef` when storing scopes on spans We often store scopes on spans via `setCapturedScopesOnSpan` and retrieve them via `getCapturedScopesOnSpan`. This change wraps the scopes in `WeakRef` to attempt fixing a potential memory leak when spans hold on to scopes indefinitely. The downside is scopes might end up with undefined scopes on them if the scope was garbage collected, we'll have to see if that's an issue or not. --- packages/core/src/tracing/utils.ts | 52 +++- packages/core/test/lib/tracing/utils.test.ts | 249 +++++++++++++++++++ 2 files changed, 295 insertions(+), 6 deletions(-) create mode 100644 packages/core/test/lib/tracing/utils.test.ts diff --git a/packages/core/src/tracing/utils.ts b/packages/core/src/tracing/utils.ts index 5fa0a4b34420..f7b5ab54c49e 100644 --- a/packages/core/src/tracing/utils.ts +++ b/packages/core/src/tracing/utils.ts @@ -1,29 +1,69 @@ import type { Scope } from '../scope'; import type { Span } from '../types-hoist/span'; import { addNonEnumerableProperty } from '../utils/object'; +import { GLOBAL_OBJ } from '../utils/worldwide'; 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]?: Scope; + [SCOPE_ON_START_SPAN_FIELD]?: ScopeWeakRef; + [ISOLATION_SCOPE_ON_START_SPAN_FIELD]?: ScopeWeakRef; }; +/** 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, isolationScope); - addNonEnumerableProperty(span, SCOPE_ON_START_SPAN_FIELD, scope); + addNonEnumerableProperty(span, ISOLATION_SCOPE_ON_START_SPAN_FIELD, wrapScopeWithWeakRef(isolationScope)); + addNonEnumerableProperty(span, SCOPE_ON_START_SPAN_FIELD, wrapScopeWithWeakRef(scope)); } } /** * Grabs the scope and isolation scope off a span that were active when the span was started. + * If WeakRef was used and scopes have been garbage collected, returns undefined for those scopes. */ export function getCapturedScopesOnSpan(span: Span): { scope?: Scope; isolationScope?: Scope } { + const spanWithScopes = span as SpanWithScopes; + return { - scope: (span as SpanWithScopes)[SCOPE_ON_START_SPAN_FIELD], - isolationScope: (span as SpanWithScopes)[ISOLATION_SCOPE_ON_START_SPAN_FIELD], + scope: unwrapScopeFromWeakRef(spanWithScopes[SCOPE_ON_START_SPAN_FIELD]), + isolationScope: unwrapScopeFromWeakRef(spanWithScopes[ISOLATION_SCOPE_ON_START_SPAN_FIELD]), }; } diff --git a/packages/core/test/lib/tracing/utils.test.ts b/packages/core/test/lib/tracing/utils.test.ts new file mode 100644 index 000000000000..6fb1a8ccce9c --- /dev/null +++ b/packages/core/test/lib/tracing/utils.test.ts @@ -0,0 +1,249 @@ +import { describe, expect, it, vi } from 'vitest'; +import { Scope } from '../../../src/scope'; +import { getCapturedScopesOnSpan, setCapturedScopesOnSpan } from '../../../src/tracing/utils'; +import type { Span } from '../../../src/types-hoist/span'; + +// Mock span object that implements the minimum needed interface +function createMockSpan(): Span { + return {} as Span; +} + +describe('tracing utils', () => { + describe('setCapturedScopesOnSpan / getCapturedScopesOnSpan', () => { + it('stores and retrieves scopes correctly', () => { + const span = createMockSpan(); + const scope = new Scope(); + const isolationScope = new Scope(); + + scope.setTag('test-scope', 'value1'); + isolationScope.setTag('test-isolation-scope', 'value2'); + + setCapturedScopesOnSpan(span, scope, isolationScope); + const retrieved = getCapturedScopesOnSpan(span); + + expect(retrieved.scope).toBe(scope); + expect(retrieved.isolationScope).toBe(isolationScope); + expect(retrieved.scope?.getScopeData().tags).toEqual({ 'test-scope': 'value1' }); + expect(retrieved.isolationScope?.getScopeData().tags).toEqual({ 'test-isolation-scope': 'value2' }); + }); + + it('handles undefined span gracefully in setCapturedScopesOnSpan', () => { + const scope = new Scope(); + const isolationScope = new Scope(); + + expect(() => { + setCapturedScopesOnSpan(undefined, scope, isolationScope); + }).not.toThrow(); + }); + + it('returns undefined scopes when span has no captured scopes', () => { + const span = createMockSpan(); + const retrieved = getCapturedScopesOnSpan(span); + + expect(retrieved.scope).toBeUndefined(); + expect(retrieved.isolationScope).toBeUndefined(); + }); + + it('uses WeakRef', () => { + const span = createMockSpan(); + const scope = new Scope(); + const isolationScope = new Scope(); + + setCapturedScopesOnSpan(span, scope, isolationScope); + + // Check that WeakRef instances were stored + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const spanWithScopes = span as any; + expect(spanWithScopes._sentryScope).toBeInstanceOf(WeakRef); + expect(spanWithScopes._sentryIsolationScope).toBeInstanceOf(WeakRef); + + // Verify we can still retrieve the scopes + const retrieved = getCapturedScopesOnSpan(span); + expect(retrieved.scope).toBe(scope); + expect(retrieved.isolationScope).toBe(isolationScope); + }); + + it('falls back to direct storage when WeakRef is not available', () => { + // Temporarily disable WeakRef + const originalWeakRef = globalThis.WeakRef; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (globalThis as any).WeakRef = undefined; + + try { + const span = createMockSpan(); + const scope = new Scope(); + const isolationScope = new Scope(); + + setCapturedScopesOnSpan(span, scope, isolationScope); + + // Check that scopes were stored directly + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const spanWithScopes = span as any; + expect(spanWithScopes._sentryScope).toBe(scope); + expect(spanWithScopes._sentryIsolationScope).toBe(isolationScope); + + // When WeakRef is available, check that stored values are not WeakRef instances + if (originalWeakRef) { + expect(spanWithScopes._sentryScope).not.toBeInstanceOf(originalWeakRef); + expect(spanWithScopes._sentryIsolationScope).not.toBeInstanceOf(originalWeakRef); + } + + // Verify we can still retrieve the scopes + const retrieved = getCapturedScopesOnSpan(span); + expect(retrieved.scope).toBe(scope); + expect(retrieved.isolationScope).toBe(isolationScope); + } finally { + // Restore WeakRef + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (globalThis as any).WeakRef = originalWeakRef; + } + }); + + it('handles WeakRef deref returning undefined gracefully', () => { + const span = createMockSpan(); + const scope = new Scope(); + const isolationScope = new Scope(); + + setCapturedScopesOnSpan(span, scope, isolationScope); + + // Mock WeakRef.deref to return undefined (simulating garbage collection) + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const spanWithScopes = span as any; + const mockScopeWeakRef = { + deref: vi.fn().mockReturnValue(undefined), + }; + const mockIsolationScopeWeakRef = { + deref: vi.fn().mockReturnValue(undefined), + }; + + spanWithScopes._sentryScope = mockScopeWeakRef; + spanWithScopes._sentryIsolationScope = mockIsolationScopeWeakRef; + + const retrieved = getCapturedScopesOnSpan(span); + expect(retrieved.scope).toBeUndefined(); + expect(retrieved.isolationScope).toBeUndefined(); + expect(mockScopeWeakRef.deref).toHaveBeenCalled(); + expect(mockIsolationScopeWeakRef.deref).toHaveBeenCalled(); + }); + + it('handles corrupted WeakRef objects gracefully', () => { + const span = createMockSpan(); + + // Simulate corrupted WeakRef objects + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const spanWithScopes = span as any; + spanWithScopes._sentryScope = { + deref: vi.fn().mockImplementation(() => { + throw new Error('WeakRef deref failed'); + }), + }; + spanWithScopes._sentryIsolationScope = { + deref: vi.fn().mockImplementation(() => { + throw new Error('WeakRef deref failed'); + }), + }; + + const retrieved = getCapturedScopesOnSpan(span); + expect(retrieved.scope).toBeUndefined(); + expect(retrieved.isolationScope).toBeUndefined(); + }); + + it('preserves scope data when using WeakRef', () => { + const span = createMockSpan(); + const scope = new Scope(); + const isolationScope = new Scope(); + + // Add various types of data to scopes + scope.setTag('string-tag', 'value'); + scope.setTag('number-tag', 123); + scope.setTag('boolean-tag', true); + scope.setContext('test-context', { key: 'value' }); + scope.setUser({ id: 'test-user' }); + + isolationScope.setExtra('extra-data', { complex: { nested: 'object' } }); + isolationScope.setLevel('warning'); + + setCapturedScopesOnSpan(span, scope, isolationScope); + const retrieved = getCapturedScopesOnSpan(span); + + // Verify all data is preserved + expect(retrieved.scope?.getScopeData().tags).toEqual({ + 'string-tag': 'value', + 'number-tag': 123, + 'boolean-tag': true, + }); + expect(retrieved.scope?.getScopeData().contexts).toEqual({ + 'test-context': { key: 'value' }, + }); + expect(retrieved.scope?.getScopeData().user).toEqual({ id: 'test-user' }); + + expect(retrieved.isolationScope?.getScopeData().extra).toEqual({ + 'extra-data': { complex: { nested: 'object' } }, + }); + expect(retrieved.isolationScope?.getScopeData().level).toBe('warning'); + }); + + it('handles multiple spans with different scopes', () => { + const span1 = createMockSpan(); + const span2 = createMockSpan(); + + const scope1 = new Scope(); + const scope2 = new Scope(); + const isolationScope1 = new Scope(); + const isolationScope2 = new Scope(); + + scope1.setTag('span', '1'); + scope2.setTag('span', '2'); + isolationScope1.setTag('isolation', '1'); + isolationScope2.setTag('isolation', '2'); + + setCapturedScopesOnSpan(span1, scope1, isolationScope1); + setCapturedScopesOnSpan(span2, scope2, isolationScope2); + + const retrieved1 = getCapturedScopesOnSpan(span1); + const retrieved2 = getCapturedScopesOnSpan(span2); + + expect(retrieved1.scope?.getScopeData().tags).toEqual({ span: '1' }); + expect(retrieved1.isolationScope?.getScopeData().tags).toEqual({ isolation: '1' }); + + expect(retrieved2.scope?.getScopeData().tags).toEqual({ span: '2' }); + expect(retrieved2.isolationScope?.getScopeData().tags).toEqual({ isolation: '2' }); + + // Ensure they are different scope instances + expect(retrieved1.scope).not.toBe(retrieved2.scope); + expect(retrieved1.isolationScope).not.toBe(retrieved2.isolationScope); + }); + + it('handles span reuse correctly', () => { + const span = createMockSpan(); + + // First use + const scope1 = new Scope(); + const isolationScope1 = new Scope(); + scope1.setTag('first', 'use'); + isolationScope1.setTag('first-isolation', 'use'); + + setCapturedScopesOnSpan(span, scope1, isolationScope1); + const retrieved1 = getCapturedScopesOnSpan(span); + + expect(retrieved1.scope?.getScopeData().tags).toEqual({ first: 'use' }); + expect(retrieved1.isolationScope?.getScopeData().tags).toEqual({ 'first-isolation': 'use' }); + + // Reuse with different scopes (overwrite) + const scope2 = new Scope(); + const isolationScope2 = new Scope(); + scope2.setTag('second', 'use'); + isolationScope2.setTag('second-isolation', 'use'); + + setCapturedScopesOnSpan(span, scope2, isolationScope2); + const retrieved2 = getCapturedScopesOnSpan(span); + + expect(retrieved2.scope?.getScopeData().tags).toEqual({ second: 'use' }); + expect(retrieved2.isolationScope?.getScopeData().tags).toEqual({ 'second-isolation': 'use' }); + + // Should be the new scopes, not the old ones + expect(retrieved2.scope).toBe(scope2); + expect(retrieved2.isolationScope).toBe(isolationScope2); + }); + }); +}); From 459b93bed0a37679fd86961f7fe9d08bca9dcaee Mon Sep 17 00:00:00 2001 From: Andrei Borza Date: Mon, 22 Sep 2025 11:49:59 +0200 Subject: [PATCH 2/5] Webkit? --- packages/core/src/tracing/utils.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/core/src/tracing/utils.ts b/packages/core/src/tracing/utils.ts index f7b5ab54c49e..1b7be4747ad0 100644 --- a/packages/core/src/tracing/utils.ts +++ b/packages/core/src/tracing/utils.ts @@ -52,6 +52,8 @@ export function setCapturedScopesOnSpan(span: Span | undefined, scope: Scope, is if (span) { addNonEnumerableProperty(span, ISOLATION_SCOPE_ON_START_SPAN_FIELD, wrapScopeWithWeakRef(isolationScope)); addNonEnumerableProperty(span, SCOPE_ON_START_SPAN_FIELD, wrapScopeWithWeakRef(scope)); + // Testing + setTimeout(() => [scope, isolationScope], 10_000); // keep this reference around for at least 10s } } From fa93143eb9217cb5acce487368879ffe533c110a Mon Sep 17 00:00:00 2001 From: Andrei Borza Date: Mon, 22 Sep 2025 12:10:48 +0200 Subject: [PATCH 3/5] See which one of the scopes it is --- packages/core/src/tracing/utils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/src/tracing/utils.ts b/packages/core/src/tracing/utils.ts index 1b7be4747ad0..e96a8b15c6d4 100644 --- a/packages/core/src/tracing/utils.ts +++ b/packages/core/src/tracing/utils.ts @@ -53,7 +53,7 @@ export function setCapturedScopesOnSpan(span: Span | undefined, scope: Scope, is addNonEnumerableProperty(span, ISOLATION_SCOPE_ON_START_SPAN_FIELD, wrapScopeWithWeakRef(isolationScope)); addNonEnumerableProperty(span, SCOPE_ON_START_SPAN_FIELD, wrapScopeWithWeakRef(scope)); // Testing - setTimeout(() => [scope, isolationScope], 10_000); // keep this reference around for at least 10s + setTimeout(() => [isolationScope], 10_000); // keep this reference around for at least 10s } } From 8b8da13b47d8de568f82d94487c454dc72b4a530 Mon Sep 17 00:00:00 2001 From: Andrei Borza Date: Mon, 22 Sep 2025 13:11:02 +0200 Subject: [PATCH 4/5] Test with scope keeping --- packages/core/src/tracing/utils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/src/tracing/utils.ts b/packages/core/src/tracing/utils.ts index e96a8b15c6d4..c0048c1d4e5d 100644 --- a/packages/core/src/tracing/utils.ts +++ b/packages/core/src/tracing/utils.ts @@ -53,7 +53,7 @@ export function setCapturedScopesOnSpan(span: Span | undefined, scope: Scope, is addNonEnumerableProperty(span, ISOLATION_SCOPE_ON_START_SPAN_FIELD, wrapScopeWithWeakRef(isolationScope)); addNonEnumerableProperty(span, SCOPE_ON_START_SPAN_FIELD, wrapScopeWithWeakRef(scope)); // Testing - setTimeout(() => [isolationScope], 10_000); // keep this reference around for at least 10s + setTimeout(() => [scope], 10_000); // keep this reference around for at least 10s } } From 251a3ddb324b3d5d3158a69b11049f95ad4d9ec1 Mon Sep 17 00:00:00 2001 From: Andrei Borza Date: Mon, 22 Sep 2025 14:04:31 +0200 Subject: [PATCH 5/5] Only store isolationscope as WeakRef --- packages/core/src/tracing/utils.ts | 10 ++--- packages/core/test/lib/tracing/utils.test.ts | 43 +++++++++----------- 2 files changed, 24 insertions(+), 29 deletions(-) diff --git a/packages/core/src/tracing/utils.ts b/packages/core/src/tracing/utils.ts index c0048c1d4e5d..6ca5594b3da6 100644 --- a/packages/core/src/tracing/utils.ts +++ b/packages/core/src/tracing/utils.ts @@ -9,7 +9,7 @@ const ISOLATION_SCOPE_ON_START_SPAN_FIELD = '_sentryIsolationScope'; type ScopeWeakRef = { deref(): Scope | undefined } | Scope; type SpanWithScopes = Span & { - [SCOPE_ON_START_SPAN_FIELD]?: ScopeWeakRef; + [SCOPE_ON_START_SPAN_FIELD]?: Scope; [ISOLATION_SCOPE_ON_START_SPAN_FIELD]?: ScopeWeakRef; }; @@ -51,9 +51,9 @@ function unwrapScopeFromWeakRef(scopeRef: ScopeWeakRef | undefined): Scope | und 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, SCOPE_ON_START_SPAN_FIELD, wrapScopeWithWeakRef(scope)); - // Testing - setTimeout(() => [scope], 10_000); // keep this reference around for at least 10s + // 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); } } @@ -65,7 +65,7 @@ export function getCapturedScopesOnSpan(span: Span): { scope?: Scope; isolationS const spanWithScopes = span as SpanWithScopes; return { - scope: unwrapScopeFromWeakRef(spanWithScopes[SCOPE_ON_START_SPAN_FIELD]), + scope: spanWithScopes[SCOPE_ON_START_SPAN_FIELD], isolationScope: unwrapScopeFromWeakRef(spanWithScopes[ISOLATION_SCOPE_ON_START_SPAN_FIELD]), }; } diff --git a/packages/core/test/lib/tracing/utils.test.ts b/packages/core/test/lib/tracing/utils.test.ts index 6fb1a8ccce9c..63aba8c35529 100644 --- a/packages/core/test/lib/tracing/utils.test.ts +++ b/packages/core/test/lib/tracing/utils.test.ts @@ -44,18 +44,18 @@ describe('tracing utils', () => { expect(retrieved.isolationScope).toBeUndefined(); }); - it('uses WeakRef', () => { + it('uses WeakRef only for isolation scopes', () => { const span = createMockSpan(); const scope = new Scope(); const isolationScope = new Scope(); setCapturedScopesOnSpan(span, scope, isolationScope); - // Check that WeakRef instances were stored + // Check that only isolation scope is wrapped with WeakRef // eslint-disable-next-line @typescript-eslint/no-explicit-any const spanWithScopes = span as any; - expect(spanWithScopes._sentryScope).toBeInstanceOf(WeakRef); - expect(spanWithScopes._sentryIsolationScope).toBeInstanceOf(WeakRef); + expect(spanWithScopes._sentryScope).toBe(scope); // Regular scope stored directly + expect(spanWithScopes._sentryIsolationScope).toBeInstanceOf(WeakRef); // Isolation scope wrapped // Verify we can still retrieve the scopes const retrieved = getCapturedScopesOnSpan(span); @@ -76,13 +76,13 @@ describe('tracing utils', () => { setCapturedScopesOnSpan(span, scope, isolationScope); - // Check that scopes were stored directly + // Check that both scopes are stored directly when WeakRef is not available // eslint-disable-next-line @typescript-eslint/no-explicit-any const spanWithScopes = span as any; - expect(spanWithScopes._sentryScope).toBe(scope); - expect(spanWithScopes._sentryIsolationScope).toBe(isolationScope); + expect(spanWithScopes._sentryScope).toBe(scope); // Regular scope always stored directly + expect(spanWithScopes._sentryIsolationScope).toBe(isolationScope); // Isolation scope falls back to direct storage - // When WeakRef is available, check that stored values are not WeakRef instances + // When WeakRef is available, ensure regular scope is not wrapped but isolation scope would be if (originalWeakRef) { expect(spanWithScopes._sentryScope).not.toBeInstanceOf(originalWeakRef); expect(spanWithScopes._sentryIsolationScope).not.toBeInstanceOf(originalWeakRef); @@ -106,37 +106,32 @@ describe('tracing utils', () => { setCapturedScopesOnSpan(span, scope, isolationScope); - // Mock WeakRef.deref to return undefined (simulating garbage collection) + // Mock WeakRef.deref to return undefined for isolation scope (simulating garbage collection) + // Regular scope is stored directly, so it should always be available // eslint-disable-next-line @typescript-eslint/no-explicit-any const spanWithScopes = span as any; - const mockScopeWeakRef = { - deref: vi.fn().mockReturnValue(undefined), - }; const mockIsolationScopeWeakRef = { deref: vi.fn().mockReturnValue(undefined), }; - spanWithScopes._sentryScope = mockScopeWeakRef; + // Keep the regular scope as is (stored directly) + // Only replace the isolation scope with a mock WeakRef spanWithScopes._sentryIsolationScope = mockIsolationScopeWeakRef; const retrieved = getCapturedScopesOnSpan(span); - expect(retrieved.scope).toBeUndefined(); - expect(retrieved.isolationScope).toBeUndefined(); - expect(mockScopeWeakRef.deref).toHaveBeenCalled(); + expect(retrieved.scope).toBe(scope); // Regular scope should still be available + expect(retrieved.isolationScope).toBeUndefined(); // Isolation scope should be undefined due to GC expect(mockIsolationScopeWeakRef.deref).toHaveBeenCalled(); }); it('handles corrupted WeakRef objects gracefully', () => { const span = createMockSpan(); + const scope = new Scope(); - // Simulate corrupted WeakRef objects + // Set up a regular scope (stored directly) and a corrupted isolation scope WeakRef // eslint-disable-next-line @typescript-eslint/no-explicit-any const spanWithScopes = span as any; - spanWithScopes._sentryScope = { - deref: vi.fn().mockImplementation(() => { - throw new Error('WeakRef deref failed'); - }), - }; + spanWithScopes._sentryScope = scope; // Regular scope stored directly spanWithScopes._sentryIsolationScope = { deref: vi.fn().mockImplementation(() => { throw new Error('WeakRef deref failed'); @@ -144,8 +139,8 @@ describe('tracing utils', () => { }; const retrieved = getCapturedScopesOnSpan(span); - expect(retrieved.scope).toBeUndefined(); - expect(retrieved.isolationScope).toBeUndefined(); + expect(retrieved.scope).toBe(scope); // Regular scope should still be available + expect(retrieved.isolationScope).toBeUndefined(); // Isolation scope should be undefined due to error }); it('preserves scope data when using WeakRef', () => {