Skip to content

Instantly share code, notes, and snippets.

@npezarro
Last active April 21, 2026 19:58
Show Gist options
  • Select an option

  • Save npezarro/020617d8f27c31c64e9b2f9fcd561a70 to your computer and use it in GitHub Desktop.

Select an option

Save npezarro/020617d8f27c31c64e9b2f9fcd561a70 to your computer and use it in GitHub Desktop.
// ==UserScript==
// @name Browser Logs
// @namespace https://github.com/npezarro/scripts
// @version 1.5
// @description Lightweight read-only browser observer. Sends page state, console logs, and navigation events to a relay for CLI querying. No page interaction, no freezing.
// @author npezarro
// @match *://*/*
// @grant GM_xmlhttpRequest
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_registerMenuCommand
// @grant unsafeWindow
// @connect pezant.ca
// @connect localhost
// @run-at document-start
// @updateURL https://gist.githubusercontent.com/npezarro/020617d8f27c31c64e9b2f9fcd561a70/raw/browser-logs.user.js
// @downloadURL https://gist.githubusercontent.com/npezarro/020617d8f27c31c64e9b2f9fcd561a70/raw/browser-logs.user.js
// ==/UserScript==
(function () {
'use strict';
// Skip iframes
if (window.self !== window.top) return;
// --- Config ---
const API_BASE = GM_getValue('browserLogsApi', 'https://pezant.ca/api/browser-logs');
const SEND_INTERVAL_MS = 10000;
const MAX_CONSOLE_BUFFER = 50;
const MAX_ERROR_BUFFER = 20;
// --- Device fingerprint (auto-detected, not persisted — identifies the machine) ---
function detectDeviceInfo() {
const ua = navigator.userAgent;
const uad = navigator.userAgentData;
// OS detection
let os = 'unknown';
if (uad?.platform) {
os = uad.platform.toLowerCase();
} else if (/Windows/.test(ua)) {
os = 'windows';
} else if (/Macintosh|Mac OS/.test(ua)) {
os = 'macos';
} else if (/Android/.test(ua)) {
os = 'android';
} else if (/iPhone|iPad/.test(ua)) {
os = 'ios';
} else if (/Linux/.test(ua)) {
os = 'linux';
} else if (/CrOS/.test(ua)) {
os = 'chromeos';
}
// Browser detection
let browser = 'unknown';
if (/Edg\//.test(ua)) browser = 'edge';
else if (/Firefox\//.test(ua)) browser = 'firefox';
else if (/Chrome\//.test(ua)) browser = 'chrome';
else if (/Safari\//.test(ua) && !/Chrome/.test(ua)) browser = 'safari';
// Device type
const mobile = uad?.mobile ?? /Mobi|Android|iPhone|iPad/i.test(ua);
const type = mobile ? 'mobile' : 'desktop';
// Screen signature (helps distinguish same-OS machines)
const screen = `${window.screen.width}x${window.screen.height}`;
return { os, browser, type, screen, ua: ua.slice(0, 200) };
}
// --- Device ID: prompt on first run, persist across sessions ---
let deviceId = GM_getValue('browserLogsDeviceId', '');
const deviceInfo = detectDeviceInfo();
if (!deviceId) {
// Build a suggestion from detected info
const suggestion = `${deviceInfo.type}-${deviceInfo.os}-${deviceInfo.browser}`;
// Defer prompt until DOM is interactive so it doesn't block page load
const promptForName = () => {
const name = prompt(
`[Browser Logs] Name this device for tracking.\n` +
`Detected: ${deviceInfo.type}, ${deviceInfo.os}, ${deviceInfo.browser}, ${deviceInfo.screen}\n\n` +
`Examples: "home-pc", "work-mac", "phone", "laptop"`,
suggestion
);
deviceId = (name || suggestion).trim().toLowerCase().replace(/[^a-z0-9-]/g, '-');
GM_setValue('browserLogsDeviceId', deviceId);
};
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', promptForName, { once: true });
} else {
promptForName();
}
}
// --- State ---
const consoleBuffer = [];
const errorBuffer = [];
let lastUrl = location.href;
let lastTitle = '';
let tabId = null;
// --- Inject console patcher into the REAL page context via <script> tag ---
// Perf: buffers raw args in a plain array in page context, NO JSON.stringify on hot path.
// The TM script drains the buffer on a 2s interval instead of per-call CustomEvent dispatch.
const patchScript = document.createElement('script');
patchScript.textContent = `(function() {
var orig = {
log: console.log.bind(console),
warn: console.warn.bind(console),
error: console.error.bind(console),
info: console.info.bind(console)
};
window.__browserLogsOrig = orig;
window.__browserLogQueue = [];
var MAX_QUEUE = 100;
function enqueue(level, args) {
var q = window.__browserLogQueue;
if (q.length >= MAX_QUEUE) return;
try {
var parts = [];
for (var i = 0; i < args.length && i < 5; i++) {
try {
parts.push(typeof args[i] === 'string' ? args[i].slice(0, 300) : typeof args[i] === 'object' ? '[object]' : String(args[i]).slice(0, 300));
} catch(e) {
parts.push('[unserializable]');
}
}
q.push({ level: level, message: parts.join(' '), ts: Date.now() });
} catch(e) {}
}
console.log = function() { enqueue('log', arguments); return orig.log.apply(console, arguments); };
console.warn = function() { enqueue('warn', arguments); return orig.warn.apply(console, arguments); };
console.error = function() { enqueue('error', arguments); return orig.error.apply(console, arguments); };
console.info = function() { enqueue('info', arguments); return orig.info.apply(console, arguments); };
})();`;
(document.head || document.documentElement).prepend(patchScript);
patchScript.remove();
// --- Drain console log queue from page context on interval (not per-call) ---
setInterval(() => {
if (document.visibilityState === 'hidden') return;
try {
const queue = unsafeWindow.__browserLogQueue;
if (!queue || queue.length === 0) return;
const items = queue.splice(0, 50);
for (const item of items) {
consoleBuffer.push(item);
if (consoleBuffer.length > MAX_CONSOLE_BUFFER) consoleBuffer.shift();
}
} catch {}
}, 2000);
// --- Capture uncaught errors ---
window.addEventListener('error', (e) => {
errorBuffer.push({
message: e.message,
source: e.filename,
line: e.lineno,
col: e.colno,
ts: Date.now(),
});
if (errorBuffer.length > MAX_ERROR_BUFFER) errorBuffer.shift();
});
window.addEventListener('unhandledrejection', (e) => {
errorBuffer.push({
message: `Unhandled rejection: ${e.reason}`,
ts: Date.now(),
});
if (errorBuffer.length > MAX_ERROR_BUFFER) errorBuffer.shift();
});
// --- Init session state (deferred until DOM is available) ---
function initSession() {
tabId = sessionStorage.getItem('browserLogsTabId');
if (!tabId) {
tabId = 'tab_' + Math.random().toString(36).slice(2, 10);
sessionStorage.setItem('browserLogsTabId', tabId);
}
lastTitle = document.title;
}
// --- Build page snapshot ---
function getSnapshot() {
return {
tabId,
deviceId,
deviceInfo,
url: location.href,
title: document.title,
hostname: location.hostname,
timestamp: new Date().toISOString(),
viewport: {
scrollY: Math.round(window.scrollY),
scrollHeight: document.documentElement.scrollHeight,
innerHeight: window.innerHeight,
},
consoleLogs: consoleBuffer.slice(-20),
errors: errorBuffer.slice(-10),
meta: {
hasFocus: document.hasFocus(),
visibilityState: document.visibilityState,
readyState: document.readyState,
},
};
}
// --- Send to relay ---
function sendHeartbeat() {
if (!tabId) return;
if (document.visibilityState === 'hidden') return;
const snapshot = getSnapshot();
try {
GM_xmlhttpRequest({
method: 'POST',
url: API_BASE + '/heartbeat',
headers: { 'Content-Type': 'application/json' },
data: JSON.stringify(snapshot),
timeout: 5000,
onload: () => {},
onerror: () => {},
ontimeout: () => {},
});
} catch {
// silently fail
}
}
// --- Navigation change detection (SPA support) ---
function checkNavigation() {
if (location.href !== lastUrl || document.title !== lastTitle) {
const from = lastUrl;
lastUrl = location.href;
lastTitle = document.title;
consoleBuffer.push({
level: 'info',
message: `[BrowserLogs] Navigation: ${from} → ${location.href}`,
ts: Date.now(),
});
}
}
// --- Start heartbeat loop once DOM is ready ---
function start() {
initSession();
console.log('[BrowserLogs] v1.5 active |', deviceId, '|', deviceInfo.os, deviceInfo.browser, deviceInfo.screen, '→', API_BASE);
setInterval(() => {
checkNavigation();
sendHeartbeat();
}, SEND_INTERVAL_MS);
setTimeout(sendHeartbeat, 2000);
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', start);
} else {
start();
}
// --- Menu commands ---
GM_registerMenuCommand('Browser Logs: Set API URL', () => {
const current = GM_getValue('browserLogsApi', API_BASE);
const newUrl = prompt('Browser Logs API base URL:', current);
if (newUrl) {
GM_setValue('browserLogsApi', newUrl);
location.reload();
}
});
GM_registerMenuCommand('Browser Logs: Rename Device', () => {
const current = GM_getValue('browserLogsDeviceId', deviceId);
const newName = prompt(
`Current: "${current}"\nDetected: ${deviceInfo.os}, ${deviceInfo.browser}, ${deviceInfo.screen}\n\nNew device name:`,
current
);
if (newName) {
const clean = newName.trim().toLowerCase().replace(/[^a-z0-9-]/g, '-');
GM_setValue('browserLogsDeviceId', clean);
deviceId = clean;
alert(`Device renamed to "${clean}". Will take effect on next heartbeat.`);
}
});
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment