Skip to content
Draft
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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@

## Unreleased

### Fixes

- Fix `Object.freeze` type pollution from `@sentry-internal/replay` ([#5408](https://github.com/getsentry/sentry-react-native/issues/5408))

### Dependencies

- Bump Android SDK from v8.27.0 to v8.27.1 ([#5404](https://github.com/getsentry/sentry-react-native/pull/5404))
Expand Down
104 changes: 104 additions & 0 deletions packages/core/test/typings/object-freeze.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
/**
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test file location at packages/core/test/typings/object-freeze.test.ts may not be picked up by Jest's testPathIgnorePatterns in jest.config.js, which currently ignores <rootDir>/test/typings/ directory (note the different directory structure). Verify that this test file is actually executed by the test suite. If the intent is to have type-checking tests ignored from runtime testing, consider either updating the Jest configuration or moving the test to a location that matches the configured patterns.
Severity: MEDIUM

🤖 Prompt for AI Agent

Review the code at the location below. A potential bug has been identified by an AI
agent.
Verify if this is a real issue. If it is, propose a fix; if not, explain why it's not
valid.

Location: packages/core/test/typings/object-freeze.test.ts#L1

Potential issue: The test file location at
`packages/core/test/typings/object-freeze.test.ts` may not be picked up by Jest's
`testPathIgnorePatterns` in `jest.config.js`, which currently ignores
`<rootDir>/test/typings/` directory (note the different directory structure). Verify
that this test file is actually executed by the test suite. If the intent is to have
type-checking tests ignored from runtime testing, consider either updating the Jest
configuration or moving the test to a location that matches the configured patterns.

Did we get this right? 👍 / 👎 to inform future reviews.
Reference ID: 3552648

* Type test for Object.freeze pollution fix
*
* This test ensures that Object.freeze() resolves to the correct built-in type
* and not to a polluted type from @sentry-internal/replay.
*
* See: https://github.com/getsentry/sentry-react-native/issues/5407
*/

describe('Object.freeze type inference', () => {
it('should correctly freeze plain objects', () => {
const frozenObject = Object.freeze({
key: 'value',
num: 42,
});

// Runtime test: should be frozen
expect(Object.isFrozen(frozenObject)).toBe(true);

// Type test: TypeScript should infer Readonly<{ key: string; num: number; }>
expect(frozenObject.key).toBe('value');
expect(frozenObject.num).toBe(42);
});

it('should correctly freeze objects with as const', () => {
const EVENTS = Object.freeze({
CLICK: 'click',
SUBMIT: 'submit',
} as const);

// Runtime test: should be frozen
expect(Object.isFrozen(EVENTS)).toBe(true);

// Type test: TypeScript should infer literal types
expect(EVENTS.CLICK).toBe('click');
expect(EVENTS.SUBMIT).toBe('submit');

// TypeScript should infer: Readonly<{ CLICK: "click"; SUBMIT: "submit"; }>
const eventType: 'click' | 'submit' = EVENTS.CLICK;
expect(eventType).toBe('click');
});

it('should correctly freeze functions', () => {
const frozenFn = Object.freeze((x: number) => x * 2);

// Runtime test: should be frozen
expect(Object.isFrozen(frozenFn)).toBe(true);

// Type test: function should still be callable
const result: number = frozenFn(5);
expect(result).toBe(10);
});

it('should correctly freeze nested objects', () => {
const ACTIONS = Object.freeze({
USER: Object.freeze({
LOGIN: 'user:login',
LOGOUT: 'user:logout',
} as const),
ADMIN: Object.freeze({
DELETE: 'admin:delete',
} as const),
} as const);

// Runtime test: should be frozen
expect(Object.isFrozen(ACTIONS)).toBe(true);
expect(Object.isFrozen(ACTIONS.USER)).toBe(true);
expect(Object.isFrozen(ACTIONS.ADMIN)).toBe(true);

// Type test: should preserve nested literal types
expect(ACTIONS.USER.LOGIN).toBe('user:login');
expect(ACTIONS.ADMIN.DELETE).toBe('admin:delete');

// TypeScript should infer the correct literal type
const action: 'user:login' = ACTIONS.USER.LOGIN;
expect(action).toBe('user:login');
});

it('should maintain type safety and prevent modifications at compile time', () => {
const frozen = Object.freeze({ value: 42 });

// Runtime: attempting to modify should silently fail (in non-strict mode)
// or throw (in strict mode)
expect(() => {
// @ts-expect-error - TypeScript should prevent this at compile time
(frozen as any).value = 100;
}).not.toThrow(); // In non-strict mode, this silently fails

// Value should remain unchanged
expect(frozen.value).toBe(42);
});

it('should work with array freeze', () => {
const frozenArray = Object.freeze([1, 2, 3]);

// Runtime test
expect(Object.isFrozen(frozenArray)).toBe(true);
expect(frozenArray).toEqual([1, 2, 3]);

// Array methods that don't mutate should still work
expect(frozenArray.map(x => x * 2)).toEqual([2, 4, 6]);
expect(frozenArray.filter(x => x > 1)).toEqual([2, 3]);
});
});
3 changes: 2 additions & 1 deletion packages/core/tsconfig.build.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@
"./src/js/playground/*.tsx",
"./src/js/**/*.web.ts",
"./src/js/**/*.web.tsx",
"./typings/react-native.d.ts"
"./typings/react-native.d.ts",
"./typings/global.d.ts"
],
"exclude": ["node_modules"],
"compilerOptions": {
Expand Down
30 changes: 30 additions & 0 deletions packages/core/typings/global.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/**
* Global type augmentations for the Sentry React Native SDK
*
* This file contains global type fixes and augmentations to resolve conflicts
* with transitive dependencies.
*/

/**
* Fix for Object.freeze type pollution from @sentry-internal/replay
*
* Issue: TypeScript incorrectly resolves Object.freeze() to a freeze method
* from @sentry-internal/replay's CanvasManagerInterface instead of the built-in.
*
* See: https://github.com/getsentry/sentry-react-native/issues/5407
*/
declare global {
interface ObjectConstructor {
freeze<T>(o: T): Readonly<T>;

// eslint-disable-next-line @typescript-eslint/ban-types -- Matching TypeScript's official Object.freeze signature from lib.es5.d.ts
freeze<T extends Function>(f: T): T;

freeze<T extends {[idx: string]: U | null | undefined | object}, U extends string | bigint | number | boolean | symbol>(o: T): Readonly<T>;

freeze<T>(o: T): Readonly<T>;
}
}

export {};

Loading