Skip to content

Commit

Permalink
feat: add more utilities
Browse files Browse the repository at this point in the history
  • Loading branch information
saikumarrs committed Nov 18, 2024
1 parent d821ee3 commit 7bd0cc9
Show file tree
Hide file tree
Showing 7 changed files with 400 additions and 8 deletions.
18 changes: 18 additions & 0 deletions packages/analytics-js-common/__tests__/utilities/errors.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { dispatchErrorEvent } from '../../src/utilities/errors';

describe('Errors - utilities', () => {
describe('dispatchErrorEvent', () => {
it('should dispatch an error event', () => {
const dispatchEvent = jest.fn();
const originalDispatchEvent = globalThis.dispatchEvent;

globalThis.dispatchEvent = dispatchEvent;
const error = new Error('Test error');
dispatchErrorEvent(error);
expect(dispatchEvent).toHaveBeenCalledWith(new ErrorEvent('error', { error }));

// Cleanup
globalThis.dispatchEvent = originalDispatchEvent;
});
});
});
251 changes: 250 additions & 1 deletion packages/analytics-js-common/__tests__/utilities/json.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import { clone } from 'ramda';
import { stringifyWithoutCircular } from '../../src/utilities/json';
import {
getSanitizedValue,
stringifyData,
stringifyWithoutCircular,
} from '../../src/utilities/json';

const identifyTraitsPayloadMock: Record<string, any> = {
firstName: 'Dummy Name',
Expand Down Expand Up @@ -47,6 +51,7 @@ const identifyTraitsPayloadMock: Record<string, any> = {
};

const circularReferenceNotice = '[Circular Reference]';
const bigIntNotice = '[BigInt]';

describe('Common Utils - JSON', () => {
describe('stringifyWithoutCircular', () => {
Expand Down Expand Up @@ -152,4 +157,248 @@ describe('Common Utils - JSON', () => {
);
});
});

describe('stringifyData', () => {
it('should stringify json excluding null values', () => {
// Define an object with null values in multiple levels along with other data types
const obj = {
key1: 'value1',
key2: null,
key3: {
key4: null,
key5: 'value5',
key10: undefined,
key6: {
key7: null,
key8: 'value8',
key9: undefined,
key11: [1, 2, null, 3],
},
},
};

expect(stringifyData(obj)).toBe(
'{"key1":"value1","key3":{"key5":"value5","key6":{"key8":"value8","key11":[1,2,null,3]}}}',
);
});

it('should stringify json without excluding null values', () => {
// Define an object with null values in multiple levels along with other data types
const obj = {
key1: 'value1',
key2: null,
key3: {
key4: null,
key5: 'value5',
key6: {
key7: null,
key8: 'value8',
},
},
};

expect(stringifyData(obj, false)).toBe(
'{"key1":"value1","key2":null,"key3":{"key4":null,"key5":"value5","key6":{"key7":null,"key8":"value8"}}}',
);
});

it('should stringify json after excluding certain keys', () => {
// Define an object with null values in multiple levels along with other data types
const obj = {
key1: 'value1',
key2: null,
key3: {
key4: null,
key5: 'value5',
key6: {
key7: null,
key8: 'value8',
},
},
};

const keysToExclude = ['key1', 'key7'];

expect(stringifyData(obj, true, keysToExclude)).toBe(
'{"key3":{"key5":"value5","key6":{"key8":"value8"}}}',
);

expect(stringifyData(obj, false, keysToExclude)).toBe(
'{"key2":null,"key3":{"key4":null,"key5":"value5","key6":{"key8":"value8"}}}',
);
});
});

describe('getSanitizedValue', () => {
const mockLogger = {
warn: jest.fn(),
};

it('should sanitize json without excluding null and undefined values', () => {
const obj = {
a: 1,
b: null,
c: 'value',
d: undefined,
i: () => {},
e: {
f: 2,
g: null,
h: 'value',
i: undefined,
j: {
k: 3,
l: null,
m: 'value',
n: [1, 2, 3],
o: [1, 2, 3, new Date()],
s: () => {},
},
},
};

expect(getSanitizedValue(obj)).toEqual(obj);
});

it('should sanitize json after replacing BigInt and circular references', () => {
const obj = {
a: BigInt(1),
b: undefined,
c: 'value',
d: {
e: BigInt(2),
f: undefined,
g: 'value',
h: {
i: BigInt(3),
j: undefined,
k: 'value',
},
},
};

obj.myself = obj;
obj.d.myself2 = obj.d;
obj.d.h.myself3 = obj.d;

expect(getSanitizedValue(obj, mockLogger)).toEqual({
a: bigIntNotice,
c: 'value',
b: undefined,
myself: circularReferenceNotice,
d: {
e: bigIntNotice,
g: 'value',
f: undefined,
myself2: circularReferenceNotice,
h: {
i: bigIntNotice,
k: 'value',
j: undefined,
myself3: circularReferenceNotice,
},
},
});

expect(mockLogger.warn).toHaveBeenCalledTimes(6);

expect(mockLogger.warn).toHaveBeenNthCalledWith(
1,
'JSON:: A bad data (like circular reference, BigInt) has been detected in the object and the property "a" has been dropped from the output.',
);

expect(mockLogger.warn).toHaveBeenNthCalledWith(
2,
'JSON:: A bad data (like circular reference, BigInt) has been detected in the object and the property "e" has been dropped from the output.',
);

expect(mockLogger.warn).toHaveBeenNthCalledWith(
3,
'JSON:: A bad data (like circular reference, BigInt) has been detected in the object and the property "i" has been dropped from the output.',
);

expect(mockLogger.warn).toHaveBeenNthCalledWith(
4,
'JSON:: A bad data (like circular reference, BigInt) has been detected in the object and the property "myself3" has been dropped from the output.',
);

expect(mockLogger.warn).toHaveBeenNthCalledWith(
5,
'JSON:: A bad data (like circular reference, BigInt) has been detected in the object and the property "myself2" has been dropped from the output.',
);

expect(mockLogger.warn).toHaveBeenNthCalledWith(
6,
'JSON:: A bad data (like circular reference, BigInt) has been detected in the object and the property "myself" has been dropped from the output.',
);
});

it('should sanitize json even if it contains reused objects', () => {
const obj = {
a: BigInt(1),
b: undefined,
c: 'value',
d: {
e: BigInt(2),
f: undefined,
g: 'value',
h: {
i: BigInt(3),
j: undefined,
k: 'value',
},
},
};

const reusableArray = [1, 2, 3];
const reusableObject = { dummy: 'val' };
obj.reused = reusableArray;
obj.reusedAgain = [1, 2, reusableArray];
obj.reusedObj = reusableObject;
obj.reusedObjAgain = { reused: reusableObject };

obj.d.reused = reusableArray;
obj.d.h.reused = reusableObject;
obj.d.h.reusedAgain = [1, 2, reusableArray];

expect(getSanitizedValue(obj)).toEqual({
a: bigIntNotice,
c: 'value',
b: undefined,
reused: [1, 2, 3],
reusedAgain: [1, 2, [1, 2, 3]],
reusedObj: { dummy: 'val' },
reusedObjAgain: { reused: { dummy: 'val' } },
d: {
e: bigIntNotice,
g: 'value',
f: undefined,
reused: [1, 2, 3],
h: {
i: bigIntNotice,
k: 'value',
j: undefined,
reused: { dummy: 'val' },
reusedAgain: [1, 2, [1, 2, 3]],
},
},
});
});

it('should sanitize all data types', () => {
const array = [1, 2, 3];
const number = 1;
const string = '';
const object = {};
const date = new Date(2023, 1, 20, 0, 0, 0);

expect(getSanitizedValue(array)).toEqual(array);
expect(getSanitizedValue(number)).toEqual(number);
expect(getSanitizedValue(string)).toEqual(string);
expect(getSanitizedValue(object)).toEqual(object);
expect(getSanitizedValue(date)).toEqual(date);
expect(getSanitizedValue(null)).toEqual(null);
expect(getSanitizedValue(undefined)).toEqual(undefined);
});
});
});
8 changes: 6 additions & 2 deletions packages/analytics-js-common/src/constants/logMessages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,15 @@ const CIRCULAR_REFERENCE_WARNING = (context: string, key: string): string =>

