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
257 changes: 257 additions & 0 deletions packages/analytics-js-common/__tests__/utilities/object.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
removeUndefinedAndNullValues,
getNormalizedBooleanValue,
getNormalizedObjectValue,
deepFreeze,
} from '../../src/utilities/object';

const identifyTraitsPayloadMock = {
Expand Down Expand Up @@ -538,4 +539,260 @@ describe('Common Utils - Object', () => {
},
);
});

describe('deepFreeze', () => {
const tcData = [
{
input: { a: 1, b: 2, c: { d: 3 } },
output: { a: 1, b: 2, c: { d: 3 } },
},
{
input: { a: 1, b: 2, c: { d: 3, e: { f: 4 } } },
output: { a: 1, b: 2, c: { d: 3, e: { f: 4 } } },
},
{
input: { a: 1, b: null, c: { d: 3, e: { f: null } } },
output: { a: 1, b: null, c: { d: 3, e: { f: null } } },
},
{
input: { a: [{ b: 1 }, { c: 2 }, null] },
output: { a: [{ b: 1 }, { c: 2 }, null] },
},
{
input: { a: undefined, b: null, c: 0, d: false, e: '', f: NaN },
output: { a: undefined, b: null, c: 0, d: false, e: '', f: NaN },
},
{
input: {
nested: {
deep: {
very: {
deeply: {
nested: {
value: 42,
nullValue: null,
undefinedValue: undefined,
},
},
},
},
},
},
output: {
nested: {
deep: {
very: {
deeply: {
nested: {
value: 42,
nullValue: null,
undefinedValue: undefined,
},
},
},
},
},
},
},
{
input: {
mixedArray: [
1,
'string',
null,
undefined,
{ nested: true },
[1, 2, { deep: 'value' }],
function testFn() {
return 'test';
},
new Date('2024-01-01'),
/regex/g,
Symbol('test'),
],
},
output: {
mixedArray: [
1,
'string',
null,
undefined,
{ nested: true },
[1, 2, { deep: 'value' }],
expect.any(Function),
expect.any(Date),
expect.any(RegExp),
expect.any(Symbol),
],
},
},
{
input: {
sparseArray: [1, , , 4, undefined, null],
emptyObject: {},
emptyArray: [],
circularRef: { self: null },
},
output: {
sparseArray: [1, , , 4, undefined, null],
emptyObject: {},
emptyArray: [],
circularRef: { self: null },
},
},
{
input: {
complexNesting: {
level1: {
array: [
{
id: 1,
data: null,
meta: {
tags: ['a', 'b', null, undefined],
settings: {
enabled: false,
config: {
nested: {
value: 'deep',
empty: {},
},
},
},
},
},
{
id: 2,
data: {
values: [1, 2, 3],
nullField: null,
undefinedField: undefined,
},
},
],
},
},
},
output: {
complexNesting: {
level1: {
array: [
{
id: 1,
data: null,
meta: {
tags: ['a', 'b', null, undefined],
settings: {
enabled: false,
config: {
nested: {
value: 'deep',
empty: {},
},
},
},
},
},
{
id: 2,
data: {
values: [1, 2, 3],
nullField: null,
undefinedField: undefined,
},
},
],
},
},
},
},
{
input: {
primitiveTypes: {
number: 42,
string: 'hello',
boolean: true,
nullValue: null,
undefinedValue: undefined,
zero: 0,
emptyString: '',
falseValue: false,
infinity: Infinity,
negativeInfinity: -Infinity,
nanValue: NaN,
},
},
output: {
primitiveTypes: {
number: 42,
string: 'hello',
boolean: true,
nullValue: null,
undefinedValue: undefined,
zero: 0,
emptyString: '',
falseValue: false,
infinity: Infinity,
negativeInfinity: -Infinity,
nanValue: NaN,
},
},
},
{
input: {
arrayOfObjects: [
{ id: 1, name: 'first', data: null },
{ id: 2, name: 'second', data: { nested: { value: 'test' } } },
{ id: 3, name: 'third', data: undefined },
null,
undefined,
{ id: 4, name: 'fourth', data: [] },
],
},
output: {
arrayOfObjects: [
{ id: 1, name: 'first', data: null },
{ id: 2, name: 'second', data: { nested: { value: 'test' } } },
{ id: 3, name: 'third', data: undefined },
null,
undefined,
{ id: 4, name: 'fourth', data: [] },
],
},
},
];

it.each(tcData)(
'should freeze the object',
({ input, output }: { input: any; output: any }) => {
const outcome = deepFreeze(input);
expect(outcome).toEqual(output);

// Check if the object is frozen
expect(Object.isFrozen(outcome)).toBe(true);

// Check if the object is immutable
expect(Object.isSealed(outcome)).toBe(true);

// Check if the object is immutable
expect(Object.isExtensible(outcome)).toBe(false);

// try to mutate the object and ensure it has no effect and relevant errors are thrown
const firstKey = Object.keys(outcome)[0];
if (firstKey) {
expect(() => {
outcome[firstKey] = 'mutated';
}).toThrow();

expect(() => {
delete outcome[firstKey];
}).toThrow();
}

expect(() => {
outcome.newProperty = 'added';
}).toThrow();
},
);
});
});
7 changes: 7 additions & 0 deletions packages/analytics-js-common/src/types/EventApi.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { IntegrationOpts } from './Integration';
import type { Nullable } from './Nullable';
import type { ApiObject } from './ApiObject';
import type { UserSessionKey } from './UserSessionStorage';

