A reliable solution might be to keep a connected runtime port in the background script for each content script, no unload/beforeunload would be necessary because when the page is finally navigated away the port will automatically disconnect in the background script thus notifying it of the end-of-life for this page.
Some notes:
- Since timers set by a content script can be accidentally/intentionally cleared by the page, the example tracks the time in the background script.
- A bug or a poor design decision in MV3 causes runtime ports to disconnect after five minutes so the example reconnects the ports periodically.
- Not tested so maybe the added props on `port` won't persist and you'll need to use an additional Map().
let port;
reconnect();
function reconnect(port) {
port?.onDisconnect.removeListener(reconnect);
port = chrome.runtime.connect({name: 'tracker'});
port.onDisconnect.addListener(reconnect);
}
function onIdleDetected() {
port.postMessage({idle: true});
}
function onActivityDetected() {
port.postMessage({idle: false});
}
// background script:
chrome.runtime.onConnect.addListener(port => {
port._timer = setTimeout(forceReconnect, 270e3, port);
port._timeSpent = 0;
port._timeLastActive = 0;
port.onDisconnect.addListener(onPortDisconnected);
port.onMessage.addListener(onPortMessage);
});
function onPortMessage(msg, port) {
const now = performance.now();
if (msg.idle && port._timeLastActive) {
port._timeSpent += now - port._timeLastActive;
port._timeLastActive = 0;
} else if (!msg.idle && !port._timeLastActive) {
port._timeLastActive = now;
}
}
function onPortDisconnected(port) {
clearTimeout(port._timer);
onPortMessage({idle: true}, port);
console.log(port.sender.url, 'spent', (port._timeSpent / 1000).toFixed(1), 's');
}
function forceReconnect(port) {
port.onDisconnect.removeListener(onPortDisconnected);
port.onMessage.removeListener(onPortMessage);
port.disconnect();