diff --git a/src/vector/rageshake.js b/src/vector/rageshake.js index e12ee329..a3d39635 100644 --- a/src/vector/rageshake.js +++ b/src/vector/rageshake.js @@ -17,18 +17,26 @@ limitations under the License. import PlatformPeg from 'matrix-react-sdk/lib/PlatformPeg'; import request from "browser-request"; -// This module contains all the code needed to log the console, persist it to disk and submit bug reports. Rationale is as follows: -// - Monkey-patching the console is preferable to having a log library because we can catch logs by other libraries more easily, -// without having to all depend on the same log framework / pass the logger around. -// - We use IndexedDB to persists logs because it has generous disk space limits compared to local storage. IndexedDB does not work -// in incognito mode, in which case this module will not be able to write logs to disk. However, the logs will still be stored -// in-memory, so can still be submitted in a bug report should the user wish to: we can also store more logs in-memory than in -// local storage, which does work in incognito mode. We also need to handle the case where there are 2+ tabs. Each JS runtime -// generates a random string which serves as the "ID" for that tab/session. These IDs are stored along with the log lines. -// - Bug reports are sent as a POST over HTTPS: it purposefully does not use Matrix as bug reports may be made when Matrix is -// not responsive (which may be the cause of the bug). We send the most recent N MB of UTF-8 log data, starting with the most -// recent, which we know because the "ID"s are actually timestamps. We then purge the remaining logs. We also do this purge -// on startup to prevent logs from accumulating. +// This module contains all the code needed to log the console, persist it to +// disk and submit bug reports. Rationale is as follows: +// - Monkey-patching the console is preferable to having a log library because +// we can catch logs by other libraries more easily, without having to all +// depend on the same log framework / pass the logger around. +// - We use IndexedDB to persists logs because it has generous disk space +// limits compared to local storage. IndexedDB does not work in incognito +// mode, in which case this module will not be able to write logs to disk. +// However, the logs will still be stored in-memory, so can still be +// submitted in a bug report should the user wish to: we can also store more +// logs in-memory than in local storage, which does work in incognito mode. +// We also need to handle the case where there are 2+ tabs. Each JS runtime +// generates a random string which serves as the "ID" for that tab/session. +// These IDs are stored along with the log lines. +// - Bug reports are sent as a POST over HTTPS: it purposefully does not use +// Matrix as bug reports may be made when Matrix is not responsive (which may +// be the cause of the bug). We send the most recent N MB of UTF-8 log data, +// starting with the most recent, which we know because the "ID"s are +// actually timestamps. We then purge the remaining logs. We also do this +// purge on startup to prevent logs from accumulating. const FLUSH_RATE_MS = 30 * 1000; @@ -60,9 +68,10 @@ class ConsoleLogger { // We don't know what locale the user may be running so use ISO strings const ts = new Date().toISOString(); // Some browsers support string formatting which we're not doing here - // so the lines are a little more ugly but easy to implement / quick to run. + // so the lines are a little more ugly but easy to implement / quick to + // run. // Example line: - // 2017-01-18T11:23:53.214Z W Failed to set badge count: Error setting badge. Message: Too many badges requests in queue. + // 2017-01-18T11:23:53.214Z W Failed to set badge count const line = `${ts} ${level} ${args.join(' ')}\n`; // Using + really is the quickest way in JS // http://jsperf.com/concat-vs-plus-vs-join @@ -74,7 +83,8 @@ class ConsoleLogger { * @return {string} \n delimited log lines to flush. */ flush() { - // The ConsoleLogger doesn't care how these end up on disk, it just flushes them to the caller. + // The ConsoleLogger doesn't care how these end up on disk, it just + // flushes them to the caller. const logsToFlush = this.logs; this.logs = ""; return logsToFlush; @@ -105,7 +115,9 @@ class IndexedDBLogStore { }; req.onerror = (event) => { - const err = "Failed to open log database: " + event.target.errorCode; + const err = ( + "Failed to open log database: " + event.target.errorCode + ); console.error(err); reject(new Error(err)); }; @@ -117,8 +129,10 @@ class IndexedDBLogStore { keyPath: ["id", "index"] }); // Keys in the database look like: [ "instance-148938490", 0 ] - // Later on we need to query for everything with index=0, and query everything for an instance id. - // In order to do this, we need to set up indexes on both "id" and "index". + // Later on we need to query for everything with index=0, and + // query everything for an instance id. + // In order to do this, we need to set up indexes on both "id" + // and "index". objectStore.createIndex("index", "index", { unique: false }); objectStore.createIndex("id", "id", { unique: false }); @@ -151,23 +165,30 @@ class IndexedDBLogStore { resolve(); }; txn.onerror = (event) => { - console.error("Failed to flush logs : " + event.target.errorCode); - reject(new Error("Failed to write logs: " + event.target.errorCode)); + console.error( + "Failed to flush logs : " + event.target.errorCode + ); + reject( + new Error("Failed to write logs: " + event.target.errorCode) + ); } }); } /** - * Consume the most recent logs and return them. Older logs which are not returned are deleted at the same time, - * so this can be called at startup to do house-keeping to keep the logs from growing too large. + * Consume the most recent logs and return them. Older logs which are not + * returned are deleted at the same time, so this can be called at startup + * to do house-keeping to keep the logs from growing too large. * - * @param {boolean} clearAll True to clear the most recent logs returned in addition to the - * older logs. This is desirable when sending bug reports as we do not want to submit the - * same logs twice. This is not desirable when doing house-keeping at startup in case they - * want to submit a bug report later. - * @return {Promise<Object[]>} Resolves to an array of objects. The array is sorted in time (oldest first) based on - * when the log file was created (the 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. + * @param {boolean} clearAll True to clear the most recent logs returned in + * addition to the older logs. This is desirable when sending bug reports as + * we do not want to submit the same logs twice. This is not desirable when + * doing house-keeping at startup in case they want to submit a bug report + * later. + * @return {Promise<Object[]>} Resolves to an array of objects. The array is + * sorted in time (oldest first) based on when the log file was created (the + * 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. */ async consume(clearAll) { const MAX_LOG_SIZE = 1024 * 1024 * 50; // 50 MB @@ -176,13 +197,15 @@ class IndexedDBLogStore { // Returns: a string representing the concatenated logs for this ID. function fetchLogs(id) { const o = db.transaction("logs", "readonly").objectStore("logs"); - return selectQuery(o.index("id"), IDBKeyRange.only(id), (cursor) => { + return selectQuery(o.index("id"), IDBKeyRange.only(id), + (cursor) => { return { lines: cursor.value.lines, index: cursor.value.index, } }).then((linesArray) => { - // We have been storing logs periodically, so string them all together *in order of index* now + // We have been storing logs periodically, so string them all + // together *in order of index* now linesArray.sort((a, b) => { return a.index - b.index; }) @@ -192,13 +215,18 @@ class IndexedDBLogStore { // Returns: A sorted array of log IDs. (newest first) function fetchLogIds() { - // To gather all the log IDs, query for every log entry with index "0", this will let us - // know all the IDs from different tabs/sessions. + // To gather all the log IDs, query for every log entry with index + // "0", this will let us know all the IDs from different + // tabs/sessions. const o = db.transaction("logs", "readonly").objectStore("logs"); - return selectQuery(o.index("index"), IDBKeyRange.only(0), (cursor) => cursor.value.id).then((res) => { - // we know each entry has a unique ID, and we know IDs are timestamps, so accumulate all the IDs, - // ignoring the logs for now, and sort them to work out the correct log ID ordering, newest first. - // E.g. [ "instance-1484827160051", "instance-1374827160051", "instance-1000007160051"] + return selectQuery(o.index("index"), IDBKeyRange.only(0), + (cursor) => cursor.value.id).then((res) => { + // we know each entry has a unique ID, and we know IDs are + // timestamps, so accumulate all the IDs, ignoring the logs for + // now, and sort them to work out the correct log ID ordering, + // newest first. + // E.g. [ "instance-1484827160051", "instance-1374827160051", + // "instance-1000007160051"] return res.sort().reverse(); }); } @@ -221,7 +249,12 @@ class IndexedDBLogStore { resolve(); }; txn.onerror = (event) => { - reject(new Error(`Failed to delete logs for '${id}' : ${event.target.errorCode}`)); + reject( + new Error( + "Failed to delete logs for " + + `'${id}' : ${event.target.errorCode}` + ) + ); } }); } @@ -238,7 +271,8 @@ class IndexedDBLogStore { }); size += lines.length; if (size > MAX_LOG_SIZE) { - // the remaining log IDs should be removed. If we go out of bounds this is just [] + // the remaining log IDs should be removed. If we go out of + // bounds this is just [] removeLogIds = allLogIds.slice(i + 1); break; } @@ -248,7 +282,8 @@ class IndexedDBLogStore { } if (removeLogIds.length > 0) { console.log("Removing logs: ", removeLogIds); - // Don't await this because it's non-fatal if we can't clean up logs. + // Don't await this because it's non-fatal if we can't clean up + // logs. Promise.all(removeLogIds.map((id) => deleteLogs(id))).then(() => { console.log(`Removed ${removeLogIds.length} old logs.`); }, (err) => { @@ -271,9 +306,11 @@ class IndexedDBLogStore { * Helper method to collect results from a Cursor and promiseify it. * @param {ObjectStore|Index} store The store to perform openCursor on. * @param {IDBKeyRange=} keyRange Optional key range to apply on the cursor. - * @param {Function} resultMapper A function which is repeatedly called with a Cursor. + * @param {Function} resultMapper A function which is repeatedly called with a + * Cursor. * Return the data you want to keep. - * @return {Promise<T[]>} Resolves to an array of whatever you returned from resultMapper. + * @return {Promise<T[]>} Resolves to an array of whatever you returned from + * resultMapper. */ function selectQuery(store, keyRange, resultMapper) { const query = store.openCursor(keyRange); @@ -355,7 +392,9 @@ module.exports = { */ sendBugReport: async function(userText) { if (!logger) { - throw new Error("No console logger, did you forget to call init()?"); + throw new Error( + "No console logger, did you forget to call init()?" + ); } if (!bugReportEndpoint) { throw new Error("No bug report endpoint has been set."); @@ -372,8 +411,8 @@ module.exports = { userAgent = window.navigator.userAgent; } - // If in incognito mode, store is null, but we still want bug report sending to work going off - // the in-memory console logs. + // If in incognito mode, store is null, but we still want bug report + // sending to work going off the in-memory console logs. let logs = []; if (store) { logs = await store.consume(true); @@ -391,7 +430,9 @@ module.exports = { url: bugReportEndpoint, body: { logs: logs, - text: userText || "User did not supply any additional text.", + text: ( + userText || "User did not supply any additional text." + ), version: version, user_agent: userAgent, },