Skip to content

Commit f739030

Browse files
authored
feat: reset api with options (#2445)
* feat: reset api with options * fix: avoid constant mutation * fix: issues and add unit tests * chore: clean up env url issues * refactor: address review comments * chore: address ai bot review comments
1 parent e56acdc commit f739030

18 files changed

Lines changed: 836 additions & 79 deletions

File tree

packages/analytics-js-common/__tests__/utilities/object.test.ts

Lines changed: 257 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
removeUndefinedAndNullValues,
1111
getNormalizedBooleanValue,
1212
getNormalizedObjectValue,
13+
deepFreeze,
1314
} from '../../src/utilities/object';
1415

1516
const identifyTraitsPayloadMock = {
@@ -538,4 +539,260 @@ describe('Common Utils - Object', () => {
538539
},
539540
);
540541
});
542+
543+
describe('deepFreeze', () => {
544+
const tcData = [
545+
{
546+
input: { a: 1, b: 2, c: { d: 3 } },
547+
output: { a: 1, b: 2, c: { d: 3 } },
548+
},
549+
{
550+
input: { a: 1, b: 2, c: { d: 3, e: { f: 4 } } },
551+
output: { a: 1, b: 2, c: { d: 3, e: { f: 4 } } },
552+
},
553+
{
554+
input: { a: 1, b: null, c: { d: 3, e: { f: null } } },
555+
output: { a: 1, b: null, c: { d: 3, e: { f: null } } },
556+
},
557+
{
558+
input: { a: [{ b: 1 }, { c: 2 }, null] },
559+
output: { a: [{ b: 1 }, { c: 2 }, null] },
560+
},
561+
{
562+
input: { a: undefined, b: null, c: 0, d: false, e: '', f: NaN },
563+
output: { a: undefined, b: null, c: 0, d: false, e: '', f: NaN },
564+
},
565+
{
566+
input: {
567+
nested: {
568+
deep: {
569+
very: {
570+
deeply: {
571+
nested: {
572+
value: 42,
573+
nullValue: null,
574+
undefinedValue: undefined,
575+
},
576+
},
577+
},
578+
},
579+
},
580+
},
581+
output: {
582+
nested: {
583+
deep: {
584+
very: {
585+
deeply: {
586+
nested: {
587+
value: 42,
588+
nullValue: null,
589+
undefinedValue: undefined,
590+
},
591+
},
592+
},
593+
},
594+
},
595+
},
596+
},
597+
{
598+
input: {
599+
mixedArray: [
600+
1,
601+
'string',
602+
null,
603+
undefined,
604+
{ nested: true },
605+
[1, 2, { deep: 'value' }],
606+
function testFn() {
607+
return 'test';
608+
},
609+
new Date('2024-01-01'),
610+
/regex/g,
611+
Symbol('test'),
612+
],
613+
},
614+
output: {
615+
mixedArray: [
616+
1,
617+
'string',
618+
null,
619+
undefined,
620+
{ nested: true },
621+
[1, 2, { deep: 'value' }],
622+
expect.any(Function),
623+
expect.any(Date),
624+
expect.any(RegExp),
625+
expect.any(Symbol),
626+
],
627+
},
628+
},
629+
{
630+
input: {
631+
sparseArray: [1, , , 4, undefined, null],
632+
emptyObject: {},
633+
emptyArray: [],
634+
circularRef: { self: null },
635+
},
636+
output: {
637+
sparseArray: [1, , , 4, undefined, null],
638+
emptyObject: {},
639+
emptyArray: [],
640+
circularRef: { self: null },
641+
},
642+
},
643+
{
644+
input: {
645+
complexNesting: {
646+
level1: {
647+
array: [
648+
{
649+
id: 1,
650+
data: null,
651+
meta: {
652+
tags: ['a', 'b', null, undefined],
653+
settings: {
654+
enabled: false,
655+
config: {
656+
nested: {
657+
value: 'deep',
658+
empty: {},
659+
},
660+
},
661+
},
662+
},
663+
},
664+
{
665+
id: 2,
666+
data: {
667+
values: [1, 2, 3],
668+
nullField: null,
669+
undefinedField: undefined,
670+
},
671+
},
672+
],
673+
},
674+
},
675+
},
676+
output: {
677+
complexNesting: {
678+
level1: {
679+
array: [
680+
{
681+
id: 1,
682+
data: null,
683+
meta: {
684+
tags: ['a', 'b', null, undefined],
685+
settings: {
686+
enabled: false,
687+
config: {
688+
nested: {
689+
value: 'deep',
690+
empty: {},
691+
},
692+
},
693+
},
694+
},
695+
},
696+
{
697+
id: 2,
698+
data: {
699+
values: [1, 2, 3],
700+
nullField: null,
701+
undefinedField: undefined,
702+
},
703+
},
704+
],
705+
},
706+
},
707+
},
708+
},
709+
{
710+
input: {
711+
primitiveTypes: {
712+
number: 42,
713+
string: 'hello',
714+
boolean: true,
715+
nullValue: null,
716+
undefinedValue: undefined,
717+
zero: 0,
718+
emptyString: '',
719+
falseValue: false,
720+
infinity: Infinity,
721+
negativeInfinity: -Infinity,
722+
nanValue: NaN,
723+
},
724+
},
725+
output: {
726+
primitiveTypes: {
727+
number: 42,
728+
string: 'hello',
729+
boolean: true,
730+
nullValue: null,
731+
undefinedValue: undefined,
732+
zero: 0,
733+
emptyString: '',
734+
falseValue: false,
735+
infinity: Infinity,
736+
negativeInfinity: -Infinity,
737+
nanValue: NaN,
738+
},
739+
},
740+
},
741+
{
742+
input: {
743+
arrayOfObjects: [
744+
{ id: 1, name: 'first', data: null },
745+
{ id: 2, name: 'second', data: { nested: { value: 'test' } } },
746+
{ id: 3, name: 'third', data: undefined },
747+
null,
748+
undefined,
749+
{ id: 4, name: 'fourth', data: [] },
750+
],
751+
},
752+
output: {
753+
arrayOfObjects: [
754+
{ id: 1, name: 'first', data: null },
755+
{ id: 2, name: 'second', data: { nested: { value: 'test' } } },
756+
{ id: 3, name: 'third', data: undefined },
757+
null,
758+
undefined,
759+
{ id: 4, name: 'fourth', data: [] },
760+
],
761+
},
762+
},
763+
];
764+
765+
it.each(tcData)(
766+
'should freeze the object',
767+
({ input, output }: { input: any; output: any }) => {
768+
const outcome = deepFreeze(input);
769+
expect(outcome).toEqual(output);
770+
771+
// Check if the object is frozen
772+
expect(Object.isFrozen(outcome)).toBe(true);
773+
774+
// Check if the object is immutable
775+
expect(Object.isSealed(outcome)).toBe(true);
776+
777+
// Check if the object is immutable
778+
expect(Object.isExtensible(outcome)).toBe(false);
779+
780+
// try to mutate the object and ensure it has no effect and relevant errors are thrown
781+
const firstKey = Object.keys(outcome)[0];
782+
if (firstKey) {
783+
expect(() => {
784+
outcome[firstKey] = 'mutated';
785+
}).toThrow();
786+
787+
expect(() => {
788+
delete outcome[firstKey];
789+
}).toThrow();
790+
}
791+
792+
expect(() => {
793+
outcome.newProperty = 'added';
794+
}).toThrow();
795+
},
796+
);
797+
});
541798
});

