From fd17510ca07f89fdb57bb409dd397056810fe77f Mon Sep 17 00:00:00 2001 From: Eric Lin Date: Fri, 14 Feb 2025 10:22:38 -0500 Subject: [PATCH] refactor: DH-14692 Stop exporting client from support logs (#2368) Cherry-pick #2279 - Move `exportLogs` and `logInit` to the `log` package so they could be reused in Enterprise - Remove `@deephaven/redux` and `@deephaven/jsapi-shim` dependencies from `LogExport.ts` - Serialize Maps in redux data - Unit tests for `getReduxDataString` --- package-lock.json | 6 +- packages/code-studio/src/index.tsx | 7 +- .../code-studio/src/settings/SettingsMenu.tsx | 18 ++-- packages/log/package.json | 3 +- packages/log/src/LogExport.test.ts | 90 +++++++++++++++++++ .../src/log => log/src}/LogExport.ts | 68 ++++++++------ .../src/log => log/src}/LogInit.ts | 13 ++- packages/log/src/index.ts | 2 + 8 files changed, 164 insertions(+), 43 deletions(-) create mode 100644 packages/log/src/LogExport.test.ts rename packages/{code-studio/src/log => log/src}/LogExport.ts (74%) rename packages/{code-studio/src/log => log/src}/LogInit.ts (63%) diff --git a/package-lock.json b/package-lock.json index f473f4391a..a197a874c0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -30041,7 +30041,8 @@ "version": "0.85.0", "license": "Apache-2.0", "dependencies": { - "event-target-shim": "^6.0.2" + "event-target-shim": "^6.0.2", + "jszip": "^3.10.1" }, "engines": { "node": ">=16" @@ -32241,7 +32242,8 @@ "@deephaven/log": { "version": "file:packages/log", "requires": { - "event-target-shim": "^6.0.2" + "event-target-shim": "^6.0.2", + "jszip": "^3.10.1" } }, "@deephaven/mocks": { diff --git a/packages/code-studio/src/index.tsx b/packages/code-studio/src/index.tsx index 1e17b39045..8cfe0063c9 100644 --- a/packages/code-studio/src/index.tsx +++ b/packages/code-studio/src/index.tsx @@ -5,9 +5,12 @@ import { Provider } from 'react-redux'; import { LoadingOverlay, preloadTheme } from '@deephaven/components'; import { ApiBootstrap } from '@deephaven/jsapi-bootstrap'; import { store } from '@deephaven/redux'; -import logInit from './log/LogInit'; +import { logInit } from '@deephaven/log'; -logInit(); +logInit( + parseInt(import.meta.env.VITE_LOG_LEVEL ?? '', 10), + import.meta.env.VITE_ENABLE_LOG_PROXY === 'true' +); preloadTheme(); diff --git a/packages/code-studio/src/settings/SettingsMenu.tsx b/packages/code-studio/src/settings/SettingsMenu.tsx index 72567e42c2..1a480e1459 100644 --- a/packages/code-studio/src/settings/SettingsMenu.tsx +++ b/packages/code-studio/src/settings/SettingsMenu.tsx @@ -18,18 +18,18 @@ import { Logo, Tooltip, } from '@deephaven/components'; -import { ServerConfigValues, User } from '@deephaven/redux'; +import { ServerConfigValues, User, store } from '@deephaven/redux'; import { BROADCAST_CHANNEL_NAME, BROADCAST_LOGOUT_MESSAGE, makeMessage, } from '@deephaven/jsapi-utils'; import { PluginModuleMap } from '@deephaven/plugin'; +import { exportLogs, logHistory } from '@deephaven/log'; import FormattingSectionContent from './FormattingSectionContent'; import LegalNotice from './LegalNotice'; import SettingsMenuSection from './SettingsMenuSection'; import ShortcutSectionContent from './ShortcutsSectionContent'; -import { exportLogs } from '../log/LogExport'; import './SettingsMenu.scss'; import ColumnSpecificSectionContent from './ColumnSpecificSectionContent'; import { @@ -134,10 +134,16 @@ export class SettingsMenu extends Component< handleExportSupportLogs(): void { const { serverConfigValues, pluginData } = this.props; const pluginInfo = getFormattedPluginInfo(pluginData); - exportLogs(undefined, { - ...Object.fromEntries(serverConfigValues), - pluginInfo, - }); + exportLogs( + logHistory, + { + uiVersion: import.meta.env.npm_package_version, + userAgent: navigator.userAgent, + ...Object.fromEntries(serverConfigValues), + pluginInfo, + }, + store.getState() + ); } render(): ReactElement { diff --git a/packages/log/package.json b/packages/log/package.json index ed0a2da3e1..4c6740fdc8 100644 --- a/packages/log/package.json +++ b/packages/log/package.json @@ -21,7 +21,8 @@ "build:babel": "babel ./src --out-dir ./dist --extensions \".ts,.tsx,.js,.jsx\" --source-maps --root-mode upward" }, "dependencies": { - "event-target-shim": "^6.0.2" + "event-target-shim": "^6.0.2", + "jszip": "^3.10.1" }, "files": [ "dist" diff --git a/packages/log/src/LogExport.test.ts b/packages/log/src/LogExport.test.ts new file mode 100644 index 0000000000..7ee4805eef --- /dev/null +++ b/packages/log/src/LogExport.test.ts @@ -0,0 +1,90 @@ +import { getReduxDataString } from './LogExport'; + +describe('getReduxDataString', () => { + it('should return a JSON string of the redux data', () => { + const reduxData = { + key1: 'value1', + key2: 2, + key3: true, + }; + const result = getReduxDataString(reduxData); + const expected = JSON.stringify(reduxData, null, 2); + expect(result).toBe(expected); + }); + + it('should handle circular references', () => { + const reduxData: Record = { + key1: 'value1', + }; + reduxData.key2 = reduxData; + const result = getReduxDataString(reduxData); + const expected = JSON.stringify( + { + key1: 'value1', + key2: 'Circular ref to root', + }, + null, + 2 + ); + expect(result).toBe(expected); + }); + + it('should handle BigInt values', () => { + const reduxData = { + key1: BigInt('12345678901234567890'), + }; + const result = getReduxDataString(reduxData); + const expected = JSON.stringify( + { + key1: '12345678901234567890', + }, + null, + 2 + ); + expect(result).toBe(expected); + }); + + it('should apply blacklist paths', () => { + const reduxData = { + key1: 'should be blacklisted', + key2: { + 'key2.1': 'should also be blacklisted', + }, + key3: 'value', + }; + const result = getReduxDataString(reduxData, [ + ['key1'], + ['key2', 'key2.1'], + ]); + const expected = JSON.stringify( + { + key2: {}, + key3: 'value', + }, + null, + 2 + ); + expect(result).toBe(expected); + }); + + it('should stringify Maps', () => { + const reduxData = { + key1: new Map([ + ['key1.1', 'value1.1'], + ['key1.2', 'value1.2'], + ]), + }; + const result = getReduxDataString(reduxData); + const expected = JSON.stringify( + { + key1: [ + ['key1.1', 'value1.1'], + ['key1.2', 'value1.2'], + ], + }, + null, + 2 + ); + expect(result).toBe(expected); + }); +}); diff --git a/packages/code-studio/src/log/LogExport.ts b/packages/log/src/LogExport.ts similarity index 74% rename from packages/code-studio/src/log/LogExport.ts rename to packages/log/src/LogExport.ts index d05bc8cc75..08e9c68ed9 100644 --- a/packages/code-studio/src/log/LogExport.ts +++ b/packages/log/src/LogExport.ts @@ -1,10 +1,5 @@ -/* eslint-disable import/prefer-default-export */ import JSZip from 'jszip'; -import dh from '@deephaven/jsapi-shim'; -import { store } from '@deephaven/redux'; -import { logHistory } from './LogInit'; - -const FILENAME_DATE_FORMAT = 'yyyy-MM-dd-HHmmss'; +import type LogHistory from './LogHistory'; // List of objects to blacklist // '' represents the root object @@ -41,6 +36,10 @@ function stringifyReplacer(blacklist: string[][]) { } } + if (value instanceof Map) { + return Array.from(value.entries()); + } + // not in blacklist, return value return value; }; @@ -66,7 +65,7 @@ function makeSafeToStringify( blacklist: string[][], path = 'root', potentiallyCircularValues: Map, string> = new Map([ - [obj, ''], + [obj, 'root'], ]) ): Record { const output: Record = {}; @@ -104,8 +103,10 @@ function makeSafeToStringify( return output; } -function getReduxDataString(blacklist: string[][]): string { - const reduxData = store.getState(); +export function getReduxDataString( + reduxData: Record, + blacklist: string[][] = [] +): string { return JSON.stringify( makeSafeToStringify(reduxData, blacklist), stringifyReplacer(blacklist), @@ -113,39 +114,50 @@ function getReduxDataString(blacklist: string[][]): string { ); } -function getMetadata( - blacklist: string[][], - meta?: Record -): string { - const metadata = { - uiVersion: import.meta.env.npm_package_version, - userAgent: navigator.userAgent, - ...meta, - }; +function getFormattedMetadata(metadata?: Record): string { + return JSON.stringify(metadata, null, 2); +} + +/** Format a date to a string that can be used as a file name + * @param date Date to format + * @returns A string formatted as YYYY-MM-DD-HHMMSS + */ +function formatDate(date: Date): string { + const year = date.getFullYear(); + const month = String(date.getMonth() + 1).padStart(2, '0'); + const day = String(date.getDate()).padStart(2, '0'); + const h = String(date.getHours()).padStart(2, '0'); + const m = String(date.getMinutes()).padStart(2, '0'); + const s = String(date.getSeconds()).padStart(2, '0'); - return JSON.stringify(metadata, stringifyReplacer(blacklist), 2); + return `${year}-${month}-${day}-${h}${m}${s}`; } /** * Export support logs with the given name. - * @param fileNamePrefix The zip file name without the .zip extension. Ex: test will be saved as test.zip + * @param logHistory Log history to include in the console.txt file * @param metadata Additional metadata to include in the metadata.json file - * @param blacklist List of JSON paths to blacklist. A JSON path is a list representing the path to that value (e.g. client.data would be `['client', 'data']`) + * @param reduxData Redux data to include in the redux.json file + * @param blacklist List of JSON paths to blacklist in redux data. A JSON path is a list representing the path to that value (e.g. client.data would be `['client', 'data']`) + * @param fileNamePrefix The zip file name without the .zip extension. Ex: test will be saved as test.zip * @returns A promise that resolves successfully if the log archive is created and downloaded successfully, rejected if there's an error */ export async function exportLogs( - fileNamePrefix = `${dh.i18n.DateTimeFormat.format( - FILENAME_DATE_FORMAT, - new Date() - )}_support_logs`, + logHistory: LogHistory, metadata?: Record, - blacklist: string[][] = DEFAULT_PATH_BLACKLIST + reduxData?: Record, + blacklist: string[][] = DEFAULT_PATH_BLACKLIST, + fileNamePrefix = `${formatDate(new Date())}_support_logs` ): Promise { const zip = new JSZip(); const folder = zip.folder(fileNamePrefix) as JSZip; folder.file('console.txt', logHistory.getFormattedHistory()); - folder.file('redux.json', getReduxDataString(blacklist)); - folder.file('metadata.json', getMetadata(blacklist, metadata)); + if (metadata != null) { + folder.file('metadata.json', getFormattedMetadata(metadata)); + } + if (reduxData != null) { + folder.file('redux.json', getReduxDataString(reduxData, blacklist)); + } const blob = await zip.generateAsync({ type: 'blob' }); const link = document.createElement('a'); diff --git a/packages/code-studio/src/log/LogInit.ts b/packages/log/src/LogInit.ts similarity index 63% rename from packages/code-studio/src/log/LogInit.ts rename to packages/log/src/LogInit.ts index 6e2d6decf6..f6cc258012 100644 --- a/packages/code-studio/src/log/LogInit.ts +++ b/packages/log/src/LogInit.ts @@ -1,4 +1,7 @@ -import { LogProxy, LogHistory, Logger, Log } from '@deephaven/log'; +import Log from './Log'; +import type Logger from './Logger'; +import LogHistory from './LogHistory'; +import LogProxy from './LogProxy'; declare global { interface Window { @@ -11,10 +14,10 @@ declare global { export const logProxy = new LogProxy(); export const logHistory = new LogHistory(logProxy); -export default function logInit(): void { - Log.setLogLevel(parseInt(import.meta.env.VITE_LOG_LEVEL ?? '', 10)); +export function logInit(logLevel = 2, enableProxy = true): void { + Log.setLogLevel(logLevel); - if (import.meta.env.VITE_ENABLE_LOG_PROXY === 'true') { + if (enableProxy) { logProxy.enable(); logHistory.enable(); } @@ -26,3 +29,5 @@ export default function logInit(): void { window.DHLogHistory = logHistory; } } + +export default logInit; diff --git a/packages/log/src/index.ts b/packages/log/src/index.ts index cbefa05c20..f1209f2cc5 100644 --- a/packages/log/src/index.ts +++ b/packages/log/src/index.ts @@ -6,3 +6,5 @@ export { default as Logger } from './Logger'; export { default as LogHistory } from './LogHistory'; export { default as LogProxy } from './LogProxy'; export { default as LoggerLevel } from './LoggerLevel'; +export * from './LogExport'; +export * from './LogInit';