Skip to content

Commit

Permalink
Mobx integration added
Browse files Browse the repository at this point in the history
  • Loading branch information
RNEvok committed Jun 30, 2024
1 parent db0f1ed commit d0b0d34
Show file tree
Hide file tree
Showing 11 changed files with 335 additions and 31 deletions.
99 changes: 79 additions & 20 deletions Connector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,23 +4,19 @@ import { EventHandleError, ScenarioHandleError } from './Errors';
import { SOCKET_EVENTS_LISTEN, SOCKET_EVENTS_EMIT } from './api/api';
import { SPECIAL_INSTRUCTIONS_TABLE, SPECIAL_INSTRUCTIONS } from './constants/events';
import { io, Socket } from "socket.io-client";
import { codebudConsoleLog, codebudConsoleWarn, jsonStringifyKeepMeta, stringifyIfNotString, errorToJSON } from './helpers/helperFunctions';
import { codebudConsoleLog, codebudConsoleWarn, jsonStringifyKeepMeta, stringifyIfNotString, errorToJSON, jsonStringifyPossiblyCircular, emptyMobxStoreMonitor, removeCircularReferencesFromObject } from './helpers/helperFunctions';
import { getProcessEnv } from './helpers/environment';
import { getBrowserInfo } from './helpers/browserInfo';
import { getEnvironmentPlatform } from './helpers/platform';
import { getOS } from './helpers/os';
import { remoteSettingsService } from './services/remoteSettingsService';
import { idService } from './services/idService';
import { asyncStoragePlugin } from './asyncStorage/asyncStorage';
import { localStoragePlugin } from './localStorage/localStorage';
import moment from 'moment';

