change rageshake.ts

to be more similar to the matrix-js version
This commit is contained in:
Timo K 2022-06-11 14:28:30 +02:00
parent 0aa29f775c
commit 60ed54d6d3

View file

@ -39,6 +39,7 @@ limitations under the License.
// purge on startup to prevent logs from accumulating. // purge on startup to prevent logs from accumulating.
import { logger } from "matrix-js-sdk/src/logger"; import { logger } from "matrix-js-sdk/src/logger";
import { randomString } from "matrix-js-sdk/src/randomstring";
// the frequency with which we flush to indexeddb // the frequency with which we flush to indexeddb
const FLUSH_RATE_MS = 30 * 1000; const FLUSH_RATE_MS = 30 * 1000;
@ -46,6 +47,11 @@ const FLUSH_RATE_MS = 30 * 1000;
// the length of log data we keep in indexeddb (and include in the reports) // the length of log data we keep in indexeddb (and include in the reports)
const MAX_LOG_SIZE = 1024 * 1024 * 5; // 5 MB const MAX_LOG_SIZE = 1024 * 1024 * 5; // 5 MB
type LogFunction = (
...args: (Error | DOMException | object | string)[]
) => void;
type LogFunctionName = "log" | "info" | "warn" | "error";
// A class which monkey-patches the global console and stores log lines. // A class which monkey-patches the global console and stores log lines.
interface LogEntry { interface LogEntry {
@ -54,42 +60,40 @@ interface LogEntry {
index?: number; index?: number;
} }
interface Cursor {
id: string;
ts: number;
}
export class ConsoleLogger { export class ConsoleLogger {
logs = ""; private logs = "";
private originalFunctions: { [key in LogFunctionName]?: LogFunction } = {};
monkeyPatch(consoleObj: Console): void { public monkeyPatch(consoleObj: Console): void {
// Monkey-patch console logging // Monkey-patch console logging
const consoleFunctionsToLevels = {
const consoleFunctionsToLevels: { [level: string]: string } = {
log: "I", log: "I",
info: "I", info: "I",
warn: "W", warn: "W",
error: "E", error: "E",
}; };
Object.keys(consoleFunctionsToLevels).forEach((fnName) => {
Object.keys(consoleFunctionsToLevels).forEach( const level = consoleFunctionsToLevels[fnName];
(fnName: "log" | "info" | "warn" | "error") => { const originalFn = consoleObj[fnName].bind(consoleObj);
const level = consoleFunctionsToLevels[fnName]; this.originalFunctions[fnName] = originalFn;
const originalFn = consoleObj[fnName].bind(consoleObj); consoleObj[fnName] = (...args) => {
consoleObj[fnName] = (...args: unknown[]) => { this.log(level, ...args);
this.log(level, ...args); originalFn(...args);
originalFn(...args); };
}; });
}
);
} }
// these functions get overwritten by the monkey patch public bypassRageshake(
error(...args: unknown[]): void {} fnName: LogFunctionName,
warn(...args: unknown[]): void {} ...args: (Error | DOMException | object | string)[]
info(...args: unknown[]): void {} ): void {
this.originalFunctions[fnName](...args);
}
log(level: string, ...args: unknown[]): void { public log(
level: string,
...args: (Error | DOMException | object | string)[]
): void {
// We don't know what locale the user may be running so use ISO strings // We don't know what locale the user may be running so use ISO strings
const ts = new Date().toISOString(); const ts = new Date().toISOString();
@ -100,21 +104,7 @@ export class ConsoleLogger {
} else if (arg instanceof Error) { } else if (arg instanceof Error) {
return arg.message + (arg.stack ? `\n${arg.stack}` : ""); return arg.message + (arg.stack ? `\n${arg.stack}` : "");
} else if (typeof arg === "object") { } else if (typeof arg === "object") {
try { return JSON.stringify(arg, getCircularReplacer());
return JSON.stringify(arg);
} catch (e) {
// In development, it can be useful to log complex cyclic
// objects to the console for inspection. This is fine for
// the console, but default `stringify` can't handle that.
// We workaround this by using a special replacer function
// to only log values of the root object and avoid cycles.
return JSON.stringify(arg, (key, value) => {
if (key && typeof value === "object") {
return "<object>";
}
return value;
});
}
} else { } else {
return arg; return arg;
} }
@ -135,10 +125,10 @@ export class ConsoleLogger {
/** /**
* Retrieve log lines to flush to disk. * Retrieve log lines to flush to disk.
* @param {boolean} keepLogs True to not delete logs after flushing. Defaults to false. * @param {boolean} keepLogs True to not delete logs after flushing.
* @return {string} \n delimited log lines to flush. * @return {string} \n delimited log lines to flush.
*/ */
flush(keepLogs = false): string { public flush(keepLogs?: boolean): string {
// The ConsoleLogger doesn't care how these end up on disk, it just // The ConsoleLogger doesn't care how these end up on disk, it just
// flushes them to the caller. // flushes them to the caller.
if (keepLogs) { if (keepLogs) {
@ -152,26 +142,22 @@ export class ConsoleLogger {
// A class which stores log lines in an IndexedDB instance. // A class which stores log lines in an IndexedDB instance.
export class IndexedDBLogStore { export class IndexedDBLogStore {
index = 0; private index = 0;
db: IDBDatabase = null; private db: IDBDatabase = null;
flushPromise: Promise<void> = null; private flushPromise: Promise<void> = null;
flushAgainPromise: Promise<void> = null; private flushAgainPromise: Promise<void> = null;
indexedDB: IDBFactory; private id: string;
logger: ConsoleLogger;
id: string;
constructor(indexedDB: IDBFactory, logger: ConsoleLogger) { constructor(private indexedDB: IDBFactory, private logger: ConsoleLogger) {
this.indexedDB = indexedDB; this.id = "instance-" + randomString(16);
this.logger = logger;
this.id = "instance-" + Math.random() + Date.now();
} }
/** /**
* @return {Promise} Resolves when the store is ready. * @return {Promise} Resolves when the store is ready.
*/ */
connect(): Promise<void> { public connect(): Promise<void> {
const req = this.indexedDB.open("logs"); const req = this.indexedDB.open("logs");
return new Promise<void>((resolve, reject) => { return new Promise((resolve, reject) => {
req.onsuccess = (event: Event) => { req.onsuccess = (event: Event) => {
// @ts-ignore // @ts-ignore
this.db = event.target.result; this.db = event.target.result;
@ -180,16 +166,16 @@ export class IndexedDBLogStore {
resolve(); resolve();
}; };
req.onerror = (event: Event) => { req.onerror = (event) => {
const err = const err =
// @ts-ignore // @ts-ignore
"Failed to open log database: " + event.target.error.name; "Failed to open log database: " + event.target.error.name;
this.logger.error(err); logger.error(err);
reject(new Error(err)); reject(new Error(err));
}; };
// First time: Setup the object store // First time: Setup the object store
req.onupgradeneeded = (event: IDBVersionChangeEvent) => { req.onupgradeneeded = (event) => {
// @ts-ignore // @ts-ignore
const db = event.target.result; const db = event.target.result;
const logObjStore = db.createObjectStore("logs", { const logObjStore = db.createObjectStore("logs", {
@ -231,7 +217,7 @@ export class IndexedDBLogStore {
* *
* @return {Promise} Resolved when the logs have been flushed. * @return {Promise} Resolved when the logs have been flushed.
*/ */
flush(): Promise<void> { public flush(): Promise<void> {
// check if a flush() operation is ongoing // check if a flush() operation is ongoing
if (this.flushPromise) { if (this.flushPromise) {
if (this.flushAgainPromise) { if (this.flushAgainPromise) {
@ -267,7 +253,7 @@ export class IndexedDBLogStore {
resolve(); resolve();
}; };
txn.onerror = (event) => { txn.onerror = (event) => {
this.logger.error("Failed to flush logs : ", event); logger.error("Failed to flush logs : ", event);
// @ts-ignore // @ts-ignore
reject(new Error("Failed to write logs: " + event.target.errorCode)); reject(new Error("Failed to write logs: " + event.target.errorCode));
}; };
@ -290,13 +276,12 @@ export class IndexedDBLogStore {
* log ID). The objects have said log ID in an "id" field and "lines" which * log ID). The objects have said log ID in an "id" field and "lines" which
* is a big string with all the new-line delimited logs. * is a big string with all the new-line delimited logs.
*/ */
public async consume(): Promise<LogEntry[]> {
async consume(): Promise<Object[]> {
const db = this.db; const db = this.db;
// Returns: a string representing the concatenated logs for this ID. // Returns: a string representing the concatenated logs for this ID.
// Stops adding log fragments when the size exceeds maxSize // Stops adding log fragments when the size exceeds maxSize
function fetchLogs(id: string, maxSize: number): Promise<string[]> { function fetchLogs(id: string, maxSize: number): Promise<string> {
const objectStore = db const objectStore = db
.transaction("logs", "readonly") .transaction("logs", "readonly")
.objectStore("logs"); .objectStore("logs");
@ -314,28 +299,29 @@ export class IndexedDBLogStore {
// @ts-ignore // @ts-ignore
const cursor = event.target.result; const cursor = event.target.result;
if (!cursor) { if (!cursor) {
resolve(lines.split("\n")); resolve(lines);
return; // end of results return; // end of results
} }
lines = cursor.value.lines + lines; lines = cursor.value.lines + lines;
if (lines.length >= maxSize) { if (lines.length >= maxSize) {
resolve(lines.split("\n")); resolve(lines);
} else { } else {
cursor.continue(); cursor.continue();
} }
}; };
}); });
} }
// Returns: A sorted array of log IDs. (newest first) // Returns: A sorted array of log IDs. (newest first)
function fetchLogIds(): Promise<string[]> { function fetchLogIds(): Promise<string[]> {
// To gather all the log IDs, query for all records in logslastmod. // To gather all the log IDs, query for all records in logslastmod.
const o = db const o = db
.transaction("logslastmod", "readonly") .transaction("logslastmod", "readonly")
.objectStore("logslastmod"); .objectStore("logslastmod");
return selectQuery(o, undefined, (cursor: Cursor) => { return selectQuery<{ ts: number; id: string }>(o, undefined, (cursor) => {
return { return {
id: cursor.id, id: cursor.value.id,
ts: cursor.ts, ts: cursor.value.ts,
}; };
}).then((res) => { }).then((res) => {
// Sort IDs by timestamp (newest first) // Sort IDs by timestamp (newest first)
@ -346,8 +332,9 @@ export class IndexedDBLogStore {
.map((a) => a.id); .map((a) => a.id);
}); });
} }
function deleteLogs(id: string): Promise<void> {
return new Promise((resolve, reject) => { function deleteLogs(id: number): Promise<void> {
return new Promise<void>((resolve, reject) => {
const txn = db.transaction(["logs", "logslastmod"], "readwrite"); const txn = db.transaction(["logs", "logslastmod"], "readwrite");
const o = txn.objectStore("logs"); const o = txn.objectStore("logs");
// only load the key path, not the data which may be huge // only load the key path, not the data which may be huge
@ -380,14 +367,11 @@ export class IndexedDBLogStore {
} }
const allLogIds = await fetchLogIds(); const allLogIds = await fetchLogIds();
let removeLogIds: string[] = []; let removeLogIds = [];
const logs = []; const logs: LogEntry[] = [];
let size = 0; let size = 0;
for (let i = 0; i < allLogIds.length; i++) { for (let i = 0; i < allLogIds.length; i++) {
const lines: string[] = await fetchLogs( const lines = await fetchLogs(allLogIds[i], MAX_LOG_SIZE - size);
allLogIds[i],
MAX_LOG_SIZE - size
);
// always add the log file: fetchLogs will truncate once the maxSize we give it is // always add the log file: fetchLogs will truncate once the maxSize we give it is
// exceeded, so we'll go over the max but only by one fragment's worth. // exceeded, so we'll go over the max but only by one fragment's worth.
@ -407,22 +391,22 @@ export class IndexedDBLogStore {
} }
} }
if (removeLogIds.length > 0) { if (removeLogIds.length > 0) {
this.logger.log("Removing logs: ", removeLogIds); logger.log("Removing logs: ", removeLogIds);
// Don't await this because it's non-fatal if we can't clean up // Don't await this because it's non-fatal if we can't clean up
// logs. // logs.
Promise.all(removeLogIds.map((id) => deleteLogs(id))).then( Promise.all(removeLogIds.map((id) => deleteLogs(id))).then(
() => { () => {
this.logger.log(`Removed ${removeLogIds.length} old logs.`); logger.log(`Removed ${removeLogIds.length} old logs.`);
}, },
(err) => { (err) => {
this.logger.error(err); logger.error(err);
} }
); );
} }
return logs; return logs;
} }
generateLogEntry(lines: string): LogEntry { private generateLogEntry(lines: string): LogEntry {
return { return {
id: this.id, id: this.id,
lines: lines, lines: lines,
@ -430,7 +414,7 @@ export class IndexedDBLogStore {
}; };
} }
generateLastModifiedTime(): Cursor { private generateLastModifiedTime(): { id: string; ts: number } {
return { return {
id: this.id, id: this.id,
ts: Date.now(), ts: Date.now(),
@ -448,22 +432,22 @@ export class IndexedDBLogStore {
* @return {Promise<T[]>} Resolves to an array of whatever you returned from * @return {Promise<T[]>} Resolves to an array of whatever you returned from
* resultMapper. * resultMapper.
*/ */
function selectQuery( function selectQuery<T>(
store: IDBObjectStore, store: IDBObjectStore,
keyRange: IDBKeyRange, keyRange: IDBKeyRange,
resultMapper: (arg: Cursor) => Cursor resultMapper: (cursor: IDBCursorWithValue) => T
): Promise<Cursor[]> { ): Promise<T[]> {
const query = store.openCursor(keyRange); const query = store.openCursor(keyRange);
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const results: Cursor[] = []; const results = [];
query.onerror = (event: Event) => { query.onerror = (event) => {
// @ts-ignore // @ts-ignore
reject(new Error("Query failed: " + event.target.errorCode)); reject(new Error("Query failed: " + event.target.errorCode));
}; };
// collect results // collect results
query.onsuccess = (event: Event) => { query.onsuccess = (event) => {
// @ts-ignore // @ts-ignore
const cursor = event.target.result?.value; const cursor = event.target.result;
if (!cursor) { if (!cursor) {
resolve(results); resolve(results);
return; // end of results return; // end of results
@ -569,13 +553,30 @@ export async function getLogsForReport(): Promise<LogEntry[]> {
if (global.mx_rage_store) { if (global.mx_rage_store) {
// flush most recent logs // flush most recent logs
await global.mx_rage_store.flush(); await global.mx_rage_store.flush();
return (await global.mx_rage_store.consume()) as LogEntry[]; return global.mx_rage_store.consume();
} else { } else {
return [ return [
{ {
lines: global.mx_rage_logger.flush(true), lines: global.mx_rage_logger.flush(true),
id: "-", id: "-",
}, },
] as LogEntry[]; ];
} }
} }
type StringifyReplacer = (this: any, key: string, value: any) => any;
// From https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Errors/Cyclic_object_value#circular_references
// Injects `<$ cycle-trimmed $>` wherever it cuts a cyclical object relationship
const getCircularReplacer = (): StringifyReplacer => {
const seen = new WeakSet();
return (key: string, value: any): any => {
if (typeof value === "object" && value !== null) {
if (seen.has(value)) {
return "<$ cycle-trimmed $>";
}
seen.add(value);
}
return value;
};
};