Comparing version 0.0.11 to 0.0.12
@@ -1,5 +0,5 @@ | ||
import { BasicReport, CrashDetectionOptions } from './types'; | ||
import { BaseStateReport, CrashDetectionOptions } from './types'; | ||
/** | ||
* Main function to initialize crash detection. This should be run from the main thread of the tab. | ||
*/ | ||
export declare function initCrashDetection<CustomProperties extends BasicReport>(options: CrashDetectionOptions<CustomProperties>): void; | ||
export declare function initCrashDetection<CustomStateReport extends BaseStateReport>(options: CrashDetectionOptions<CustomStateReport>): void; |
224
dist/lib.js
@@ -15,2 +15,99 @@ (function webpackUniversalModuleDefinition(root, factory) { | ||
/***/ "./public/lib/events.ts": | ||
/*!******************************!*\ | ||
!*** ./public/lib/events.ts ***! | ||
\******************************/ | ||
/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => { | ||
__webpack_require__.r(__webpack_exports__); | ||
/* harmony export */ __webpack_require__.d(__webpack_exports__, { | ||
/* harmony export */ createCloseEvent: () => (/* binding */ createCloseEvent), | ||
/* harmony export */ createCrashDetectedEvent: () => (/* binding */ createCrashDetectedEvent), | ||
/* harmony export */ createCrashReportedEvent: () => (/* binding */ createCrashReportedEvent), | ||
/* harmony export */ createPingEvent: () => (/* binding */ createPingEvent), | ||
/* harmony export */ createStaleTabDetectedEvent: () => (/* binding */ createStaleTabDetectedEvent), | ||
/* harmony export */ createStaleTabReportedEvent: () => (/* binding */ createStaleTabReportedEvent), | ||
/* harmony export */ createStartEvent: () => (/* binding */ createStartEvent), | ||
/* harmony export */ createUpdateEvent: () => (/* binding */ createUpdateEvent), | ||
/* harmony export */ isCloseEvent: () => (/* binding */ isCloseEvent), | ||
/* harmony export */ isCrashDetectedEvent: () => (/* binding */ isCrashDetectedEvent), | ||
/* harmony export */ isCrashReportedEvent: () => (/* binding */ isCrashReportedEvent), | ||
/* harmony export */ isPingEvent: () => (/* binding */ isPingEvent), | ||
/* harmony export */ isStaleTabDetectedEvent: () => (/* binding */ isStaleTabDetectedEvent), | ||
/* harmony export */ isStaleTabReportedEvent: () => (/* binding */ isStaleTabReportedEvent), | ||
/* harmony export */ isStartEvent: () => (/* binding */ isStartEvent), | ||
/* harmony export */ isUpdateEvent: () => (/* binding */ isUpdateEvent) | ||
/* harmony export */ }); | ||
function isStartEvent(event) { | ||
return (event === null || event === void 0 ? void 0 : event.event) === 'start'; | ||
} | ||
function createStartEvent(info) { | ||
return { | ||
event: 'start', | ||
info: info, | ||
}; | ||
} | ||
function createPingEvent() { | ||
return { event: 'ping' }; | ||
} | ||
function isPingEvent(event) { | ||
return (event === null || event === void 0 ? void 0 : event.event) === 'ping'; | ||
} | ||
function isUpdateEvent(event) { | ||
return (event === null || event === void 0 ? void 0 : event.event) === 'update'; | ||
} | ||
function createUpdateEvent(info) { | ||
return { event: 'update', info: info }; | ||
} | ||
function isCloseEvent(event) { | ||
return (event === null || event === void 0 ? void 0 : event.event) === 'close'; | ||
} | ||
function createCloseEvent(info) { | ||
return { | ||
event: 'close', | ||
info: info, | ||
}; | ||
} | ||
function isCrashDetectedEvent(event) { | ||
return (event === null || event === void 0 ? void 0 : event.event) === 'crash-detected'; | ||
} | ||
function createCrashDetectedEvent(tab, reporter) { | ||
return { | ||
event: 'crash-detected', | ||
tab: tab, | ||
reporter: reporter, | ||
}; | ||
} | ||
function isStaleTabDetectedEvent(event) { | ||
return (event === null || event === void 0 ? void 0 : event.event) === 'stale-tab-detected'; | ||
} | ||
function createStaleTabDetectedEvent(tab, reporter) { | ||
return { | ||
event: 'stale-tab-detected', | ||
tab: tab, | ||
reporter: reporter, | ||
}; | ||
} | ||
function isCrashReportedEvent(event) { | ||
return (event === null || event === void 0 ? void 0 : event.event) === 'crash-reported'; | ||
} | ||
function createCrashReportedEvent(id) { | ||
return { | ||
event: 'crash-reported', | ||
id: id, | ||
}; | ||
} | ||
function isStaleTabReportedEvent(event) { | ||
return (event === null || event === void 0 ? void 0 : event.event) === 'stale-tab-reported'; | ||
} | ||
function createStaleTabReportedEvent(tab) { | ||
return { | ||
event: 'stale-tab-reported', | ||
tab: tab, | ||
}; | ||
} | ||
/***/ }), | ||
/***/ "./public/lib/init.client.worker.ts": | ||
@@ -27,3 +124,3 @@ /*!******************************************!*\ | ||
/* harmony import */ var _utils__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./utils */ "./public/lib/utils.ts"); | ||
// @ts-nocheck | ||
/* harmony import */ var _events__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./events */ "./public/lib/events.ts"); | ||
var __assign = (undefined && undefined.__assign) || function () { | ||
@@ -77,2 +174,3 @@ __assign = Object.assign || function(t) { | ||
/** | ||
@@ -89,3 +187,3 @@ * Main logic of the Web Worker running with the tab. Create a separate file for the worker with code: | ||
var _this = this; | ||
var lastInfo; | ||
var lastStateReport; | ||
var tabLastActive = Date.now(); | ||
@@ -98,3 +196,3 @@ var db; | ||
} | ||
if (lastInfo === null || lastInfo === void 0 ? void 0 : lastInfo.id) { | ||
if (lastStateReport === null || lastStateReport === void 0 ? void 0 : lastStateReport.id) { | ||
var transaction = db.transaction(['tabs'], 'readwrite'); | ||
@@ -104,9 +202,10 @@ var store = transaction.objectStore('tabs'); | ||
// save latest received info here - the tab may be paused because of debugging but we need to mark the tab as alive anyway because the worker is still alive | ||
store.put(__assign(__assign({}, structuredClone(lastInfo)), { tabLastActive: tabLastActive, workerLastActive: workerLastActive })); | ||
store.put(__assign(__assign({}, structuredClone(lastStateReport)), { tabLastActive: tabLastActive, workerLastActive: workerLastActive })); | ||
} | ||
// ping to tab so it can send latest values | ||
// saving will happen on the next pingInterval | ||
postMessage({ event: 'ping' }); | ||
var pingEvent = (0,_events__WEBPACK_IMPORTED_MODULE_1__.createPingEvent)(); | ||
postMessage(pingEvent); | ||
}, options.pingInterval); | ||
addEventListener('message', function (event) { return __awaiter(_this, void 0, void 0, function () { | ||
addEventListener('message', function (message) { return __awaiter(_this, void 0, void 0, function () { | ||
var transaction, store; | ||
@@ -116,8 +215,8 @@ return __generator(this, function (_a) { | ||
case 0: | ||
if (event.data.event === 'update') { | ||
if ((0,_events__WEBPACK_IMPORTED_MODULE_1__.isUpdateEvent)(message.data)) { | ||
tabLastActive = Date.now(); | ||
lastInfo = structuredClone(event.data.info); | ||
lastStateReport = structuredClone(message.data.info); | ||
// saving cannot happen here because message may not be sent when tab is paused (e.g. while debugging) | ||
} | ||
if (!(event.data.event === 'start')) return [3 /*break*/, 2]; | ||
if (!(0,_events__WEBPACK_IMPORTED_MODULE_1__.isStartEvent)(message.data)) return [3 /*break*/, 2]; | ||
return [4 /*yield*/, (0,_utils__WEBPACK_IMPORTED_MODULE_0__.getDb)(options.dbName)]; | ||
@@ -128,6 +227,6 @@ case 1: | ||
case 2: | ||
if (event.data.event === 'close') { | ||
if ((0,_events__WEBPACK_IMPORTED_MODULE_1__.isCloseEvent)(message.data) && db) { | ||
transaction = db.transaction(['tabs'], 'readwrite'); | ||
store = transaction.objectStore('tabs'); | ||
store.delete(event.data.info.id); | ||
store.delete(message.data.info.id); | ||
db = undefined; | ||
@@ -155,3 +254,3 @@ } | ||
/* harmony import */ var _utils__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./utils */ "./public/lib/utils.ts"); | ||
// @ts-nocheck | ||
/* harmony import */ var _events__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./events */ "./public/lib/events.ts"); | ||
var __assign = (undefined && undefined.__assign) || function () { | ||
@@ -205,2 +304,3 @@ __assign = Object.assign || function(t) { | ||
/** | ||
@@ -222,3 +322,3 @@ * Main logic of the Shared Worker responsible for detecting crashes. Create a separate file for the worker with code: | ||
function handleMessageFromReporter(event) { | ||
if (event.data.event === 'crash-reported' && event.data.id) { | ||
if ((0,_events__WEBPACK_IMPORTED_MODULE_1__.isCrashReportedEvent)(event.data)) { | ||
var transaction = db.transaction(['tabs'], 'readwrite'); | ||
@@ -228,6 +328,6 @@ var store = transaction.objectStore('tabs'); | ||
} | ||
if (event.data.event === 'stale-tab-reported' && event.data.tab) { | ||
if ((0,_events__WEBPACK_IMPORTED_MODULE_1__.isStaleTabReportedEvent)(event.data)) { | ||
var transaction = db.transaction(['tabs'], 'readwrite'); | ||
var store = transaction.objectStore('tabs'); | ||
store.store(__assign(__assign({}, event.data.tab), { staleReported: true })); | ||
store.put(__assign(__assign({}, event.data.tab), { staleReported: true })); | ||
} | ||
@@ -265,6 +365,7 @@ } | ||
// use only one tab for reporting | ||
var reporter = activeTabs.pop(); | ||
var reporter = activeTabs.pop(); // must be defined based on the check above | ||
inactiveTabs.forEach(function (tab) { | ||
openPorts.forEach(function (port) { | ||
port.postMessage({ event: 'crash-detected', tab: tab, reporter: reporter }); | ||
var event = (0,_events__WEBPACK_IMPORTED_MODULE_1__.createCrashDetectedEvent)(tab, reporter); | ||
port.postMessage(event); | ||
}); | ||
@@ -274,3 +375,4 @@ }); | ||
openPorts.forEach(function (port) { | ||
port.postMessage({ event: 'stale-tab-detected', tab: tab, reporter: reporter }); | ||
var event = (0,_events__WEBPACK_IMPORTED_MODULE_1__.createStaleTabDetectedEvent)(tab, reporter); | ||
port.postMessage(event); | ||
}); | ||
@@ -280,3 +382,3 @@ }); | ||
} | ||
self.onconnect = function (event) { | ||
self.addEventListener('connect', function (event) { | ||
return __awaiter(this, void 0, void 0, function () { | ||
@@ -305,3 +407,3 @@ var port_1; | ||
}); | ||
}; | ||
}); | ||
} | ||
@@ -323,3 +425,3 @@ | ||
/* harmony import */ var _utils__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./utils */ "./public/lib/utils.ts"); | ||
// @ts-nocheck | ||
/* harmony import */ var _events__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./events */ "./public/lib/events.ts"); | ||
var __awaiter = (undefined && undefined.__awaiter) || function (thisArg, _arguments, P, generator) { | ||
@@ -362,2 +464,3 @@ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } | ||
/** | ||
@@ -369,38 +472,36 @@ * Main function to initialize crash detection. This should be run from the main thread of the tab. | ||
var detector; | ||
var info = {}; | ||
var stateReport = {}; | ||
var db; | ||
var log = function (log) { | ||
var _a; | ||
log.id = info.id; | ||
log.id = String(stateReport.id); | ||
(_a = options.log) === null || _a === void 0 ? void 0 : _a.call(options, log); | ||
}; | ||
function handleDetectorMessage(event) { | ||
function handleDetectorMessage(message) { | ||
return __awaiter(this, void 0, void 0, function () { | ||
var tab, success, tab, success; | ||
var tab, success, crashReportedEvent, tab, success, staleTabReportedEvent; | ||
return __generator(this, function (_a) { | ||
switch (_a.label) { | ||
case 0: | ||
if (!(event.data.event === 'crash-detected' && event.data.reporter.id === info.id)) return [3 /*break*/, 2]; | ||
log({ event: 'crash-detected' }); | ||
tab = event.data.tab; | ||
if (!((0,_events__WEBPACK_IMPORTED_MODULE_1__.isCrashDetectedEvent)(message.data) && message.data.reporter.id === stateReport.id)) return [3 /*break*/, 2]; | ||
tab = message.data.tab; | ||
return [4 /*yield*/, options.reportCrash(tab)]; | ||
case 1: | ||
success = _a.sent(); | ||
log({ event: 'crash-reported', success: success }); | ||
if (success) { | ||
log({ event: 'crash-report-confirmed' }); | ||
detector.port.postMessage({ event: 'crash-reported', id: event.data.tab.id }); | ||
crashReportedEvent = (0,_events__WEBPACK_IMPORTED_MODULE_1__.createCrashReportedEvent)(message.data.tab.id); | ||
detector.port.postMessage(crashReportedEvent); | ||
} | ||
_a.label = 2; | ||
case 2: | ||
if (!(options.reportStaleTab && event.data.event === 'stale-tab-detected' && event.data.reporter.id === info.id)) return [3 /*break*/, 4]; | ||
log({ event: 'stale-tab-detected' }); | ||
tab = event.data.tab; | ||
if (!(options.reportStaleTab && | ||
(0,_events__WEBPACK_IMPORTED_MODULE_1__.isStaleTabDetectedEvent)(message.data) && | ||
message.data.reporter.id === stateReport.id)) return [3 /*break*/, 4]; | ||
tab = message.data.tab; | ||
return [4 /*yield*/, options.reportStaleTab(tab)]; | ||
case 3: | ||
success = _a.sent(); | ||
log({ event: 'stale-tab-reported', success: success }); | ||
if (success) { | ||
log({ event: 'stale-tab-confirmed' }); | ||
detector.port.postMessage({ event: 'stale-tab-reported', id: event.data.tab.id }); | ||
staleTabReportedEvent = (0,_events__WEBPACK_IMPORTED_MODULE_1__.createStaleTabReportedEvent)(message.data.tab); | ||
detector.port.postMessage(staleTabReportedEvent); | ||
} | ||
@@ -417,4 +518,4 @@ _a.label = 4; | ||
function initialize() { | ||
info.id = options.id; | ||
info.tabFirstActive = Date.now(); | ||
stateReport.id = options.id; | ||
stateReport.tabFirstActive = Date.now(); | ||
} | ||
@@ -424,13 +525,13 @@ /** | ||
*/ | ||
function updateInfo() { | ||
options.updateInfo(info); | ||
//log({ event: 'updated' }); | ||
worker.postMessage({ | ||
event: 'update', | ||
info: info, | ||
}); | ||
function handleWebWorkerMessage(message) { | ||
if ((0,_events__WEBPACK_IMPORTED_MODULE_1__.isPingEvent)(message.data)) { | ||
// info should have all required info when passed here | ||
options.updateInfo(stateReport); | ||
var updateEvent = (0,_events__WEBPACK_IMPORTED_MODULE_1__.createUpdateEvent)(stateReport); | ||
worker.postMessage(updateEvent); | ||
} | ||
} | ||
function registerWorkers() { | ||
worker = options.createClientWorker(); | ||
worker.addEventListener('message', updateInfo); | ||
worker.addEventListener('message', handleWebWorkerMessage); | ||
detector = options.createDetectorWorker(); | ||
@@ -441,3 +542,3 @@ detector.port.addEventListener('message', handleDetectorMessage); | ||
function unregisterWorkers() { | ||
worker.removeEventListener('message', updateInfo); | ||
worker.removeEventListener('message', handleWebWorkerMessage); | ||
detector.port.removeEventListener('message', handleDetectorMessage); | ||
@@ -447,3 +548,2 @@ } | ||
// beforeunload is triggered only after at least one interaction | ||
log({ event: 'loaded' }); | ||
window.addEventListener('click', start); | ||
@@ -453,4 +553,4 @@ } | ||
return __awaiter(this, void 0, void 0, function () { | ||
var startEvent; | ||
return __generator(this, function (_a) { | ||
log({ event: 'started' }); | ||
window.removeEventListener('click', start); | ||
@@ -460,19 +560,12 @@ initialize(); | ||
window.addEventListener('beforeunload', function () { | ||
log({ event: 'unloaded' }); | ||
// to avoid any delays clean-up happens in the current tab as well | ||
var transaction = db.transaction(['tabs'], 'readwrite'); | ||
var store = transaction.objectStore('tabs'); | ||
store.delete(info.id); | ||
worker.postMessage({ | ||
event: 'close', | ||
info: info, | ||
}); | ||
store.delete(stateReport.id); | ||
var closeEvent = (0,_events__WEBPACK_IMPORTED_MODULE_1__.createCloseEvent)(stateReport); | ||
worker.postMessage(closeEvent); | ||
unregisterWorkers(); | ||
log({ event: 'unloaded-done' }); | ||
}); | ||
worker.postMessage({ | ||
event: 'start', | ||
info: info, | ||
}); | ||
log({ event: 'started-done' }); | ||
startEvent = (0,_events__WEBPACK_IMPORTED_MODULE_1__.createStartEvent)(stateReport); | ||
worker.postMessage(startEvent); | ||
return [2 /*return*/]; | ||
@@ -548,8 +641,6 @@ }); | ||
var request = indexedDB.open(dbName); | ||
request.onerror = function (event) { | ||
// reject(event.target.error); | ||
request.onerror = function () { | ||
reject(request.error); | ||
}; | ||
request.onsuccess = function (event) { | ||
// resolve(event.target.result); | ||
request.onsuccess = function () { | ||
resolve(request.result); | ||
@@ -561,3 +652,2 @@ }; | ||
} | ||
// const db = event.target.result; | ||
var db = request.result; | ||
@@ -564,0 +654,0 @@ if (!db.objectStoreNames.contains('tabs')) { |
@@ -1,2 +0,2 @@ | ||
export type CrashDetectionOptions<CustomProperties> = { | ||
export type CrashDetectionOptions<CustomStateReport extends BaseStateReport> = { | ||
/** | ||
@@ -14,3 +14,3 @@ * Unique id of a tab | ||
*/ | ||
reportCrash: (crashedTab: CustomProperties) => Promise<boolean>; | ||
reportCrash: (crashedTab: CustomStateReport) => Promise<boolean>; | ||
/** | ||
@@ -20,7 +20,7 @@ * Report a stale tab (e.g. over HTTP). Needs to return true if reporting was successful. | ||
*/ | ||
reportStaleTab?: (crashedTab: CustomProperties) => Promise<boolean>; | ||
reportStaleTab?: (crashedTab: CustomStateReport) => Promise<boolean>; | ||
/** | ||
* Modify currentTab param with any parameters about the current tab needed to be reported back | ||
*/ | ||
updateInfo: (currentTab: CustomProperties) => void; | ||
updateInfo: (currentTab: CustomStateReport) => void; | ||
/** | ||
@@ -67,7 +67,8 @@ * Create the shared detector worker (see detector-worker.js) | ||
*/ | ||
export type BasicReport = { | ||
id: string; | ||
export type BaseStateReport = { | ||
id: IDBValidKey; | ||
tabLastActive: number; | ||
tabFirstActive: number; | ||
workerLastActive: number; | ||
staleReported: boolean; | ||
}; |
@@ -1,1 +0,1 @@ | ||
export declare function getDb(dbName: string): Promise<unknown>; | ||
export declare function getDb(dbName: string): Promise<IDBDatabase>; |
{ | ||
"name": "crashme", | ||
"version": "0.0.11", | ||
"version": "0.0.12", | ||
"main": "dist/lib.js", | ||
@@ -5,0 +5,0 @@ "types": "dist/index.d.ts", |
152
README.md
@@ -5,6 +5,7 @@ # Detecting Browser/Tab crashes POC | ||
## How to run it | ||
## How to run demo? | ||
1. Run `node ./server.js` | ||
2. Open http://localhost:1234 | ||
1. Run `npm run dev` | ||
2. Run `npm run server` | ||
3Open http://localhost:1234 | ||
3. You can open multiple tabs (each tab will get a unique name) | ||
@@ -22,119 +23,78 @@ 4. Logs are sent to the terminal via server.js | ||
## Tested approaches | ||
## How does it work? | ||
1. Detecting crashes before they occur | ||
2. Track and persist state of tabs (last alive ping + if it was closed properly). Send crash reports based on the state. | ||
There are two basic concept: | ||
### Detecting crashes before they occur | ||
1. Tab tracking | ||
The idea was to check if the page becomes unresponsive or is very close to hitting memory limits and report it over HTTP to persisted storage. | ||
Each browser tab reports its current state on regular intervals. The current state is saved in IndexedDB as a state report. | ||
The state contains properties like: last time when it was active, url, memory usage, etc. | ||
The state is removed from the db when then tab is closed by the user | ||
The tab may be near crash when: | ||
2. Crash detection | ||
1. Memory usage can be checked with `window.performance.memory` | ||
A separate process reads all state reports. If it happens that there's a state report that was saved and not removed for long period of time it means the tab was not closed correctly and it probably crashed. | ||
- Pros: | ||
- It provides total JS heap size, used heap size and the limit | ||
- Cons: | ||
- Browser may dynamically change limits and allocate additional memory | ||
- Available only in Chrome | ||
## How to use it in my project? | ||
2. When browser slows down. This could be checked by a ping mechanism using web/service workers. | ||
- Pros: | ||
- Available in all browsers | ||
- Cons: | ||
- When only one tab is opened and crashes the service worker may be killed immediately and there might be not enough time to ensure it manages to send a report about the crash. Based on some experiments only Firefox keeps the worker alive a bit longer. | ||
You need to create 2 files for workers and run init function in your app: | ||
I wasn't able to get reliable, consistent results with this approach | ||
`detector.worker.js` | ||
### Track and persist state of tabs | ||
```javascript | ||
import { initDetectorWorker } from 'crashme'; | ||
The idea is to track active tabs and last active pings + stop tracking when tab closes correctly. Based on that info if the tab stopped sending pings + it was not closed correctly we assume it's frozen or crashed. | ||
initDetectorWorker({ | ||
dbName: 'crashme.crashes', | ||
staleThreshold: 60000, | ||
crashThreshold: 5000, | ||
interval: 5000, | ||
}); | ||
``` | ||
Detection would consist of following components: | ||
`client.worker.js` | ||
1. Storage to keep information about active tabs | ||
2. Client code used to periodically report the state of a tab to the storage | ||
3. Detection logic that periodically checks the state of storage | ||
```javascript | ||
import { initDetectorWorker } from 'crashme'; | ||
### POC | ||
initClientWorker({ | ||
dbName: 'crashme.crashes', | ||
pingInterval: 1000, | ||
}); | ||
``` | ||
In the POC following approaches were considered: | ||
and run function to initialize detection: | ||
1. Storage | ||
- Browser local storage: | ||
- pros: easy to use | ||
- cons: can lead to incorrect state when multiple tabs access it at the same time (https://html.spec.whatwg.org/multipage/webstorage.html#introduction-15): "(...) authors are encouraged to assume that there is no locking mechanism (...)" | ||
- Browser session storage | ||
- cons: used by Sentry (see resources) but it's isolated to a single tab so to make it work it would require user to refresh the tab (not close it) after the crash which may not happen every time (https://developer.mozilla.org/en-US/docs/Web/API/Window/sessionStorage) | ||
- Browser indexed db | ||
- pros: can be shared between workers and allows transactional updates | ||
- External storage (over HTTP): | ||
- cons: will stop reporting when the user if offline though the tab may not crashed | ||
```javascript | ||
import { initCrashDetection } from 'crashme'; | ||
Choice: IndexedDB | ||
export function startReportingCrashes() { | ||
initCrashDetection({ | ||
id: Math.random().toString(36).substr(2,5), // create unique id | ||
2. Client code to periodically report the state (ping + closing) | ||
- setInterval inside the tab thread | ||
- cons: when tab is inactive setInterval gets deprioritized and will be executed less frequently | ||
- setInterval inside service/web worker thread | ||
- pros: when a message is sent the inactive/invisible tab can respond immediately | ||
dbName: 'crashme.crashes', | ||
Choice: setInterval inside a service/web worker | ||
createClientWorker: () => { | ||
return new Worker(new URL('./client.worker', import.meta.url)); | ||
}, | ||
3. Client code to save state to storage | ||
createDetectorWorker: () => { | ||
return new SharedWorker(new URL('./detector.worker', import.meta.url)); | ||
}, | ||
- save inside the tab thread | ||
- cons: debugging the page will stop the thread and detector could say it crashed | ||
- save inside service/web worker | ||
- pros: worker will keep working even if the tab is paused; | ||
reportCrash: async (tab) => { | ||
// add your logic to report the crash | ||
// return true if reporting was successul | ||
return true; | ||
}, | ||
Choice: Save inside a web worker | ||
updateInfo: (report) => { | ||
// enrich report with any properties you would like to track | ||
}, | ||
}); | ||
} | ||
A caveat is that Firefox doesn't kill the web worker immediately when tab crashes. This could lead to scenario when the detector thinks that the tab is still alive. At the same time we need to track the time tab was last active. To mitigate it we can keep both: last time the tab was active (for reporting) and last time the worker was active (to detect crashes). | ||
3. Detection logic | ||
- In a service/shared web worker | ||
- cons: can be a single instance running independently to tabs | ||
Choice: Use shared web worker. In theory it should work with a service worker as well though based on experiments service worker may be killed when tab crashes, while shared web workers seems to keep running. | ||
### | ||
```mermaid | ||
sequenceDiagram | ||
autonumber | ||
ClientController->>ClientWorker: Start | ||
loop Update Loop | ||
ClientWorker-->+ClientWorker: setInterval(..., 1000) | ||
ClientWorker-->>ClientController: ping | ||
ClientController->>ClientWorker: on ping from worker: post update { id, url, memory, ... } | ||
ClientWorker->>IndexedDb: put { id, url, memory, tabLastActive, ... } | ||
end | ||
loop Activity Loop | ||
ClientWorker-->+ClientWorker: setInterval(..., 1000) | ||
ClientWorker->>IndexedDb: put { workerLastActive, ... } | ||
end | ||
ClientController->>ClientWorker: Stop | ||
ClientWorker->>IndexedDb: delete { id } | ||
``` | ||
1. Client code executes in the same thread as the main app. It's responsible for starting the update loop in the worker. | ||
2. WebWorker starts the loop with setInterval. This is done in the worker to avoid slowing down setInterval on inactive tabs | ||
3. WebWorker pings the client for the data (WebWorker have no access to url, memory usage, etc.) | ||
4. WebWorker save the data with tabLastActive timestamps to the IndexedDB when receives a message from the tab. Saving is done in the worker to ensure it's a separate thread in cases the Client thread is paused because of debugging. | ||
5. WebWorker saves workerLastActive timestamp every second | ||
6. When Client is unloaded properly it sends the message to the WebWorker to remove the entry from IndexedDb | ||
A separate process check for stale tabs and reports back to the backend. It connects to the same IndexedDB | ||
```mermaid | ||
sequenceDiagram | ||
autonumber | ||
Detector->>+Detector: setInterval(..., 1000) | ||
Detector->>IndexedDb: get all tabs | ||
Detector->>Detector: check if workerLastActive > 3 seconds | ||
Detector->>-Backend: /crash-report { id, url, memory, tabLastActive ... } | ||
``` | ||
workerLastActive timestamp is used to detect actual crash of a tab and tabLastActive is used for reporting. They may be out of step in Firefox which keeps the worked active after the tab crashes OR when thread on the tab is paused due to debugging (web worker will keep running) |
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
78758
11
866
99