class Connector {
private _eventListenersTable: T.EventListenersTable = {};
private _currentInterceptedReduxActionId = 0;
private _currentInterceptedStorageActionId = 0;
private _currentCapturedEventId = 0;
private _currentCrashReportId = 0;
private _currentInterceptedTanStackQueryEventId = 0;
private _connectorInitiated: boolean = false;
private _apiKey: string = "";
private _projectInfo: T.ProjectInfo | null = null;
Expand Down Expand Up @@ -63,6 +59,10 @@ class Connector {
private _swizzledWindowUnhandledListeners: boolean = false;
private _windowInitialOnunhandledrejection: any;
private _windowInitialOnerror: any;
private _sendMobxStateBatchingTimer: NodeJS.Timeout | null = null;
private _currentMobxStateCopy: any = null;
private _sendMobxEventsBatchingTimer: NodeJS.Timeout | null = null;
private _currentMobxEventsBatch: T.InterceptedMobxEventPreparedData[] = [];

public lastEvent: T.RemoteEvent | null = null;

Expand Down Expand Up @@ -92,11 +92,11 @@ class Connector {
}
};

private _encryptData(json: any): {result: string, ok: boolean} {
private _encryptData(json: any, removeCircularReferences?: boolean): {result: string, ok: boolean} {
if (!this._encryption)
return jsonStringifyKeepMeta(json);
return jsonStringifyKeepMeta(json, removeCircularReferences);

return this._encryption.encryptData(json);
return this._encryption.encryptData(json, removeCircularReferences);
};

private _fillInstructionsTable(instructions: T.Instruction[]) {
Expand Down Expand Up @@ -380,9 +380,9 @@ class Connector {
public async codebudHandleDispatchedReduxAction(action: T.InterceptedReduxAction, batchingTimeMs: number) {
if (this._socket?.connected) {
const timestamp = moment().valueOf();
const actionId = this._currentInterceptedReduxActionId++;
const actionId = idService.currentInterceptedReduxActionId;
const _stackTraceData = await this._getStackTraceFn?.(new Error(''), this._stackTraceOptions);
const reduxActionData = {actionId: `RA_${actionId}`, action, timestamp, _stackTraceData};
const reduxActionData = {actionId, action, timestamp, _stackTraceData};
jsonStringifyKeepMeta(reduxActionData).ok && this._currentReduxActionsBatch.push(reduxActionData);

if (this._sendReduxActionsBatchingTimer)
Expand Down Expand Up @@ -423,9 +423,9 @@ class Connector {
private async _handleInterceptedStorageAction(action: string, data?: any) {
if (this._socket?.connected) {
const timestamp = moment().valueOf();
const storageActionId = this._currentInterceptedStorageActionId++;
const storageActionId = idService.currentInterceptedStorageActionId;
const _stackTraceData = await this._getStackTraceFn?.(new Error(''), this._stackTraceOptions);
const storageActionData = {storageActionId: `SA_${storageActionId}`, action, data, timestamp, _stackTraceData};
const storageActionData = {storageActionId, action, data, timestamp, _stackTraceData};
jsonStringifyKeepMeta(storageActionData).ok && this._currentStorageActionsBatch.push(storageActionData);

if (this._sendStorageActionsBatchingTimer)
Expand Down Expand Up @@ -460,11 +460,11 @@ class Connector {
public async captureEvent(title: string, data: any) {
if (this._socket?.connected) {
const timestamp = moment().valueOf();
const capturedEventId = this._currentCapturedEventId++;
const capturedEventId = idService.currentCapturedEventId;

const _stackTraceData = await this._getStackTraceFn?.(new Error(''), this._stackTraceOptions);

const encryptedData = this._encryptData({timestamp, capturedEventId: `UCE_${capturedEventId}`, title, data, _stackTraceData});
const encryptedData = this._encryptData({timestamp, capturedEventId, title, data, _stackTraceData});
encryptedData.ok && this._socket?.emit(SOCKET_EVENTS_EMIT.CAPTURE_EVENT, encryptedData.result);
}
}
Expand All @@ -474,7 +474,7 @@ class Connector {

if (this._socket?.connected) {
const timestamp = moment().valueOf();
const crashReportId = this._currentCrashReportId++;
const crashReportId = idService.currentCrashReportId;

let _stackTraceData;
if (this._getStackTraceFn) {
Expand All @@ -483,7 +483,7 @@ class Connector {
else if (type === "React ErrorBoundary" && data.componentStack)
_stackTraceData = await this._getStackTraceFn(data.componentStack, this._stackTraceOptions);
}
const encryptedData = this._encryptData({timestamp, crashReportId: `ACR_${crashReportId}`, type, data, _stackTraceData});
const encryptedData = this._encryptData({timestamp, crashReportId, type, data, _stackTraceData});
encryptedData.ok && this._socket?.emit(SOCKET_EVENTS_EMIT.CAPTURE_CRASH_REPORT, encryptedData.result);
}
}
Expand Down Expand Up @@ -595,9 +595,9 @@ class Connector {
private async _proceedInterceptedTanStackQueryEvent(event: T.TanStackQueryCacheEvent, batchingTimeMs: number) {
if (this._socket?.connected) {
const timestamp = moment().valueOf();
const tanStackQueryEventId = this._currentInterceptedTanStackQueryEventId++;
const tanStackQueryEventId = idService.currentInterceptedTanStackQueryEventId;
// const _stackTraceData = await this._getStackTraceFn?.(new Error(''), this._stackTraceOptions);
const tanStackQueryEventData: T.InterceptedTanStackQueryEventPreparedData = {tanStackQueryEventId: `TQE_${tanStackQueryEventId}`, event, timestamp};
const tanStackQueryEventData: T.InterceptedTanStackQueryEventPreparedData = {tanStackQueryEventId, event, timestamp};
jsonStringifyKeepMeta(tanStackQueryEventData).ok && this._currentTanStackQueryEventsBatch.push(tanStackQueryEventData);

if (this._sendTanStackQueryEventsBatchingTimer)
Expand Down Expand Up @@ -641,7 +641,56 @@ class Connector {
encryptedData.ok && this._socket?.emit(SOCKET_EVENTS_EMIT.SAVE_CONTEXT_VALUE_COPY, encryptedData.result);
}, waitMs);
}
};
}

public createMobxStoreMonitor(store: any, batchingTimeMs: number): T.MobxStoreMonitor {
try {
return [
() => jsonStringifyPossiblyCircular(store),
(currentMobxStateCopyStr: string) => {
const previousMobxStateCopyStr = jsonStringifyPossiblyCircular(this._currentMobxStateCopy);
this._currentMobxStateCopy = JSON.parse(currentMobxStateCopyStr);

if (this._socket?.connected && previousMobxStateCopyStr !== currentMobxStateCopyStr) {
if (this._sendMobxStateBatchingTimer)
clearTimeout(this._sendMobxStateBatchingTimer);

this._sendMobxStateBatchingTimer = setTimeout(() => {
const encryptedData = this._encryptData({state: this._currentMobxStateCopy, timestamp: moment().valueOf()}); // Here we aren't passing removeCircularReferences flag because to this moment they are already removed
encryptedData.ok && this._socket?.emit(SOCKET_EVENTS_EMIT.SAVE_MOBX_STATE_COPY, encryptedData.result);
}, batchingTimeMs);
}
}
];
} catch (e) {
codebudConsoleWarn(`Error while trying to create MobxStoreMonitor`, e);
return emptyMobxStoreMonitor;
}
}

public createMobxEventHandler(batchingTimeMs: number) {
return async (event: T.MobxSpyEvent) => {
if (event.type !== "action")
return;

if (this._socket?.connected) {
const timestamp = moment().valueOf();
const mobxEventId = idService.currentInterceptedMobxEventId;
const _stackTraceData = await this._getStackTraceFn?.(new Error(''), this._stackTraceOptions);
const mobxEventData = {mobxEventId, event: removeCircularReferencesFromObject(event), timestamp, _stackTraceData};
jsonStringifyKeepMeta(mobxEventData).ok && this._currentMobxEventsBatch.push(mobxEventData);

if (this._sendMobxEventsBatchingTimer)
clearTimeout(this._sendMobxEventsBatchingTimer);

this._sendMobxEventsBatchingTimer = setTimeout(() => {
const encryptedData = this._encryptData({events: this._currentMobxEventsBatch});
encryptedData.ok && this._socket?.emit(SOCKET_EVENTS_EMIT.SAVE_MOBX_EVENTS_BATCH, encryptedData.result);
this._currentMobxEventsBatch = [];
}, batchingTimeMs);
}
};
}

public disconnect() {
this._connectorInitiated = false;
Expand Down Expand Up @@ -708,6 +757,16 @@ class Connector {
clearTimeout(this._sendTanStackQueriesDataBatchingTimer);

this._currentTanStackQueriesDataCopy = null;

if (this._sendMobxStateBatchingTimer)
clearTimeout(this._sendMobxStateBatchingTimer);

this._currentMobxStateCopy = null;

if (this._sendMobxEventsBatchingTimer)
clearTimeout(this._sendMobxEventsBatchingTimer);

this._currentMobxEventsBatch = [];

this._unsubscribeFromTanStackQueryEvents?.();
this._unsubscribeFromTanStackQueryEvents = undefined;
Expand Down
90 changes: 89 additions & 1 deletion __tests__/helpers/helperFunctions.test.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,15 @@
import { test, expect } from '@jest/globals';
import { stringifyIfNotString, jsonStringifyKeepMeta } from './../../helpers/helperFunctions';
import { stringifyIfNotString, jsonStringifyKeepMeta, wrapInObjectIfNotObject, jsonStringifyPossiblyCircular, removeCircularReferencesFromObject } from './../../helpers/helperFunctions';
import { makeRandomString } from './../../helpers/random';
import { CONFIG } from '../../config';

const getTestObjectWithCircularReference = () => {
const circularObject: any = {data: "123"};
circularObject.circ = circularObject;

return circularObject;
};

test("stringifyIfNotString test", () => {
const userStr = `{"name":"Alex","id":330,"numberOfCars":2}`;
const userStrSingleQuote = `{'name':'Alex','id':330,'numberOfCars':2}`;
Expand Down Expand Up @@ -42,6 +49,75 @@ test("stringifyIfNotString test", () => {
expect(test4).toEqual(JSON.stringify(company));
});

test("wrapInObjectIfNotObject test", () => {
const test1 = wrapInObjectIfNotObject(undefined);
const test2 = wrapInObjectIfNotObject("");
const test3 = wrapInObjectIfNotObject(123, "nums");
const test4 = wrapInObjectIfNotObject(null);
const test5 = wrapInObjectIfNotObject([]);
const test6 = wrapInObjectIfNotObject({});
const test7 = wrapInObjectIfNotObject({a: 1, b: 2, c: 3});

expect(test1).toEqual({data: undefined});
expect(test2).toEqual({data: ""});
expect(test3).toEqual({nums: 123});
expect(test4).toEqual(null);
expect(test5).toEqual([]);
expect(test6).toEqual({});
expect(test7).toEqual({a: 1, b: 2, c: 3});
});

test("jsonStringifyPossiblyCircular test", () => {
const test1 = jsonStringifyPossiblyCircular([]);
const test2 = jsonStringifyPossiblyCircular({});
const test3 = jsonStringifyPossiblyCircular({a: 1, b: 2, c: 3});
const data4 = {a: {x: 1, y: 2}, b: {x: 0, y: -1}};
const test4 = jsonStringifyPossiblyCircular(data4);
const test4Reversed = JSON.parse(test4);

const circularObject = getTestObjectWithCircularReference();

const test5 = jsonStringifyPossiblyCircular(circularObject);

expect(test1).toEqual("[]");
expect(test2).toEqual("{}");
expect(test3).toEqual(`{"a":1,"b":2,"c":3}`);
expect(test4).toEqual(`{"a":{"x":1,"y":2},"b":{"x":0,"y":-1}}`);
expect(test4Reversed).toEqual(data4);
expect(test5).toEqual(`{"data":"123"}`);
});

test("removeCircularReferencesFromObject test", () => {
const a: any = [];
const b = {};
const c = {a: 1, b: 2, c: 3};
const d = {a: {x: 1, y: 2}, b: {x: 0, y: -1}};

const test1 = removeCircularReferencesFromObject(a);
const test2 = removeCircularReferencesFromObject(b);
const test3 = removeCircularReferencesFromObject(c);
const test4 = removeCircularReferencesFromObject(d);

const circularObject = getTestObjectWithCircularReference();

const test5 = removeCircularReferencesFromObject(circularObject);

const aRefersToB: any = {x: 10};
const bRefersToA: any = {y: 100};
aRefersToB.linkB = bRefersToA;
bRefersToA.linkA = aRefersToB;
const innerRecursiveLinksObject = {aRefersToB, bRefersToA};

const test6 = removeCircularReferencesFromObject(innerRecursiveLinksObject);

expect(test1).toEqual(a);
expect(test2).toEqual(b);
expect(test3).toEqual(c);
expect(test4).toEqual(d);
expect(test5).toEqual({data: "123"});
expect(test6).toEqual({aRefersToB: { x: 10, linkB: { y: 100 } }});
});

test("jsonStringifyKeepMeta test", () => {
const data1 = {
a: 101,
Expand All @@ -66,4 +142,16 @@ test("jsonStringifyKeepMeta test", () => {
const test2 = jsonStringifyKeepMeta(data2);
expect(test2.ok).toBe(false);
expect(test2.result).toEqual(`{\"message\":\"Payload data was skipped (1024 Kb limit exceeded)\"}`);

const data3 = {
...data1,
circularObject: getTestObjectWithCircularReference()
};

const test3 = () => jsonStringifyKeepMeta(data3);
expect(test3).toThrow();

const test4 = jsonStringifyKeepMeta(data3, true);
expect(test4.ok).toBe(true);
expect(test4.result).toEqual(`{"a":101,"b":"abc","c":true,"d":null,"f":[1,2,3],"g":{"x":10,"y":12},"circularObject":{"data":"123"}}`);
});
4 changes: 2 additions & 2 deletions encryption/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,13 @@ export class EncryptionPlugin {
return randomBytes(box.nonceLength);
};

public encryptData(json: any): {result: string, ok: boolean} {
public encryptData(json: any, removeCircularReferences?: boolean): {result: string, ok: boolean} {
try {
if (!this._sharedKey)
throw new Error("Shared key not generated");

const nonce = this.newNonce();
const jsonStringified = jsonStringifyKeepMeta(json);
const jsonStringified = jsonStringifyKeepMeta(json, removeCircularReferences);

const messageUint8 = decodeUTF8(jsonStringified.result);

Expand Down
44 changes: 39 additions & 5 deletions helpers/helperFunctions.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { CONFIG } from "../config";
import { ObjectT } from "../types/types";
import { ObjectT, MobxStoreMonitor } from "../types/types";
import { payloadSizeValidator } from "./payloadSizeValidator";

export function delay(ms: number) {
Expand All @@ -15,6 +15,13 @@ export const stringifyIfNotString = (data: any) => {
return JSON.stringify(data);
}

export const wrapInObjectIfNotObject = (obj: any, fallbackDataKey: string = "data") => {
if (typeof obj === "object")
return obj;

return {[fallbackDataKey]: obj};
}

const memo = {
warn: {
data: "",
Expand All @@ -39,8 +46,29 @@ export const codebudConsoleLog = (...data: any[]) => {
console.log(`${CONFIG.PRODUCT_NAME}:`, ...data);
}

export const emptyMiddleware = () => (next: any) => (action: any) => {
return next(action);
export const emptyMobxStoreMonitor: MobxStoreMonitor = [
() => "",
() => {}
];

export const jsonStringifyPossiblyCircular = (data: ObjectT<any>) => {
const seen = new WeakSet();

return JSON.stringify(
data,
(k, v) => {
if (typeof v === 'object' && v) {
if (seen.has(v))
return;
seen.add(v);
}
return v;
}
);
}

export const removeCircularReferencesFromObject = (data: ObjectT<any>) => {
return JSON.parse(jsonStringifyPossiblyCircular(data));
}

export const getFormDataMeta = (fData: FormData) => {
Expand Down Expand Up @@ -70,15 +98,21 @@ export const getFormDataMeta = (fData: FormData) => {
}

// Custom JSON.stringify wrapper that keeps as much metadata as possible
export const jsonStringifyKeepMeta = (data: ObjectT<any>): {result: string, ok: boolean} => {
export const jsonStringifyKeepMeta = (data: ObjectT<any>, removeCircularReferences: boolean = false): {result: string, ok: boolean} => {
const dataStringified = JSON.stringify(
data,
function(key, value) {
const type = typeof value;

switch (type) {
case "object":
return (value instanceof FormData) ? getFormDataMeta(value) : value;
if (value instanceof FormData)
return getFormDataMeta(value);

if (removeCircularReferences && value)
return removeCircularReferencesFromObject(value);

return value;
case "function":
return "Function (...)";
default:
Expand Down
Loading

0 comments on commit d0b0d34

Please sign in to comment.