2025-12-09 11:17:57 -05:00
|
|
|
// 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}
|
|
|
|
|
|
2025-12-09 11:23:43 -05:00
|
|
|
// Constants
|
2025-12-09 11:17:57 -05:00
|
|
|
const DATA_RETENTION_DAYS = 30;
|
2025-12-09 11:23:43 -05:00
|
|
|
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,
|
|
|
|
|
});
|
|
|
|
|
}
|
2025-12-09 11:17:57 -05:00
|
|
|
|
2025-12-09 11:23:43 -05:00
|
|
|
// Initialize database - use promise to handle race conditions
|
|
|
|
|
const dbInitPromise = db
|
|
|
|
|
.init()
|
2025-12-09 11:17:57 -05:00
|
|
|
.then(() => {
|
|
|
|
|
console.log("NetTrack database initialized");
|
2025-12-09 11:23:43 -05:00
|
|
|
return db;
|
2025-12-09 11:17:57 -05:00
|
|
|
})
|
|
|
|
|
.catch((err) => {
|
|
|
|
|
console.error("Failed to initialize database:", err);
|
2025-12-09 11:23:43 -05:00
|
|
|
throw err;
|
2025-12-09 11:17:57 -05:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// 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) {
|
2025-12-09 11:23:43 -05:00
|
|
|
// Clean up if tab was closed during operation
|
|
|
|
|
pageSessions.delete(tabId);
|
2025-12-09 11:17:57 -05:00
|
|
|
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;
|
2025-12-09 11:23:43 -05:00
|
|
|
await persistState();
|
2025-12-09 11:17:57 -05:00
|
|
|
|
|
|
|
|
// 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];
|
2025-12-09 11:23:43 -05:00
|
|
|
if (timing && timing.loadEventEnd > 0) {
|
|
|
|
|
// Use performance.timeOrigin to get actual start time
|
|
|
|
|
const actualStartTime = performance.timeOrigin + timing.fetchStart;
|
2025-12-09 11:17:57 -05:00
|
|
|
return {
|
2025-12-09 11:23:43 -05:00
|
|
|
startTime: actualStartTime,
|
2025-12-09 11:17:57 -05:00
|
|
|
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 = {
|
2025-12-09 11:23:43 -05:00
|
|
|
request_started: Math.round(timingData.startTime),
|
2025-12-09 11:17:57 -05:00
|
|
|
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 {
|
2025-12-09 11:23:43 -05:00
|
|
|
await dbInitPromise;
|
2025-12-09 11:17:57 -05:00
|
|
|
await db.addRequest(record);
|
|
|
|
|
} catch (e) {
|
|
|
|
|
console.error("Error storing record:", e);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-09 11:23:43 -05:00
|
|
|
// 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) {
|
2025-12-09 11:17:57 -05:00
|
|
|
await cleanupOldData();
|
|
|
|
|
}
|
2025-12-09 11:23:43 -05:00
|
|
|
});
|
2025-12-09 11:17:57 -05:00
|
|
|
|
|
|
|
|
// Clean up data older than 30 days
|
|
|
|
|
async function cleanupOldData() {
|
|
|
|
|
try {
|
2025-12-09 11:23:43 -05:00
|
|
|
await dbInitPromise;
|
2025-12-09 11:17:57 -05:00
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-09 11:23:43 -05:00
|
|
|
// Run cleanup on extension install/update
|
|
|
|
|
chrome.runtime.onInstalled.addListener(async () => {
|
|
|
|
|
await cleanupOldData();
|
2025-12-09 11:17:57 -05:00
|
|
|
});
|