const JSON_STRINGIFY_WARNING = `Failed to convert the value to a JSON string.`;

const BAD_DATA_WARNING = (context: string, key: string): string =>
`${context}${LOG_CONTEXT_SEPARATOR}A bad data (like circular reference, BigInt) has been detected in the object and the property "${key}" has been dropped from the output.`;

export {
LOG_CONTEXT_SEPARATOR,
SCRIPT_ALREADY_EXISTS_ERROR,
SCRIPT_LOAD_ERROR,
SCRIPT_LOAD_TIMEOUT_ERROR,
CIRCULAR_REFERENCE_WARNING,
JSON_STRINGIFY_WARNING
}
JSON_STRINGIFY_WARNING,
BAD_DATA_WARNING,
};
8 changes: 8 additions & 0 deletions packages/analytics-js-common/src/utilities/checks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,13 @@ const isUndefined = (value: any): value is undefined => typeof value === 'undefi
*/
const isNullOrUndefined = (value: any): boolean => isNull(value) || isUndefined(value);

/**
* Checks if the input is a BigInt
* @param value input value
* @returns True if the input is a BigInt
*/
const isBigInt = (value: any): value is bigint => typeof value === 'bigint';

/**
* A function to check given value is defined
* @param value input value
Expand Down Expand Up @@ -74,4 +81,5 @@ export {
isDefined,
isDefinedAndNotNull,
isDefinedNotNullAndNotEmptyString,
isBigInt,
};
11 changes: 10 additions & 1 deletion packages/analytics-js-common/src/utilities/errors.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { isTypeOfError } from './checks';
import { stringifyWithoutCircular } from './json';

const MANUAL_ERROR_IDENTIFIER = '[MANUAL ERROR]';

/**
* Get mutated error with issue prepended to error message
* @param err Original error
Expand All @@ -17,4 +19,11 @@ const getMutatedError = (err: any, issue: string): Error => {
return finalError;
};

export { getMutatedError };
const dispatchErrorEvent = (error: any) => {
if (isTypeOfError(error)) {
error.stack = `${error.stack ?? ''}\n${MANUAL_ERROR_IDENTIFIER}`;
}
(globalThis as typeof window).dispatchEvent(new ErrorEvent('error', { error }));
};

export { getMutatedError, dispatchErrorEvent, MANUAL_ERROR_IDENTIFIER };
Loading

0 comments on commit 7bd0cc9

Please sign in to comment.