// TODO: should we take the types from IdentifyTrait instead of any string key?
// https://www.rudderstack.com/docs/event-spec/standard-events/identify/#identify-traits
Expand Down Expand Up @@ -44,3 +45,9 @@ export type APIEvent = {
export type RudderEventType = 'page' | 'track' | 'identify' | 'alias' | 'group';

export type ReadyCallback = () => void;

export type ResetOptions = {
entries: {
[key in UserSessionKey]?: boolean;
};
};
13 changes: 9 additions & 4 deletions packages/analytics-js-common/src/types/IRudderAnalytics.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { Nullable } from './Nullable';
import type { ApiCallback, ApiOptions } from './EventApi';
import type { ApiCallback, ApiOptions, ResetOptions } from './EventApi';
import type { AnonymousIdOptions, LoadOptions } from './LoadOptions';
import type { ApiObject } from './ApiObject';
import type { ILogger, LogLevel, RSALogger } from './Logger';
Expand Down Expand Up @@ -76,6 +76,11 @@ export type AnalyticsAliasMethod = {
(to: string, callback?: ApiCallback): void;
};

export type AnalyticsResetMethod = {
(options?: ResetOptions): void;
(resetAnonymousId?: boolean): void;
};

export interface IRudderAnalytics<T = any> {
analyticsInstances: Record<string, T>;
defaultAnalyticsKey: string;
Expand Down Expand Up @@ -132,11 +137,11 @@ export interface IRudderAnalytics<T = any> {
group: AnalyticsGroupMethod;

/**
* Clear user information
* Clear user session information
*
* @param resetAnonymousId optionally clears anonymousId as well
* @param options options for reset
*/
reset(resetAnonymousId?: boolean): void;
reset: AnalyticsResetMethod;

/**
* To get anonymousId set in the SDK
Expand Down
10 changes: 10 additions & 0 deletions packages/analytics-js-common/src/utilities/object.ts
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,15 @@ const getNormalizedObjectValue = (val: any): any => {
const getNormalizedBooleanValue = (val: any, defVal: boolean): boolean =>
typeof val === 'boolean' ? val : defVal;

const deepFreeze = <T>(obj: T): T => {
Object.getOwnPropertyNames(obj).forEach(function (prop) {
if (obj[prop as keyof T] && typeof obj[prop as keyof T] === 'object') {
deepFreeze(obj[prop as keyof T]);
}
});
return Object.freeze(obj);
};

export {
getValueByPath,
hasValueByPath,
Expand All @@ -151,4 +160,5 @@ export {
isObject,
getNormalizedObjectValue,
getNormalizedBooleanValue,
deepFreeze,
};
6 changes: 3 additions & 3 deletions packages/analytics-js/.size-limit.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ export default [
name: 'Core - Modern - NPM (CJS)',
path: 'dist/npm/modern/cjs/index.cjs',
import: '*',
limit: '28 KiB',
limit: '28.5 KiB',
},
{
name: 'Core - Modern - NPM (UMD)',
Expand All @@ -47,7 +47,7 @@ export default [
{
name: 'Core - Modern - CDN',
path: 'dist/cdn/modern/iife/rsa.min.js',
limit: '28 KiB',
limit: '28.5 KiB',
},
{
name: 'Core (Bundled) - Legacy - NPM (ESM)',
Expand Down Expand Up @@ -113,7 +113,7 @@ export default [
name: 'Core (Content Script) - Modern - NPM (CJS)',
path: 'dist/npm/modern/content-script/cjs/index.cjs',
import: '*',
limit: '41 KiB',
limit: '41.5 KiB',
},
{
name: 'Core (Content Script) - Modern - NPM (UMD)',
Expand Down
Loading
Loading