log-inject 0.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../session.ts", "../serializer.ts", "../transport.ts", "../patch.ts", "../index.ts"],
4
+ "sourcesContent": ["// \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n// log-inject \u2014 session.ts\n// Generates and persists a unique session-id\n// via localStorage or a non-tracking cookie.\n// \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nimport { ConsolePatchConfig } from './types';\n\n/** Simple UUID-v4 that works in ES5+ without crypto.randomUUID() */\nfunction generateId(): string {\n var template = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx';\n return template.replace(/[xy]/g, function (c) {\n var r = (Math.random() * 16) | 0;\n var v = c === 'x' ? r : (r & 0x3) | 0x8;\n return v.toString(16);\n });\n}\n\n// \u2500\u2500 localStorage helpers \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nfunction lsGet(key: string): string | null {\n try {\n return typeof localStorage !== 'undefined' ? localStorage.getItem(key) : null;\n } catch (_) {\n return null;\n }\n}\n\nfunction lsSet(key: string, value: string): void {\n try {\n if (typeof localStorage !== 'undefined') {\n localStorage.setItem(key, value);\n }\n } catch (_) {\n /* quota exceeded or private mode \u2014 silently ignore */\n }\n}\n\n// \u2500\u2500 Cookie helpers \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nfunction cookieGet(name: string): string | null {\n try {\n if (typeof document === 'undefined') return null;\n var pairs = document.cookie.split(';');\n for (var i = 0; i < pairs.length; i++) {\n var pair = pairs[i].trim().split('=');\n if (pair[0] === name) {\n return decodeURIComponent(pair[1] || '');\n }\n }\n } catch (_) {\n /* no cookie access */\n }\n return null;\n}\n\nfunction cookieSet(\n name: string,\n value: string,\n maxAgeDays: number,\n sameSite: string,\n secure: boolean\n): void {\n try {\n if (typeof document === 'undefined') return;\n var maxAge = maxAgeDays * 24 * 60 * 60;\n var parts = [\n name + '=' + encodeURIComponent(value),\n 'Max-Age=' + maxAge,\n 'SameSite=' + sameSite,\n 'Path=/',\n ];\n if (secure) parts.push('Secure');\n document.cookie = parts.join('; ');\n } catch (_) {\n /* silently ignore */\n }\n}\n\n// \u2500\u2500 Public API \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n/**\n * Resolves the session-id for this browser session.\n * Creates a new one and persists it if none is found.\n */\nexport function resolveSessionId(config: ConsolePatchConfig): string {\n var key = config.sessionKey || '__cpoly_sid';\n var storageType = config.storageType || 'localStorage';\n\n var existing: string | null = null;\n\n if (storageType === 'localStorage') {\n existing = lsGet(key);\n } else if (storageType === 'cookie') {\n existing = cookieGet(key);\n }\n // 'none' \u2192 always generate ephemeral id (not persisted)\n\n if (existing && existing.length > 0) {\n return existing;\n }\n\n var id = generateId();\n\n if (storageType === 'localStorage') {\n lsSet(key, id);\n } else if (storageType === 'cookie') {\n var opts = config.cookieOptions || {};\n cookieSet(\n key,\n id,\n opts.maxAgeDays !== undefined ? opts.maxAgeDays : 365,\n opts.sameSite || 'Strict',\n opts.secure || false\n );\n }\n\n return id;\n}\n", "// \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n// log-inject \u2014 serializer.ts\n// Safely converts arbitrary JS values into\n// JSON-safe strings without throwing.\n// \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n/**\n * Converts a single console argument to a safe, readable string.\n * Circular references and non-serialisable values are handled gracefully.\n */\nexport function serializeArg(value: unknown, maxLength: number): string {\n var result: string;\n\n if (value === null) {\n result = 'null';\n } else if (value === undefined) {\n result = 'undefined';\n } else if (typeof value === 'function') {\n result = '[Function: ' + (value.name || 'anonymous') + ']';\n } else if (typeof value === 'symbol') {\n result = value.toString();\n } else if (typeof value === 'string') {\n result = value;\n } else if (typeof value === 'number' || typeof value === 'boolean') {\n result = String(value);\n } else if (value instanceof Error) {\n result = value.name + ': ' + value.message;\n if (value.stack) {\n result += '\\n' + value.stack;\n }\n } else if (\n typeof HTMLElement !== 'undefined' &&\n value instanceof HTMLElement\n ) {\n result = value.outerHTML\n ? value.outerHTML.slice(0, 200)\n : '[HTMLElement: ' + value.tagName + ']';\n } else {\n result = safeStringify(value);\n }\n\n if (result.length > maxLength) {\n result = result.slice(0, maxLength) + ' \u2026 [truncated]';\n }\n\n return result;\n}\n\n/**\n * JSON.stringify with circular-reference protection and a 5-level depth cap.\n */\nfunction safeStringify(value: unknown): string {\n var seen: unknown[] = [];\n try {\n return JSON.stringify(value, function (_key, val) {\n if (typeof val === 'object' && val !== null) {\n if (seen.indexOf(val) !== -1) {\n return '[Circular]';\n }\n seen.push(val);\n }\n if (typeof val === 'bigint') {\n return val.toString() + 'n';\n }\n if (typeof val === 'undefined') {\n return '[undefined]';\n }\n return val;\n }, 2);\n } catch (e) {\n return '[UnserializableObject]';\n }\n}\n\n/**\n * Extracts a clean stack-trace string from the current call site,\n * stripping the polyfill's own frames.\n */\nexport function captureStack(frameOffset: number): string | undefined {\n try {\n var err = new Error();\n if (!err.stack) return undefined;\n\n var lines = err.stack.split('\\n');\n // Skip frames that belong to the polyfill itself (+1 for Error frame)\n var start = frameOffset + 2;\n return lines.slice(start).join('\\n').trim() || undefined;\n } catch (_) {\n return undefined;\n }\n}\n", "// \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n// log-inject \u2014 transport.ts\n// Batched HTTP transport with exponential retry.\n// Uses fetch() with a synchronous XHR fallback.\n// \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nimport { LogEntry, ConsolePatchConfig } from './types';\n\n/**\n * Sends a batch of entries to the configured endpoint.\n * Returns a promise that resolves when the request completes.\n * Never rejects \u2014 failures are surfaced via onFlushError callback.\n */\nexport function sendBatch(\n entries: LogEntry[],\n config: ConsolePatchConfig\n): Promise<void> {\n var endpoint = config.endpoint;\n if (!endpoint) return Promise.resolve();\n\n var body = JSON.stringify({ logs: entries });\n var headers: Record<string, string> = Object.assign(\n { 'Content-Type': 'application/json' },\n config.headers || {}\n );\n\n // \u2500\u2500 Prefer fetch() \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n if (typeof fetch !== 'undefined') {\n return fetch(endpoint, {\n method: 'POST',\n headers: headers,\n body: body,\n // keepalive allows the request to outlive the page \u2014 useful for\n // capturing errors that happen just before navigation.\n keepalive: true,\n })\n .then(function (res) {\n if (!res.ok) {\n throw new Error('HTTP ' + res.status + ' from ' + endpoint);\n }\n if (config.onFlush) config.onFlush(entries);\n })\n .catch(function (err: Error) {\n if (config.onFlushError) config.onFlushError(err, entries);\n });\n }\n\n // \u2500\u2500 XHR fallback (IE 11 / very old browsers) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n return new Promise<void>(function (resolve) {\n try {\n var xhr = new XMLHttpRequest();\n xhr.open('POST', endpoint as string, true);\n for (var key in headers) {\n if (Object.prototype.hasOwnProperty.call(headers, key)) {\n xhr.setRequestHeader(key, headers[key]);\n }\n }\n xhr.onreadystatechange = function () {\n if (xhr.readyState !== 4) return;\n if (xhr.status >= 200 && xhr.status < 300) {\n if (config.onFlush) config.onFlush(entries);\n } else {\n var err = new Error('XHR ' + xhr.status + ' from ' + endpoint);\n if (config.onFlushError) config.onFlushError(err, entries);\n }\n resolve();\n };\n xhr.send(body);\n } catch (e) {\n if (config.onFlushError) {\n config.onFlushError(e instanceof Error ? e : new Error(String(e)), entries);\n }\n resolve();\n }\n });\n}\n\n/**\n * Uses sendBeacon as a last-resort flush during page unload.\n * sendBeacon doesn't support custom headers, so this is purely\n * a best-effort delivery with no auth.\n */\nexport function sendBeaconFallback(\n entries: LogEntry[],\n endpoint: string\n): boolean {\n if (typeof navigator === 'undefined' || !navigator.sendBeacon) return false;\n try {\n var blob = new Blob([JSON.stringify({ logs: entries })], {\n type: 'application/json',\n });\n return navigator.sendBeacon(endpoint, blob);\n } catch (_) {\n return false;\n }\n}\n", "// \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n// log-inject \u2014 patch.ts\n// Core interception layer.\n// Wraps every console method, builds LogEntry\n// objects, queues them, and flushes to backend.\n// \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nimport {\n ConsolePatchConfig,\n ConsoleMethod,\n LogEntry,\n LogLevel,\n PatchState,\n} from './types';\nimport { resolveSessionId } from './session';\nimport { serializeArg, captureStack } from './serializer';\nimport { sendBatch, sendBeaconFallback } from './transport';\n\n// \u2500\u2500 Constants \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n/** All console methods we know about, in declaration order */\nconst ALL_METHODS: ConsoleMethod[] = [\n 'assert', 'clear', 'count', 'countReset', 'debug', 'dir', 'dirxml',\n 'error', 'group', 'groupCollapsed', 'groupEnd', 'info', 'log',\n 'profile', 'profileEnd', 'table', 'time', 'timeEnd', 'timeLog',\n 'timeStamp', 'trace', 'warn',\n];\n\n/** Maps a console method name \u2192 normalised LogLevel */\nconst METHOD_TO_LEVEL: Record<ConsoleMethod, LogLevel> = {\n assert: 'error',\n clear: 'log',\n count: 'log',\n countReset: 'log',\n debug: 'debug',\n dir: 'log',\n dirxml: 'log',\n error: 'error',\n group: 'log',\n groupCollapsed: 'log',\n groupEnd: 'log',\n info: 'info',\n log: 'log',\n profile: 'log',\n profileEnd: 'log',\n table: 'log',\n time: 'log',\n timeEnd: 'log',\n timeLog: 'log',\n timeStamp: 'log',\n trace: 'log',\n warn: 'warn',\n};\n\n// \u2500\u2500 ID generator \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nlet _idCounter = 0;\nfunction nextId(): string {\n _idCounter += 1;\n return Date.now() + '-' + _idCounter;\n}\n\n// \u2500\u2500 Singleton state \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nconst state: PatchState = {\n installed: false,\n sessionId: '',\n queue: [],\n flushTimer: null,\n counters: {},\n timers: {},\n groupDepth: 0,\n originalMethods: {},\n};\n\n// \u2500\u2500 Per-method interception logic \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n/**\n * Handles method-specific bookkeeping (timers, counters, group depth, assert)\n * and returns an optional partial LogEntry override.\n */\nfunction handleMethodSemantics(\n method: ConsoleMethod,\n args: unknown[]\n): Partial<LogEntry> {\n switch (method) {\n\n case 'assert': {\n // assert(condition, ...data)\n var condition = !!args[0];\n return { assertionPassed: condition };\n }\n\n case 'count': {\n var countLabel = String(args[0] !== undefined ? args[0] : 'default');\n state.counters[countLabel] = (state.counters[countLabel] || 0) + 1;\n return { counterValue: state.counters[countLabel] };\n }\n\n case 'countReset': {\n var resetLabel = String(args[0] !== undefined ? args[0] : 'default');\n state.counters[resetLabel] = 0;\n return { counterValue: 0 };\n }\n\n case 'time': {\n var timeLabel = String(args[0] !== undefined ? args[0] : 'default');\n state.timers[timeLabel] = Date.now();\n return { timerLabel: timeLabel };\n }\n\n case 'timeLog':\n case 'timeEnd': {\n var teLabel = String(args[0] !== undefined ? args[0] : 'default');\n var start = state.timers[teLabel];\n var elapsed = start !== undefined ? Date.now() - start : -1;\n if (method === 'timeEnd') {\n delete state.timers[teLabel];\n }\n return { timerLabel: teLabel, timerElapsed: elapsed };\n }\n\n case 'timeStamp': {\n var tsLabel = String(args[0] !== undefined ? args[0] : '');\n return { timerLabel: tsLabel };\n }\n\n case 'group':\n case 'groupCollapsed': {\n var depth = state.groupDepth;\n state.groupDepth += 1;\n return { groupDepth: depth };\n }\n\n case 'groupEnd': {\n if (state.groupDepth > 0) state.groupDepth -= 1;\n return { groupDepth: state.groupDepth };\n }\n\n default:\n return {};\n }\n}\n\n// \u2500\u2500 Core interception \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nfunction createWrapper(\n method: ConsoleMethod,\n config: ConsolePatchConfig\n): (...args: unknown[]) => void {\n var maxLen = config.maxArgLength !== undefined ? config.maxArgLength : 2000;\n var passthrough = config.passthrough !== false;\n\n return function (...args: unknown[]): void {\n // 1. Passthrough to native console so DevTools still work\n if (passthrough) {\n var native = state.originalMethods[method];\n if (typeof native === 'function') {\n try { native.apply(console, args); } catch (_) { /* ignore */ }\n }\n }\n\n // 2. For assert, skip capture if assertion passed (matches native behaviour)\n if (method === 'assert' && args[0]) return;\n\n // 3. Build the entry\n var semantics = handleMethodSemantics(method, args);\n var now = Date.now();\n var serialisedArgs = args.map(function (a) { return serializeArg(a, maxLen); });\n\n var entry: LogEntry = Object.assign(\n {\n id: nextId(),\n method: method,\n level: METHOD_TO_LEVEL[method],\n timestamp: new Date(now).toISOString(),\n timestampMs: now,\n url: typeof location !== 'undefined' ? location.href : '',\n userAgent: typeof navigator !== 'undefined' ? navigator.userAgent : '',\n sessionId: state.sessionId,\n args: serialisedArgs,\n stack: (method === 'trace' || method === 'error')\n ? captureStack(1)\n : undefined,\n groupDepth: state.groupDepth,\n },\n semantics\n );\n\n // 4. Enqueue\n state.queue.push(entry);\n\n // 5. Burst protection\n var maxQueue = config.maxQueueSize !== undefined ? config.maxQueueSize : 50;\n if (state.queue.length >= maxQueue) {\n flushNow(config);\n }\n };\n}\n\n// \u2500\u2500 Flush \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nfunction flushNow(config: ConsolePatchConfig): void {\n if (state.queue.length === 0) return;\n var batch = state.queue.splice(0);\n sendBatch(batch, config);\n}\n\nfunction scheduleFlush(config: ConsolePatchConfig): void {\n if (state.flushTimer !== null) return;\n var interval = config.flushInterval !== undefined ? config.flushInterval : 2000;\n state.flushTimer = setTimeout(function () {\n state.flushTimer = null;\n flushNow(config);\n scheduleFlush(config);\n }, interval);\n}\n\n// \u2500\u2500 Page-unload beacon \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nfunction installUnloadFlush(config: ConsolePatchConfig): void {\n var endpoint = config.endpoint;\n if (!endpoint) return;\n\n var handler = function () {\n if (state.queue.length === 0) return;\n var batch = state.queue.splice(0);\n // Try beacon first; fall back to synchronous XHR\n if (!sendBeaconFallback(batch, endpoint as string)) {\n // Best-effort synchronous XHR (deprecated but still works)\n try {\n var xhr = new XMLHttpRequest();\n xhr.open('POST', endpoint as string, false /* sync */);\n xhr.setRequestHeader('Content-Type', 'application/json');\n xhr.send(JSON.stringify({ logs: batch }));\n } catch (_) { /* nothing we can do */ }\n }\n };\n\n if (typeof addEventListener !== 'undefined') {\n addEventListener('visibilitychange', function () {\n if (document.visibilityState === 'hidden') handler();\n });\n addEventListener('pagehide', handler);\n // Older browsers\n addEventListener('beforeunload', handler);\n }\n}\n\n// \u2500\u2500 Public API \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n/**\n * Install the console polyfill / interceptor.\n *\n * Safe to call multiple times \u2014 subsequent calls are no-ops.\n */\nexport function install(userConfig?: Partial<ConsolePatchConfig>): void {\n if (state.installed) return;\n\n var config: ConsolePatchConfig = Object.assign(\n {\n endpoint: '/api/console-logs',\n methods: ALL_METHODS,\n passthrough: true,\n flushInterval: 2000,\n maxQueueSize: 50,\n sessionKey: '__cpoly_sid',\n storageType: 'localStorage',\n maxArgLength: 2000,\n },\n userConfig || {}\n );\n\n // Resolve session-id\n state.sessionId = resolveSessionId(config);\n\n var methodsToWrap = config.methods || ALL_METHODS;\n\n for (var i = 0; i < methodsToWrap.length; i++) {\n var method = methodsToWrap[i];\n var original = (console as unknown as Record<string, unknown>)[method];\n\n // Backward compatibility \u2014 only wrap if the method actually exists\n // OR install a no-op polyfill so callers don't get errors.\n if (typeof original === 'function') {\n state.originalMethods[method] = original as (...args: unknown[]) => void;\n } else {\n // Polyfill missing method with a no-op that still captures\n state.originalMethods[method] = function () { /* noop */ };\n }\n\n (console as unknown as Record<string, unknown>)[method] = createWrapper(method, config);\n }\n\n scheduleFlush(config);\n installUnloadFlush(config);\n\n state.installed = true;\n}\n\n/**\n * Remove all patches and restore the original console methods.\n * Flushes any pending entries first.\n */\nexport function uninstall(config?: Partial<ConsolePatchConfig>): void {\n if (!state.installed) return;\n\n // Final flush\n if (config) flushNow(config as ConsolePatchConfig);\n\n for (var method in state.originalMethods) {\n if (Object.prototype.hasOwnProperty.call(state.originalMethods, method)) {\n var orig = state.originalMethods[method as ConsoleMethod];\n if (orig !== undefined) {\n (console as unknown as Record<string, unknown>)[method] = orig;\n }\n }\n }\n\n state.originalMethods = {};\n state.installed = false;\n\n if (state.flushTimer !== null) {\n clearTimeout(state.flushTimer);\n state.flushTimer = null;\n }\n}\n\n/**\n * Force an immediate flush of all queued entries.\n * Useful for testing or before navigating away manually.\n */\nexport function flush(config: Partial<ConsolePatchConfig>): void {\n flushNow(config as ConsolePatchConfig);\n}\n\n/** Returns a snapshot of the current in-memory queue (non-destructive). */\nexport function getQueue(): LogEntry[] {\n return state.queue.slice();\n}\n\n/** Returns true if the polyfill is currently active. */\nexport function isInstalled(): boolean {\n return state.installed;\n}\n", "// \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n// log-inject \u2014 index.ts\n// Public barrel \u2014 the only file that consumers\n// need to import or inject into a <script> tag.\n// \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nexport type {\n ConsolePatchConfig,\n ConsoleMethod,\n LogEntry,\n LogLevel,\n PatchState,\n} from './types';\n\nexport {\n install,\n uninstall,\n flush,\n getQueue,\n isInstalled,\n} from './patch';\n\n// \u2500\u2500 Auto-install via data attributes \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n//\n// When the compiled bundle is injected as a <script> tag with a\n// data-endpoint attribute, the polyfill auto-installs so consumers don't\n// need to write any JS themselves:\n//\n// <script\n// src=\"/log-inject.js\"\n// data-endpoint=\"/api/console-logs\"\n// data-methods=\"error,warn,log\"\n// data-storage=\"localStorage\"\n// data-flush-interval=\"3000\"\n// ></script>\n//\n\nimport { install as _install } from './patch';\nimport { ConsolePatchConfig } from './types';\n\n(function autoInstall() {\n if (typeof document === 'undefined') return;\n\n // Find our own <script> tag \u2014 the last one with data-endpoint defined\n var scripts = document.querySelectorAll('script[data-endpoint]');\n if (!scripts || scripts.length === 0) return;\n\n var tag = scripts[scripts.length - 1] as HTMLElement;\n var endpoint = tag.getAttribute('data-endpoint');\n if (!endpoint) return;\n\n var rawMethods = tag.getAttribute('data-methods');\n var storage = tag.getAttribute('data-storage') as ConsolePatchConfig['storageType'];\n var flushRaw = tag.getAttribute('data-flush-interval');\n var maxQueueRaw = tag.getAttribute('data-max-queue');\n var maxArgRaw = tag.getAttribute('data-max-arg-length');\n\n var config: Partial<ConsolePatchConfig> = { endpoint: endpoint };\n\n if (rawMethods) {\n config.methods = rawMethods.split(',').map(function (m) {\n return m.trim() as import('./types').ConsoleMethod;\n });\n }\n if (storage) config.storageType = storage;\n if (flushRaw) config.flushInterval = parseInt(flushRaw, 10);\n if (maxQueueRaw) config.maxQueueSize = parseInt(maxQueueRaw, 10);\n if (maxArgRaw) config.maxArgLength = parseInt(maxArgRaw, 10);\n\n _install(config);\n})();\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }
@@ -0,0 +1,4 @@
1
+ "use strict";(()=>{function w(){var t="xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx";return t.replace(/[xy]/g,function(e){var n=Math.random()*16|0,r=e==="x"?n:n&3|8;return r.toString(16)})}function P(t){try{return typeof localStorage!="undefined"?localStorage.getItem(t):null}catch(e){return null}}function b(t,e){try{typeof localStorage!="undefined"&&localStorage.setItem(t,e)}catch(n){}}function L(t){try{if(typeof document=="undefined")return null;for(var e=document.cookie.split(";"),n=0;n<e.length;n++){var r=e[n].trim().split("=");if(r[0]===t)return decodeURIComponent(r[1]||"")}}catch(i){}return null}function k(t,e,n,r,i){try{if(typeof document=="undefined")return;var a=n*24*60*60,s=[t+"="+encodeURIComponent(e),"Max-Age="+a,"SameSite="+r,"Path=/"];i&&s.push("Secure"),document.cookie=s.join("; ")}catch(u){}}function h(t){var e=t.sessionKey||"__cpoly_sid",n=t.storageType||"localStorage",r=null;if(n==="localStorage"?r=P(e):n==="cookie"&&(r=L(e)),r&&r.length>0)return r;var i=w();if(n==="localStorage")b(e,i);else if(n==="cookie"){var a=t.cookieOptions||{};k(e,i,a.maxAgeDays!==void 0?a.maxAgeDays:365,a.sameSite||"Strict",a.secure||!1)}return i}function m(t,e){var n;return t===null?n="null":t===void 0?n="undefined":typeof t=="function"?n="[Function: "+(t.name||"anonymous")+"]":typeof t=="symbol"?n=t.toString():typeof t=="string"?n=t:typeof t=="number"||typeof t=="boolean"?n=String(t):t instanceof Error?(n=t.name+": "+t.message,t.stack&&(n+=`
2
+ `+t.stack)):typeof HTMLElement!="undefined"&&t instanceof HTMLElement?n=t.outerHTML?t.outerHTML.slice(0,200):"[HTMLElement: "+t.tagName+"]":n=M(t),n.length>e&&(n=n.slice(0,e)+" \u2026 [truncated]"),n}function M(t){var e=[];try{return JSON.stringify(t,function(n,r){if(typeof r=="object"&&r!==null){if(e.indexOf(r)!==-1)return"[Circular]";e.push(r)}return typeof r=="bigint"?r.toString()+"n":typeof r=="undefined"?"[undefined]":r},2)}catch(n){return"[UnserializableObject]"}}function y(t){try{var e=new Error;if(!e.stack)return;var n=e.stack.split(`
3
+ `),r=t+2;return n.slice(r).join(`
4
+ `).trim()||void 0}catch(i){return}}function v(t,e){var n=e.endpoint;if(!n)return Promise.resolve();var r=JSON.stringify({logs:t}),i=Object.assign({"Content-Type":"application/json"},e.headers||{});return typeof fetch!="undefined"?fetch(n,{method:"POST",headers:i,body:r,keepalive:!0}).then(function(a){if(!a.ok)throw new Error("HTTP "+a.status+" from "+n);e.onFlush&&e.onFlush(t)}).catch(function(a){e.onFlushError&&e.onFlushError(a,t)}):new Promise(function(a){try{var s=new XMLHttpRequest;s.open("POST",n,!0);for(var u in i)Object.prototype.hasOwnProperty.call(i,u)&&s.setRequestHeader(u,i[u]);s.onreadystatechange=function(){if(s.readyState===4){if(s.status>=200&&s.status<300)e.onFlush&&e.onFlush(t);else{var l=new Error("XHR "+s.status+" from "+n);e.onFlushError&&e.onFlushError(l,t)}a()}},s.send(r)}catch(l){e.onFlushError&&e.onFlushError(l instanceof Error?l:new Error(String(l)),t),a()}})}function x(t,e){if(typeof navigator=="undefined"||!navigator.sendBeacon)return!1;try{var n=new Blob([JSON.stringify({logs:t})],{type:"application/json"});return navigator.sendBeacon(e,n)}catch(r){return!1}}var C=["assert","clear","count","countReset","debug","dir","dirxml","error","group","groupCollapsed","groupEnd","info","log","profile","profileEnd","table","time","timeEnd","timeLog","timeStamp","trace","warn"],T={assert:"error",clear:"log",count:"log",countReset:"log",debug:"debug",dir:"log",dirxml:"log",error:"error",group:"log",groupCollapsed:"log",groupEnd:"log",info:"info",log:"log",profile:"log",profileEnd:"log",table:"log",time:"log",timeEnd:"log",timeLog:"log",timeStamp:"log",trace:"log",warn:"warn"},S=0;function I(){return S+=1,Date.now()+"-"+S}var o={installed:!1,sessionId:"",queue:[],flushTimer:null,counters:{},timers:{},groupDepth:0,originalMethods:{}};function A(t,e){switch(t){case"assert":{var n=!!e[0];return{assertionPassed:n}}case"count":{var r=String(e[0]!==void 0?e[0]:"default");return o.counters[r]=(o.counters[r]||0)+1,{counterValue:o.counters[r]}}case"countReset":{var i=String(e[0]!==void 0?e[0]:"default");return o.counters[i]=0,{counterValue:0}}case"time":{var a=String(e[0]!==void 0?e[0]:"default");return o.timers[a]=Date.now(),{timerLabel:a}}case"timeLog":case"timeEnd":{var s=String(e[0]!==void 0?e[0]:"default"),u=o.timers[s],l=u!==void 0?Date.now()-u:-1;return t==="timeEnd"&&delete o.timers[s],{timerLabel:s,timerElapsed:l}}case"timeStamp":{var f=String(e[0]!==void 0?e[0]:"");return{timerLabel:f}}case"group":case"groupCollapsed":{var d=o.groupDepth;return o.groupDepth+=1,{groupDepth:d}}case"groupEnd":return o.groupDepth>0&&(o.groupDepth-=1),{groupDepth:o.groupDepth};default:return{}}}function O(t,e){var n=e.maxArgLength!==void 0?e.maxArgLength:2e3,r=e.passthrough!==!1;return function(...i){if(r){var a=o.originalMethods[t];if(typeof a=="function")try{a.apply(console,i)}catch(p){}}if(!(t==="assert"&&i[0])){var s=A(t,i),u=Date.now(),l=i.map(function(p){return m(p,n)}),f=Object.assign({id:I(),method:t,level:T[t],timestamp:new Date(u).toISOString(),timestampMs:u,url:typeof location!="undefined"?location.href:"",userAgent:typeof navigator!="undefined"?navigator.userAgent:"",sessionId:o.sessionId,args:l,stack:t==="trace"||t==="error"?y(1):void 0,groupDepth:o.groupDepth},s);o.queue.push(f);var d=e.maxQueueSize!==void 0?e.maxQueueSize:50;o.queue.length>=d&&c(e)}}}function c(t){if(o.queue.length!==0){var e=o.queue.splice(0);v(e,t)}}function E(t){if(o.flushTimer===null){var e=t.flushInterval!==void 0?t.flushInterval:2e3;o.flushTimer=setTimeout(function(){o.flushTimer=null,c(t),E(t)},e)}}function _(t){var e=t.endpoint;if(e){var n=function(){if(o.queue.length!==0){var r=o.queue.splice(0);if(!x(r,e))try{var i=new XMLHttpRequest;i.open("POST",e,!1),i.setRequestHeader("Content-Type","application/json"),i.send(JSON.stringify({logs:r}))}catch(a){}}};typeof addEventListener!="undefined"&&(addEventListener("visibilitychange",function(){document.visibilityState==="hidden"&&n()}),addEventListener("pagehide",n),addEventListener("beforeunload",n))}}function g(t){if(!o.installed){var e=Object.assign({endpoint:"/api/console-logs",methods:C,passthrough:!0,flushInterval:2e3,maxQueueSize:50,sessionKey:"__cpoly_sid",storageType:"localStorage",maxArgLength:2e3},t||{});o.sessionId=h(e);for(var n=e.methods||C,r=0;r<n.length;r++){var i=n[r],a=console[i];typeof a=="function"?o.originalMethods[i]=a:o.originalMethods[i]=function(){},console[i]=O(i,e)}E(e),_(e),o.installed=!0}}function D(t){if(o.installed){t&&c(t);for(var e in o.originalMethods)if(Object.prototype.hasOwnProperty.call(o.originalMethods,e)){var n=o.originalMethods[e];n!==void 0&&(console[e]=n)}o.originalMethods={},o.installed=!1,o.flushTimer!==null&&(clearTimeout(o.flushTimer),o.flushTimer=null)}}function R(t){c(t)}function F(){return o.queue.slice()}function q(){return o.installed}(function(){if(typeof document!="undefined"){var e=document.querySelectorAll("script[data-endpoint]");if(!(!e||e.length===0)){var n=e[e.length-1],r=n.getAttribute("data-endpoint");if(r){var i=n.getAttribute("data-methods"),a=n.getAttribute("data-storage"),s=n.getAttribute("data-flush-interval"),u=n.getAttribute("data-max-queue"),l=n.getAttribute("data-max-arg-length"),f={endpoint:r};i&&(f.methods=i.split(",").map(function(d){return d.trim()})),a&&(f.storageType=a),s&&(f.flushInterval=parseInt(s,10)),u&&(f.maxQueueSize=parseInt(u,10)),l&&(f.maxArgLength=parseInt(l,10)),g(f)}}}})();})();
package/index.ts ADDED
@@ -0,0 +1,71 @@
1
+ // ─────────────────────────────────────────────
2
+ // log-inject — index.ts
3
+ // Public barrel — the only file that consumers
4
+ // need to import or inject into a <script> tag.
5
+ // ─────────────────────────────────────────────
6
+
7
+ export type {
8
+ ConsolePatchConfig,
9
+ ConsoleMethod,
10
+ LogEntry,
11
+ LogLevel,
12
+ PatchState,
13
+ } from './types';
14
+
15
+ export {
16
+ install,
17
+ uninstall,
18
+ flush,
19
+ getQueue,
20
+ isInstalled,
21
+ } from './patch';
22
+
23
+ // ── Auto-install via data attributes ─────────────────────────────────────────
24
+ //
25
+ // When the compiled bundle is injected as a <script> tag with a
26
+ // data-endpoint attribute, the polyfill auto-installs so consumers don't
27
+ // need to write any JS themselves:
28
+ //
29
+ // <script
30
+ // src="/log-inject.js"
31
+ // data-endpoint="/api/console-logs"
32
+ // data-methods="error,warn,log"
33
+ // data-storage="localStorage"
34
+ // data-flush-interval="3000"
35
+ // ></script>
36
+ //
37
+
38
+ import { install as _install } from './patch';
39
+ import { ConsolePatchConfig } from './types';
40
+
41
+ (function autoInstall() {
42
+ if (typeof document === 'undefined') return;
43
+
44
+ // Find our own <script> tag — the last one with data-endpoint defined
45
+ var scripts = document.querySelectorAll('script[data-endpoint]');
46
+ if (!scripts || scripts.length === 0) return;
47
+
48
+ var tag = scripts[scripts.length - 1] as HTMLElement;
49
+ var endpoint = tag.getAttribute('data-endpoint');
50
+ if (!endpoint) return;
51
+
52
+ var rawMethods = tag.getAttribute('data-methods');
53
+ var storage = tag.getAttribute('data-storage') as ConsolePatchConfig['storageType'];
54
+ var flushRaw = tag.getAttribute('data-flush-interval');
55
+ var maxQueueRaw = tag.getAttribute('data-max-queue');
56
+ var maxArgRaw = tag.getAttribute('data-max-arg-length');
57
+
58
+ var config: Partial<ConsolePatchConfig> = { endpoint: endpoint };
59
+
60
+ if (rawMethods) {
61
+ config.methods = rawMethods.split(',').map(function (m) {
62
+ return m.trim() as import('./types').ConsoleMethod;
63
+ });
64
+ }
65
+ if (storage) config.storageType = storage;
66
+ if (flushRaw) config.flushInterval = parseInt(flushRaw, 10);
67
+ if (maxQueueRaw) config.maxQueueSize = parseInt(maxQueueRaw, 10);
68
+ if (maxArgRaw) config.maxArgLength = parseInt(maxArgRaw, 10);
69
+
70
+ _install(config);
71
+ })();
package/package.json ADDED
@@ -0,0 +1,34 @@
1
+ {
2
+ "name": "log-inject",
3
+ "version": "0.0.0",
4
+ "description": "Drop-in console interceptor — forwards all console.* calls to your backend while preserving native browser behaviour.",
5
+ "main": "dist/log-inject.js",
6
+ "scripts": {
7
+ "build": "tsc --noEmit && npm run bundle && npm run bundle:min",
8
+ "bundle": "esbuild index.ts --bundle --platform=browser --target=es2015 --outfile=dist/log-inject.js --sourcemap=external",
9
+ "bundle:min": "esbuild index.ts --bundle --platform=browser --target=es2015 --outfile=dist/log-inject.min.js --minify",
10
+ "typecheck": "tsc --noEmit",
11
+ "dev": "esbuild index.ts --bundle --platform=browser --target=es2015 --outfile=dist/log-inject.js --watch"
12
+ },
13
+ "keywords": [
14
+ "console",
15
+ "polyfill",
16
+ "logging",
17
+ "monitoring",
18
+ "typescript",
19
+ "log",
20
+ "intercept",
21
+ "browser",
22
+ "frontend",
23
+ "inject",
24
+ "injector",
25
+ "interceptor",
26
+ "injection"
27
+ ],
28
+ "license": "MIT",
29
+ "devDependencies": {
30
+ "@types/node": "^22.0.0",
31
+ "esbuild": "^0.25.0",
32
+ "typescript": "^5.0.0"
33
+ }
34
+ }
package/patch.ts ADDED
@@ -0,0 +1,345 @@
1
+ // ─────────────────────────────────────────────
2
+ // log-inject — patch.ts
3
+ // Core interception layer.
4
+ // Wraps every console method, builds LogEntry
5
+ // objects, queues them, and flushes to backend.
6
+ // ─────────────────────────────────────────────
7
+
8
+ import {
9
+ ConsolePatchConfig,
10
+ ConsoleMethod,
11
+ LogEntry,
12
+ LogLevel,
13
+ PatchState,
14
+ } from './types';
15
+ import { resolveSessionId } from './session';
16
+ import { serializeArg, captureStack } from './serializer';
17
+ import { sendBatch, sendBeaconFallback } from './transport';
18
+
19
+ // ── Constants ─────────────────────────────────────────────────────────────────
20
+
21
+ /** All console methods we know about, in declaration order */
22
+ const ALL_METHODS: ConsoleMethod[] = [
23
+ 'assert', 'clear', 'count', 'countReset', 'debug', 'dir', 'dirxml',
24
+ 'error', 'group', 'groupCollapsed', 'groupEnd', 'info', 'log',
25
+ 'profile', 'profileEnd', 'table', 'time', 'timeEnd', 'timeLog',
26
+ 'timeStamp', 'trace', 'warn',
27
+ ];
28
+
29
+ /** Maps a console method name → normalised LogLevel */
30
+ const METHOD_TO_LEVEL: Record<ConsoleMethod, LogLevel> = {
31
+ assert: 'error',
32
+ clear: 'log',
33
+ count: 'log',
34
+ countReset: 'log',
35
+ debug: 'debug',
36
+ dir: 'log',
37
+ dirxml: 'log',
38
+ error: 'error',
39
+ group: 'log',
40
+ groupCollapsed: 'log',
41
+ groupEnd: 'log',
42
+ info: 'info',
43
+ log: 'log',
44
+ profile: 'log',
45
+ profileEnd: 'log',
46
+ table: 'log',
47
+ time: 'log',
48
+ timeEnd: 'log',
49
+ timeLog: 'log',
50
+ timeStamp: 'log',
51
+ trace: 'log',
52
+ warn: 'warn',
53
+ };
54
+
55
+ // ── ID generator ──────────────────────────────────────────────────────────────
56
+
57
+ let _idCounter = 0;
58
+ function nextId(): string {
59
+ _idCounter += 1;
60
+ return Date.now() + '-' + _idCounter;
61
+ }
62
+
63
+ // ── Singleton state ───────────────────────────────────────────────────────────
64
+
65
+ const state: PatchState = {
66
+ installed: false,
67
+ sessionId: '',
68
+ queue: [],
69
+ flushTimer: null,
70
+ counters: {},
71
+ timers: {},
72
+ groupDepth: 0,
73
+ originalMethods: {},
74
+ };
75
+
76
+ // ── Per-method interception logic ─────────────────────────────────────────────
77
+
78
+ /**
79
+ * Handles method-specific bookkeeping (timers, counters, group depth, assert)
80
+ * and returns an optional partial LogEntry override.
81
+ */
82
+ function handleMethodSemantics(
83
+ method: ConsoleMethod,
84
+ args: unknown[]
85
+ ): Partial<LogEntry> {
86
+ switch (method) {
87
+
88
+ case 'assert': {
89
+ // assert(condition, ...data)
90
+ var condition = !!args[0];
91
+ return { assertionPassed: condition };
92
+ }
93
+
94
+ case 'count': {
95
+ var countLabel = String(args[0] !== undefined ? args[0] : 'default');
96
+ state.counters[countLabel] = (state.counters[countLabel] || 0) + 1;
97
+ return { counterValue: state.counters[countLabel] };
98
+ }
99
+
100
+ case 'countReset': {
101
+ var resetLabel = String(args[0] !== undefined ? args[0] : 'default');
102
+ state.counters[resetLabel] = 0;
103
+ return { counterValue: 0 };
104
+ }
105
+
106
+ case 'time': {
107
+ var timeLabel = String(args[0] !== undefined ? args[0] : 'default');
108
+ state.timers[timeLabel] = Date.now();
109
+ return { timerLabel: timeLabel };
110
+ }
111
+
112
+ case 'timeLog':
113
+ case 'timeEnd': {
114
+ var teLabel = String(args[0] !== undefined ? args[0] : 'default');
115
+ var start = state.timers[teLabel];
116
+ var elapsed = start !== undefined ? Date.now() - start : -1;
117
+ if (method === 'timeEnd') {
118
+ delete state.timers[teLabel];
119
+ }
120
+ return { timerLabel: teLabel, timerElapsed: elapsed };
121
+ }
122
+
123
+ case 'timeStamp': {
124
+ var tsLabel = String(args[0] !== undefined ? args[0] : '');
125
+ return { timerLabel: tsLabel };
126
+ }
127
+
128
+ case 'group':
129
+ case 'groupCollapsed': {
130
+ var depth = state.groupDepth;
131
+ state.groupDepth += 1;
132
+ return { groupDepth: depth };
133
+ }
134
+
135
+ case 'groupEnd': {
136
+ if (state.groupDepth > 0) state.groupDepth -= 1;
137
+ return { groupDepth: state.groupDepth };
138
+ }
139
+
140
+ default:
141
+ return {};
142
+ }
143
+ }
144
+
145
+ // ── Core interception ─────────────────────────────────────────────────────────
146
+
147
+ function createWrapper(
148
+ method: ConsoleMethod,
149
+ config: ConsolePatchConfig
150
+ ): (...args: unknown[]) => void {
151
+ var maxLen = config.maxArgLength !== undefined ? config.maxArgLength : 2000;
152
+ var passthrough = config.passthrough !== false;
153
+
154
+ return function (...args: unknown[]): void {
155
+ // 1. Passthrough to native console so DevTools still work
156
+ if (passthrough) {
157
+ var native = state.originalMethods[method];
158
+ if (typeof native === 'function') {
159
+ try { native.apply(console, args); } catch (_) { /* ignore */ }
160
+ }
161
+ }
162
+
163
+ // 2. For assert, skip capture if assertion passed (matches native behaviour)
164
+ if (method === 'assert' && args[0]) return;
165
+
166
+ // 3. Build the entry
167
+ var semantics = handleMethodSemantics(method, args);
168
+ var now = Date.now();
169
+ var serialisedArgs = args.map(function (a) { return serializeArg(a, maxLen); });
170
+
171
+ var entry: LogEntry = Object.assign(
172
+ {
173
+ id: nextId(),
174
+ method: method,
175
+ level: METHOD_TO_LEVEL[method],
176
+ timestamp: new Date(now).toISOString(),
177
+ timestampMs: now,
178
+ url: typeof location !== 'undefined' ? location.href : '',
179
+ userAgent: typeof navigator !== 'undefined' ? navigator.userAgent : '',
180
+ sessionId: state.sessionId,
181
+ args: serialisedArgs,
182
+ stack: (method === 'trace' || method === 'error')
183
+ ? captureStack(1)
184
+ : undefined,
185
+ groupDepth: state.groupDepth,
186
+ },
187
+ semantics
188
+ );
189
+
190
+ // 4. Enqueue
191
+ state.queue.push(entry);
192
+
193
+ // 5. Burst protection
194
+ var maxQueue = config.maxQueueSize !== undefined ? config.maxQueueSize : 50;
195
+ if (state.queue.length >= maxQueue) {
196
+ flushNow(config);
197
+ }
198
+ };
199
+ }
200
+
201
+ // ── Flush ─────────────────────────────────────────────────────────────────────
202
+
203
+ function flushNow(config: ConsolePatchConfig): void {
204
+ if (state.queue.length === 0) return;
205
+ var batch = state.queue.splice(0);
206
+ sendBatch(batch, config);
207
+ }
208
+
209
+ function scheduleFlush(config: ConsolePatchConfig): void {
210
+ if (state.flushTimer !== null) return;
211
+ var interval = config.flushInterval !== undefined ? config.flushInterval : 2000;
212
+ state.flushTimer = setTimeout(function () {
213
+ state.flushTimer = null;
214
+ flushNow(config);
215
+ scheduleFlush(config);
216
+ }, interval);
217
+ }
218
+
219
+ // ── Page-unload beacon ────────────────────────────────────────────────────────
220
+
221
+ function installUnloadFlush(config: ConsolePatchConfig): void {
222
+ var endpoint = config.endpoint;
223
+ if (!endpoint) return;
224
+
225
+ var handler = function () {
226
+ if (state.queue.length === 0) return;
227
+ var batch = state.queue.splice(0);
228
+ // Try beacon first; fall back to synchronous XHR
229
+ if (!sendBeaconFallback(batch, endpoint as string)) {
230
+ // Best-effort synchronous XHR (deprecated but still works)
231
+ try {
232
+ var xhr = new XMLHttpRequest();
233
+ xhr.open('POST', endpoint as string, false /* sync */);
234
+ xhr.setRequestHeader('Content-Type', 'application/json');
235
+ xhr.send(JSON.stringify({ logs: batch }));
236
+ } catch (_) { /* nothing we can do */ }
237
+ }
238
+ };
239
+
240
+ if (typeof addEventListener !== 'undefined') {
241
+ addEventListener('visibilitychange', function () {
242
+ if (document.visibilityState === 'hidden') handler();
243
+ });
244
+ addEventListener('pagehide', handler);
245
+ // Older browsers
246
+ addEventListener('beforeunload', handler);
247
+ }
248
+ }
249
+
250
+ // ── Public API ────────────────────────────────────────────────────────────────
251
+
252
+ /**
253
+ * Install the console polyfill / interceptor.
254
+ *
255
+ * Safe to call multiple times — subsequent calls are no-ops.
256
+ */
257
+ export function install(userConfig?: Partial<ConsolePatchConfig>): void {
258
+ if (state.installed) return;
259
+
260
+ var config: ConsolePatchConfig = Object.assign(
261
+ {
262
+ endpoint: '/api/console-logs',
263
+ methods: ALL_METHODS,
264
+ passthrough: true,
265
+ flushInterval: 2000,
266
+ maxQueueSize: 50,
267
+ sessionKey: '__cpoly_sid',
268
+ storageType: 'localStorage',
269
+ maxArgLength: 2000,
270
+ },
271
+ userConfig || {}
272
+ );
273
+
274
+ // Resolve session-id
275
+ state.sessionId = resolveSessionId(config);
276
+
277
+ var methodsToWrap = config.methods || ALL_METHODS;
278
+
279
+ for (var i = 0; i < methodsToWrap.length; i++) {
280
+ var method = methodsToWrap[i];
281
+ var original = (console as unknown as Record<string, unknown>)[method];
282
+
283
+ // Backward compatibility — only wrap if the method actually exists
284
+ // OR install a no-op polyfill so callers don't get errors.
285
+ if (typeof original === 'function') {
286
+ state.originalMethods[method] = original as (...args: unknown[]) => void;
287
+ } else {
288
+ // Polyfill missing method with a no-op that still captures
289
+ state.originalMethods[method] = function () { /* noop */ };
290
+ }
291
+
292
+ (console as unknown as Record<string, unknown>)[method] = createWrapper(method, config);
293
+ }
294
+
295
+ scheduleFlush(config);
296
+ installUnloadFlush(config);
297
+
298
+ state.installed = true;
299
+ }
300
+
301
+ /**
302
+ * Remove all patches and restore the original console methods.
303
+ * Flushes any pending entries first.
304
+ */
305
+ export function uninstall(config?: Partial<ConsolePatchConfig>): void {
306
+ if (!state.installed) return;
307
+
308
+ // Final flush
309
+ if (config) flushNow(config as ConsolePatchConfig);
310
+
311
+ for (var method in state.originalMethods) {
312
+ if (Object.prototype.hasOwnProperty.call(state.originalMethods, method)) {
313
+ var orig = state.originalMethods[method as ConsoleMethod];
314
+ if (orig !== undefined) {
315
+ (console as unknown as Record<string, unknown>)[method] = orig;
316
+ }
317
+ }
318
+ }
319
+
320
+ state.originalMethods = {};
321
+ state.installed = false;
322
+
323
+ if (state.flushTimer !== null) {
324
+ clearTimeout(state.flushTimer);
325
+ state.flushTimer = null;
326
+ }
327
+ }
328
+
329
+ /**
330
+ * Force an immediate flush of all queued entries.
331
+ * Useful for testing or before navigating away manually.
332
+ */
333
+ export function flush(config: Partial<ConsolePatchConfig>): void {
334
+ flushNow(config as ConsolePatchConfig);
335
+ }
336
+
337
+ /** Returns a snapshot of the current in-memory queue (non-destructive). */
338
+ export function getQueue(): LogEntry[] {
339
+ return state.queue.slice();
340
+ }
341
+
342
+ /** Returns true if the polyfill is currently active. */
343
+ export function isInstalled(): boolean {
344
+ return state.installed;
345
+ }
package/serializer.ts ADDED
@@ -0,0 +1,91 @@
1
+ // ─────────────────────────────────────────────
2
+ // log-inject — serializer.ts
3
+ // Safely converts arbitrary JS values into
4
+ // JSON-safe strings without throwing.
5
+ // ─────────────────────────────────────────────
6
+
7
+ /**
8
+ * Converts a single console argument to a safe, readable string.
9
+ * Circular references and non-serialisable values are handled gracefully.
10
+ */
11
+ export function serializeArg(value: unknown, maxLength: number): string {
12
+ var result: string;
13
+
14
+ if (value === null) {
15
+ result = 'null';
16
+ } else if (value === undefined) {
17
+ result = 'undefined';
18
+ } else if (typeof value === 'function') {
19
+ result = '[Function: ' + (value.name || 'anonymous') + ']';
20
+ } else if (typeof value === 'symbol') {
21
+ result = value.toString();
22
+ } else if (typeof value === 'string') {
23
+ result = value;
24
+ } else if (typeof value === 'number' || typeof value === 'boolean') {
25
+ result = String(value);
26
+ } else if (value instanceof Error) {
27
+ result = value.name + ': ' + value.message;
28
+ if (value.stack) {
29
+ result += '\n' + value.stack;
30
+ }
31
+ } else if (
32
+ typeof HTMLElement !== 'undefined' &&
33
+ value instanceof HTMLElement
34
+ ) {
35
+ result = value.outerHTML
36
+ ? value.outerHTML.slice(0, 200)
37
+ : '[HTMLElement: ' + value.tagName + ']';
38
+ } else {
39
+ result = safeStringify(value);
40
+ }
41
+
42
+ if (result.length > maxLength) {
43
+ result = result.slice(0, maxLength) + ' … [truncated]';
44
+ }
45
+
46
+ return result;
47
+ }
48
+
49
+ /**
50
+ * JSON.stringify with circular-reference protection and a 5-level depth cap.
51
+ */
52
+ function safeStringify(value: unknown): string {
53
+ var seen: unknown[] = [];
54
+ try {
55
+ return JSON.stringify(value, function (_key, val) {
56
+ if (typeof val === 'object' && val !== null) {
57
+ if (seen.indexOf(val) !== -1) {
58
+ return '[Circular]';
59
+ }
60
+ seen.push(val);
61
+ }
62
+ if (typeof val === 'bigint') {
63
+ return val.toString() + 'n';
64
+ }
65
+ if (typeof val === 'undefined') {
66
+ return '[undefined]';
67
+ }
68
+ return val;
69
+ }, 2);
70
+ } catch (e) {
71
+ return '[UnserializableObject]';
72
+ }
73
+ }
74
+
75
+ /**
76
+ * Extracts a clean stack-trace string from the current call site,
77
+ * stripping the polyfill's own frames.
78
+ */
79
+ export function captureStack(frameOffset: number): string | undefined {
80
+ try {
81
+ var err = new Error();
82
+ if (!err.stack) return undefined;
83
+
84
+ var lines = err.stack.split('\n');
85
+ // Skip frames that belong to the polyfill itself (+1 for Error frame)
86
+ var start = frameOffset + 2;
87
+ return lines.slice(start).join('\n').trim() || undefined;
88
+ } catch (_) {
89
+ return undefined;
90
+ }
91
+ }