Skip to content

Commit d3fb28a

Browse files
authored
feat: log export blacklist (#1881)
- Adds #1245 - Add a replacer function to all `JSON.stringify` to filter out blacklisted path - Add the following paths to blacklist: `api`, `client`, `dashboardData.defaultLayout.connection`, `layoutStorage`, `storage`
1 parent 309ff79 commit d3fb28a

1 file changed

Lines changed: 59 additions & 9 deletions

File tree

packages/code-studio/src/log/LogExport.ts

Lines changed: 59 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,46 @@ import { logHistory } from './LogInit';
66

77
const FILENAME_DATE_FORMAT = 'yyyy-MM-dd-HHmmss';
88

9+
// List of objects to blacklist
10+
// '' represents the root object
11+
export const DEFAULT_PATH_BLACKLIST: string[][] = [
12+
['api'],
13+
['client'],
14+
['dashboardData', 'default', 'connection'],
15+
['dashboardData', 'default', 'sessionWrapper', 'dh'],
16+
['layoutStorage'],
17+
['storage'],
18+
];
19+
20+
function stringifyReplacer(blacklist: string[][]) {
21+
// modified from:
22+
// https://stackoverflow.com/questions/61681176/json-stringify-replacer-how-to-get-full-path
23+
const pathMap = new Map();
24+
// replacer function is also called for the initial object, key is ""
25+
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify#the_replacer_parameter
26+
27+
return function replacer(this: unknown, key: string, value: unknown) {
28+
// get and store path
29+
const currPath = [...(pathMap.get(this) ?? []), key];
30+
if (value === Object(value)) pathMap.set(value, [...currPath]);
31+
currPath.shift();
32+
33+
// check blacklists
34+
for (let i = 0; i < blacklist.length; i += 1) {
35+
if (
36+
currPath.length === blacklist[i].length &&
37+
currPath.every((v, index) => v === blacklist[i][index])
38+
) {
39+
// blacklist match
40+
return undefined;
41+
}
42+
}
43+
44+
// not in blacklist, return value
45+
return value;
46+
};
47+
}
48+
949
/**
1050
* Returns a new object that is safe to stringify
1151
* All circular references are replaced by the path to the value creating a circular ref
@@ -19,9 +59,11 @@ const FILENAME_DATE_FORMAT = 'yyyy-MM-dd-HHmmss';
1959
* Then if the object is seen again, it must be a circular ref since that object could not be stringified safely
2060
*
2161
* @param obj Object to make safe to stringify
62+
* @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']`)
2263
*/
2364
function makeSafeToStringify(
2465
obj: Record<string, unknown>,
66+
blacklist: string[][],
2567
path = 'root',
2668
potentiallyCircularValues: Map<Record<string, unknown>, string> = new Map([
2769
[obj, ''],
@@ -31,7 +73,7 @@ function makeSafeToStringify(
3173

3274
Object.entries(obj).forEach(([key, val]) => {
3375
try {
34-
JSON.stringify(val);
76+
JSON.stringify(val, stringifyReplacer(blacklist));
3577
output[key] = val;
3678
} catch (e) {
3779
// The value must be a Circular object or BigInt here
@@ -51,6 +93,7 @@ function makeSafeToStringify(
5193
potentiallyCircularValues.set(valRecord, curPath);
5294
output[key] = makeSafeToStringify(
5395
val as Record<string, unknown>,
96+
blacklist,
5497
curPath,
5598
potentiallyCircularValues
5699
);
@@ -61,41 +104,48 @@ function makeSafeToStringify(
61104
return output;
62105
}
63106

64-
function getReduxDataString(): string {
107+
function getReduxDataString(blacklist: string[][]): string {
65108
const reduxData = store.getState();
66109
return JSON.stringify(
67-
makeSafeToStringify(reduxData),
68-
null,
110+
makeSafeToStringify(reduxData, blacklist),
111+
stringifyReplacer(blacklist),
69112
2 // Indent w/ 2 spaces
70113
);
71114
}
72115

73-
function getMetadata(meta?: Record<string, unknown>): string {
116+
function getMetadata(
117+
blacklist: string[][],
118+
meta?: Record<string, unknown>
119+
): string {
74120
const metadata = {
75121
uiVersion: import.meta.env.npm_package_version,
76122
userAgent: navigator.userAgent,
77123
...meta,
78124
};
79125

80-
return JSON.stringify(metadata, null, 2);
126+
return JSON.stringify(metadata, stringifyReplacer(blacklist), 2);
81127
}
82128

83129
/**
84130
* Export support logs with the given name.
85131
* @param fileNamePrefix The zip file name without the .zip extension. Ex: test will be saved as test.zip
132+
* @param metadata Additional metadata to include in the metadata.json file
133+
* @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']`)
134+
* @returns A promise that resolves successfully if the log archive is created and downloaded successfully, rejected if there's an error
86135
*/
87136
export async function exportLogs(
88137
fileNamePrefix = `${dh.i18n.DateTimeFormat.format(
89138
FILENAME_DATE_FORMAT,
90139
new Date()
91140
)}_support_logs`,
92-
metadata?: Record<string, unknown>
141+
metadata?: Record<string, unknown>,
142+
blacklist: string[][] = DEFAULT_PATH_BLACKLIST
93143
): Promise<void> {
94144
const zip = new JSZip();
95145
const folder = zip.folder(fileNamePrefix) as JSZip;
96146
folder.file('console.txt', logHistory.getFormattedHistory());
97-
folder.file('redux.json', getReduxDataString());
98-
folder.file('metadata.json', getMetadata(metadata));
147+
folder.file('redux.json', getReduxDataString(blacklist));
148+
folder.file('metadata.json', getMetadata(blacklist, metadata));
99149

100150
const blob = await zip.generateAsync({ type: 'blob' });
101151
const link = document.createElement('a');

0 commit comments

Comments
 (0)