1
0
Fork 0
mirror of https://github.com/imjasonh/nettrack synced 2026-07-07 00:32:38 +00:00
nettrack/background.js

283 lines
7.3 KiB
JavaScript
Raw Permalink Normal View History

// Import db.js
importScripts("db.js");
// Track focused tab and window
let focusedTabId = null;
let focusedWindowId = null;
// Track page visit sessions (time spent on page while focused)
const pageSessions = new Map(); // tabId -> {startTime, url, host}
// Constants
const DATA_RETENTION_DAYS = 30;
const CLEANUP_ALARM_NAME = "nettrack-cleanup";
// Restore service worker state on startup
(async () => {
const state = await chrome.storage.local.get([
"focusedTabId",
"focusedWindowId",
]);
if (state.focusedTabId) focusedTabId = state.focusedTabId;
if (state.focusedWindowId) focusedWindowId = state.focusedWindowId;
})();
// Persist service worker state
async function persistState() {
await chrome.storage.local.set({
focusedTabId,
focusedWindowId,
});
}
// Initialize database - use promise to handle race conditions
const dbInitPromise = db
.init()
.then(() => {
console.log("NetTrack database initialized");
return db;
})
.catch((err) => {
console.error("Failed to initialize database:", err);
throw err;
});
// End a page session and store the record
async function endPageSession(tabId) {
const session = pageSessions.get(tabId);
if (!session) return;
const endTime = Date.now();
const duration = endTime - session.startTime;
// Store the page visit record
const record = {
request_started: session.startTime,
browser_host: session.host,
resource_host: session.host,
milliseconds: duration,
resource_type: "page_visit",
};
await storeRecord(record);
pageSessions.delete(tabId);
}
// Start a page session
async function startPageSession(tabId) {
try {
const tab = await chrome.tabs.get(tabId);
if (
!tab.url ||
tab.url.startsWith("chrome://") ||
tab.url.startsWith("about:")
) {
return;
}
const host = extractHost(tab.url);
pageSessions.set(tabId, {
startTime: Date.now(),
url: tab.url,
host: host,
});
} catch (e) {
// Clean up if tab was closed during operation
pageSessions.delete(tabId);
console.error("Error starting page session:", e);
}
}
// Track which tab is currently focused
chrome.tabs.onActivated.addListener(async (activeInfo) => {
// End session for previously focused tab
if (focusedTabId !== null && focusedTabId !== activeInfo.tabId) {
await endPageSession(focusedTabId);
}
focusedTabId = activeInfo.tabId;
focusedWindowId = activeInfo.windowId;
await persistState();
// Start session for newly focused tab
await startPageSession(focusedTabId);
});
// Track which window is currently focused
chrome.windows.onFocusChanged.addListener(async (windowId) => {
// If window lost focus, end current session
if (windowId === chrome.windows.WINDOW_ID_NONE) {
if (focusedTabId !== null) {
await endPageSession(focusedTabId);
}
focusedTabId = null;
focusedWindowId = windowId;
} else {
// Window gained focus
focusedWindowId = windowId;
// Get the active tab in the focused window
try {
const tabs = await chrome.tabs.query({
active: true,
windowId: windowId,
});
if (tabs.length > 0) {
const newFocusedTabId = tabs[0].id;
// End previous session if different tab
if (focusedTabId !== null && focusedTabId !== newFocusedTabId) {
await endPageSession(focusedTabId);
}
focusedTabId = newFocusedTabId;
// Start new session
await startPageSession(focusedTabId);
}
} catch (e) {
console.error("Error getting active tab:", e);
}
}
});
// Initialize focused tab on startup
chrome.tabs.query({ active: true, currentWindow: true }).then((tabs) => {
if (tabs.length > 0) {
focusedTabId = tabs[0].id;
focusedWindowId = tabs[0].windowId;
startPageSession(focusedTabId);
}
});
// Extract hostname from URL
function extractHost(url) {
try {
const urlObj = new URL(url);
return urlObj.hostname;
} catch (e) {
return url;
}
}
// Check if a tab is currently focused
function isTabFocused(tabId) {
return (
tabId === focusedTabId && focusedWindowId !== chrome.windows.WINDOW_ID_NONE
);
}
// Track page navigations using webNavigation API
chrome.webNavigation.onCompleted.addListener(async (details) => {
// Only track main frame (actual page loads)
if (details.frameId !== 0) return;
// Only track if tab is focused
if (!isTabFocused(details.tabId)) return;
// End previous session and start new one (navigation within same tab)
await endPageSession(details.tabId);
await startPageSession(details.tabId);
try {
const tab = await chrome.tabs.get(details.tabId);
// Skip chrome:// and other internal URLs
if (
!tab.url ||
tab.url.startsWith("chrome://") ||
tab.url.startsWith("about:") ||
tab.url.startsWith("chrome-extension://")
) {
return;
}
const browserHost = extractHost(tab.url);
// Get timing information via content script
chrome.scripting
.executeScript({
target: { tabId: details.tabId },
func: () => {
const timing = performance.getEntriesByType("navigation")[0];
if (timing && timing.loadEventEnd > 0) {
// Use performance.timeOrigin to get actual start time
const actualStartTime = performance.timeOrigin + timing.fetchStart;
return {
startTime: actualStartTime,
duration: timing.loadEventEnd - timing.fetchStart,
};
}
return null;
},
})
.then(async (results) => {
if (results && results[0] && results[0].result) {
const timingData = results[0].result;
// Only record if we have valid timing data
if (timingData.duration > 0) {
const record = {
request_started: Math.round(timingData.startTime),
browser_host: browserHost,
resource_host: browserHost,
milliseconds: Math.round(timingData.duration),
resource_type: "navigation",
};
await storeRecord(record);
}
}
})
.catch((err) => {
console.error("Error getting navigation timing:", err);
});
} catch (e) {
console.error("Error tracking navigation:", e);
}
});
// Clean up page session when tab is closed
chrome.tabs.onRemoved.addListener(async (tabId) => {
await endPageSession(tabId);
});
// Store a record in IndexedDB
async function storeRecord(record) {
try {
await dbInitPromise;
await db.addRequest(record);
} catch (e) {
console.error("Error storing record:", e);
}
}
// Setup chrome.alarms for periodic cleanup
chrome.alarms.create(CLEANUP_ALARM_NAME, { periodInMinutes: 60 });
chrome.alarms.onAlarm.addListener(async (alarm) => {
if (alarm.name === CLEANUP_ALARM_NAME) {
await cleanupOldData();
}
});
// Clean up data older than 30 days
async function cleanupOldData() {
try {
await dbInitPromise;
const cutoffTime = Date.now() - DATA_RETENTION_DAYS * 24 * 60 * 60 * 1000;
const deletedCount = await db.deleteOlderThan(cutoffTime);
if (deletedCount > 0) {
console.log(`Cleaned up ${deletedCount} old records`);
}
} catch (e) {
console.error("Error cleaning up old data:", e);
}
}
// Run cleanup on extension install/update
chrome.runtime.onInstalled.addListener(async () => {
await cleanupOldData();
});