packages/analytics-js-common/src/types/EventApi.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import type { IntegrationOpts } from './Integration';
22
import type { Nullable } from './Nullable';
33
import type { ApiObject } from './ApiObject';
4+
import type { UserSessionKey } from './UserSessionStorage';
45

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

4647
export type ReadyCallback = () => void;
48+
49+
export type ResetOptions = {
50+
entries: {
51+
[key in UserSessionKey]?: boolean;
52+
};
53+
};

packages/analytics-js-common/src/types/IRudderAnalytics.ts

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type { Nullable } from './Nullable';
2-
import type { ApiCallback, ApiOptions } from './EventApi';
2+
import type { ApiCallback, ApiOptions, ResetOptions } from './EventApi';
33
import type { AnonymousIdOptions, LoadOptions } from './LoadOptions';
44
import type { ApiObject } from './ApiObject';
55
import type { ILogger, LogLevel, RSALogger } from './Logger';
@@ -76,6 +76,11 @@ export type AnalyticsAliasMethod = {
7676
(to: string, callback?: ApiCallback): void;
7777
};
7878

79+
export type AnalyticsResetMethod = {
80+
(options?: ResetOptions): void;
81+
(resetAnonymousId?: boolean): void;
82+
};
83+
7984
export interface IRudderAnalytics<T = any> {
8085
analyticsInstances: Record<string, T>;
8186
defaultAnalyticsKey: string;
@@ -132,11 +137,11 @@ export interface IRudderAnalytics<T = any> {
132137
group: AnalyticsGroupMethod;
133138

134139
/**
135-
* Clear user information
140+
* Clear user session information
136141
*
137-
* @param resetAnonymousId optionally clears anonymousId as well
142+
* @param options options for reset
138143
*/
139-
reset(resetAnonymousId?: boolean): void;
144+
reset: AnalyticsResetMethod;
140145

141146
/**
142147
* To get anonymousId set in the SDK

packages/analytics-js-common/src/utilities/object.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,15 @@ const getNormalizedObjectValue = (val: any): any => {
138138
const getNormalizedBooleanValue = (val: any, defVal: boolean): boolean =>
139139
typeof val === 'boolean' ? val : defVal;
140140

141+
const deepFreeze = <T>(obj: T): T => {
142+
Object.getOwnPropertyNames(obj).forEach(function (prop) {
143+
if (obj[prop as keyof T] && typeof obj[prop as keyof T] === 'object') {
144+
deepFreeze(obj[prop as keyof T]);
145+
}
146+
});
147+
return Object.freeze(obj);
148+
};
149+
141150
export {
142151
getValueByPath,
143152
hasValueByPath,
@@ -151,4 +160,5 @@ export {
151160
isObject,
152161
getNormalizedObjectValue,
153162
getNormalizedBooleanValue,
163+
deepFreeze,
154164
};

packages/analytics-js/.size-limit.mjs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ export default [
3636
name: 'Core - Modern - NPM (CJS)',
3737
path: 'dist/npm/modern/cjs/index.cjs',
3838
import: '*',
39-
limit: '28 KiB',
39+
limit: '28.5 KiB',
4040
},
4141
{
4242
name: 'Core - Modern - NPM (UMD)',
@@ -47,7 +47,7 @@ export default [
4747
{
4848
name: 'Core - Modern - CDN',
4949
path: 'dist/cdn/modern/iife/rsa.min.js',
50-
limit: '28 KiB',
50+
limit: '28.5 KiB',
5151
},
5252
{
5353
name: 'Core (Bundled) - Legacy - NPM (ESM)',
@@ -113,7 +113,7 @@ export default [
113113
name: 'Core (Content Script) - Modern - NPM (CJS)',
114114
path: 'dist/npm/modern/content-script/cjs/index.cjs',
115115
import: '*',
116-
limit: '41 KiB',
116+
limit: '41.5 KiB',
117117
},
118118
{
119119
name: 'Core (Content Script) - Modern - NPM (UMD)',

0 commit comments

Comments
 (0)