From 8de225033521d068dca835e4cdd07b45e36197f3 Mon Sep 17 00:00:00 2001 From: RNEvok <81921589+RNEvok@users.noreply.github.com> Date: Thu, 18 Jul 2024 17:29:52 +0300 Subject: [PATCH] localStorage / AsyncStorage getEntireStorage feature + build diagnostics + ReadMe update --- Connector.ts | 42 ++++++++++++++++++++++++++++++++++++ README.md | 1 + api/api.ts | 2 ++ asyncStorage/asyncStorage.ts | 31 ++++++++++++++++++++++++++ config.ts | 3 ++- localStorage/localStorage.ts | 29 +++++++++++++++++++++++++ package.json | 2 +- types/types.ts | 14 +++++++++++- 8 files changed, 121 insertions(+), 3 deletions(-) diff --git a/Connector.ts b/Connector.ts index 21b80c9..c09c416 100644 --- a/Connector.ts +++ b/Connector.ts @@ -38,9 +38,11 @@ class Connector { private _encryption: any = null; private _asyncStorageHandler: any = null; private _trackAsyncStorage: (() => void) | undefined; + private _getEntireAsyncStorageAsObject: (() => Promise>) | undefined; private _untrackAsyncStorage: (() => void) | undefined; private _localStorageHandler: any = null; private _trackLocalStorage: (() => void) | undefined; + private _getEntireLocalStorageAsObject: (() => T.ObjectT) | undefined; private _untrackLocalStorage: (() => void) | undefined; private _storageActionsBatchingTimeMs: number = 500; private _sendStorageActionsBatchingTimer: NodeJS.Timeout | null = null; @@ -270,6 +272,32 @@ class Connector { ); }; + private async _getEntireStorageAsObject() { + let obj: T.ObjectT; + let storageType: T.StorageType = "unknown"; + + if (this._getEntireLocalStorageAsObject) { + obj = this._getEntireLocalStorageAsObject(); + storageType = "localStorage"; + } else if (this._getEntireAsyncStorageAsObject) { + obj = await this._getEntireAsyncStorageAsObject(); + storageType = "AsyncStorage"; + } else { + const info = "Unable to get entire storage as object: No AsyncStorage / localStorage monitor set up"; + codebudConsoleWarn(info); + obj = {info}; + } + + const storageSnapshot: T.StorageSnapshot = { + timestamp: moment().valueOf(), + storageType, + storageAsObject: obj + }; + + const encryptedData = this._encryptData(storageSnapshot); + encryptedData.ok && this._socket?.emit(SOCKET_EVENTS_EMIT.SAVE_FULL_STORAGE_SNAPSHOT, encryptedData.result); + }; + public init(apiKey: string, instructions: T.Instruction[], usersCustomCallback: T.OnEventUsersCustomCallback, config?: T.PackageConfig) { this._apiKey = apiKey; this._fillInstructionsTable(instructions); @@ -332,6 +360,16 @@ class Connector { this._socket.on(SOCKET_EVENTS_LISTEN.SAVE_NEW_REMOTE_SETTINGS, (r: T.RemoteSettings) => remoteSettingsService.onGotNewRemoteSettings(r)); + this._socket.on(SOCKET_EVENTS_LISTEN.FORCE_REFRESH, (data: T.ForceRefreshPayload) => { + switch (data.type) { + case "storage": + this._getEntireStorageAsObject(); + break; + default: + break; + } + }); + this._socket.on(SOCKET_EVENTS_LISTEN.CONNECT_ERROR, (err) => { codebudConsoleWarn(`Socket connect_error: ${err.message}`); }); @@ -445,6 +483,7 @@ class Connector { // passing Connector class context to asyncStoragePlugin function const controlFunctions = asyncStoragePlugin.bind(this as any)(ignoreKeys); this._trackAsyncStorage = controlFunctions.trackAsyncStorage; + this._getEntireAsyncStorageAsObject = controlFunctions.getEntireAsyncStorageAsObject; this._untrackAsyncStorage = controlFunctions.untrackAsyncStorage; } @@ -454,6 +493,7 @@ class Connector { // passing Connector class context to localStoragePlugin function const controlFunctions = localStoragePlugin.bind(this as any)(ignoreKeys); this._trackLocalStorage = controlFunctions.trackLocalStorage; + this._getEntireLocalStorageAsObject = controlFunctions.getEntireLocalStorageAsObject; this._untrackLocalStorage = controlFunctions.untrackLocalStorage; } @@ -729,6 +769,7 @@ class Connector { if (this._asyncStorageHandler) { this._untrackAsyncStorage?.(); this._untrackAsyncStorage = undefined; + this._getEntireAsyncStorageAsObject = undefined; this._trackAsyncStorage = undefined; this._asyncStorageHandler = null; } @@ -736,6 +777,7 @@ class Connector { if (this._localStorageHandler) { this._untrackLocalStorage?.(); this._untrackLocalStorage = undefined; + this._getEntireLocalStorageAsObject = undefined; this._trackLocalStorage = undefined; this._localStorageHandler = null; } diff --git a/README.md b/README.md index 9f520a3..93f77bd 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,7 @@ JavaScript package for remote app testing & debugging * Redux state observer + action monitor * Zustand state observer * TanStack query observer +* MobX state observer * React contexts monitoring * Storage actions monitoring * Feature management diff --git a/api/api.ts b/api/api.ts index 07bc930..4b9f077 100644 --- a/api/api.ts +++ b/api/api.ts @@ -12,6 +12,7 @@ const SOCKET_EVENTS_LISTEN = { DISCONNECT: "disconnect", SAVE_NEW_REMOTE_SETTINGS: "saveNewRemoteSettings", ADMIN_CONNECTED: "ADMIN_CONNECTED", + FORCE_REFRESH: "forceRefresh" }; const SOCKET_EVENTS_EMIT = { @@ -27,6 +28,7 @@ const SOCKET_EVENTS_EMIT = { SAVE_INTERCEPTED_RESPONSE: "saveInterceptedResponse", SAVE_MOBILE_APP_STATE: "saveMobileAppState", SAVE_INTERCEPTED_STORAGE_ACTIONS_BATCH: "saveInterceptedStorageActionsBatch", + SAVE_FULL_STORAGE_SNAPSHOT: "saveFullStorageSnapshot", CAPTURE_EVENT: "captureEvent", CAPTURE_CRASH_REPORT : "captureCrashReport", SAVE_TANSTACK_QUERIES_DATA_COPY: "saveTanStackQueriesDataCopy", diff --git a/asyncStorage/asyncStorage.ts b/asyncStorage/asyncStorage.ts index 2b2eead..c95e57d 100644 --- a/asyncStorage/asyncStorage.ts +++ b/asyncStorage/asyncStorage.ts @@ -1,3 +1,7 @@ +import { CONFIG } from "./../config"; +import { codebudConsoleWarn, errorToJSON, stringIsJson } from "./../helpers/helperFunctions"; +import { ObjectT } from "./../types"; + type ConnectorContext = { _asyncStorageHandler: any; _handleInterceptedStorageAction: (action: string, data?: any) => void; @@ -114,6 +118,32 @@ export function asyncStoragePlugin (this: ConnectorContext, ignoreKeys: string[] return swizzMultiMerge(pairs, callback) } + const getEntireAsyncStorageAsObject = async (): Promise> => { + try { + if (!isSwizzled) + throw new Error("AsyncStorage monitor not set up"); + + const keys = await this._asyncStorageHandler.getAllKeys(); + + if (keys.length > CONFIG.PAYLOAD_LIMITS.MAX_KEYS_IN_STORAGE) + throw new Error(`AsyncStorage is too large to handle (${keys.length} keys found)`); + + const values = await swizzMultiGet(keys); + const obj: ObjectT = {}; + + values.forEach(([key, value]: [string, string]) => { + obj[key] = stringIsJson(value) ? JSON.parse(value) : value; + }); + + return obj; + } catch (e) { + const info = "Unable to prepare entire AsyncStorage as object"; + codebudConsoleWarn(info, e); + + return { info, error: errorToJSON(e) }; + } + } + const trackAsyncStorage = () => { if (isSwizzled) return; @@ -172,6 +202,7 @@ export function asyncStoragePlugin (this: ConnectorContext, ignoreKeys: string[] return { trackAsyncStorage, + getEntireAsyncStorageAsObject, untrackAsyncStorage }; } \ No newline at end of file diff --git a/config.ts b/config.ts index f2afa5c..33d09d7 100644 --- a/config.ts +++ b/config.ts @@ -32,7 +32,8 @@ const CONFIG_INNER = { PAYLOAD_LIMITS: { MAX_KB_SIZE: PAYLOAD_LIMIT_MAX_KB_SIZE, MAX_BYTE_SIZE: 1024 * PAYLOAD_LIMIT_MAX_KB_SIZE, - MIN_STRING_LENGTH_POSSIBLE_OVERLOAD: 1024 * PAYLOAD_LIMIT_MAX_KB_SIZE / 4 // MAX_BYTE_SIZE / 4 (4 is max possible byteSize of UTF char) + MIN_STRING_LENGTH_POSSIBLE_OVERLOAD: 1024 * PAYLOAD_LIMIT_MAX_KB_SIZE / 4, // MAX_BYTE_SIZE / 4 (4 is max possible byteSize of UTF char) + MAX_KEYS_IN_STORAGE: 2048 } }; diff --git a/localStorage/localStorage.ts b/localStorage/localStorage.ts index 5607183..c1eb5e4 100644 --- a/localStorage/localStorage.ts +++ b/localStorage/localStorage.ts @@ -1,3 +1,7 @@ +import { CONFIG } from "./../config"; +import { codebudConsoleWarn, errorToJSON, stringIsJson } from "./../helpers/helperFunctions"; +import { ObjectT } from "./../types"; + type ConnectorContext = { _localStorageHandler: any; _handleInterceptedStorageAction: (action: string, data?: any) => void; @@ -43,6 +47,30 @@ export function localStoragePlugin (this: ConnectorContext, ignoreKeys: string[] swizzRemoveItem(key); } + const getEntireLocalStorageAsObject = (): ObjectT => { + try { + if (!isSwizzled) + throw new Error("localStorage monitor not set up"); + + const obj = {...this._localStorageHandler}; + const keys = Object.keys(obj); + + if (keys.length > CONFIG.PAYLOAD_LIMITS.MAX_KEYS_IN_STORAGE) + throw new Error(`localStorage is too large to handle (${keys.length} keys found)`); + + for (const key of keys) + if (stringIsJson(obj[key])) + obj[key] = JSON.parse(obj[key]); + + return obj; + } catch (e) { + const info = "Unable to prepare entire localStorage as object"; + codebudConsoleWarn(info, e); + + return { info, error: errorToJSON(e) }; + } + } + const trackLocalStorage = () => { if (isSwizzled) return; @@ -81,6 +109,7 @@ export function localStoragePlugin (this: ConnectorContext, ignoreKeys: string[] return { trackLocalStorage, + getEntireLocalStorageAsObject, untrackLocalStorage }; } \ No newline at end of file diff --git a/package.json b/package.json index de4820b..907ee5f 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "scripts": { "test": "jest", "start": "ts-node index.ts", - "build": "tsc --build tsconfig.build.json", + "build": "tsc --build tsconfig.build.json --diagnostics", "clean": "tsc --build --clean" }, "author": "Aleksandr Nikolotov", diff --git a/types/types.ts b/types/types.ts index 12e9285..5fe1c8b 100644 --- a/types/types.ts +++ b/types/types.ts @@ -239,4 +239,16 @@ export type InterceptedMobxEventPreparedData = WithStackTrace<{ mobxEventId: string; event: MobxSpyEvent; timestamp: number; -}>; \ No newline at end of file +}>; + +export type StorageType = "unknown" | "localStorage" | "AsyncStorage"; + +export type ForceRefreshPayload = { + type: "storage"; +}; + +export type StorageSnapshot = { + timestamp: number; + storageType: StorageType; + storageAsObject: ObjectT; +}; \ No newline at end of file