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.
package/session.ts ADDED
@@ -0,0 +1,119 @@
1
+ // ─────────────────────────────────────────────
2
+ // log-inject — session.ts
3
+ // Generates and persists a unique session-id
4
+ // via localStorage or a non-tracking cookie.
5
+ // ─────────────────────────────────────────────
6
+
7
+ import { ConsolePatchConfig } from './types';
8
+
9
+ /** Simple UUID-v4 that works in ES5+ without crypto.randomUUID() */
10
+ function generateId(): string {
11
+ var template = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx';
12
+ return template.replace(/[xy]/g, function (c) {
13
+ var r = (Math.random() * 16) | 0;
14
+ var v = c === 'x' ? r : (r & 0x3) | 0x8;
15
+ return v.toString(16);
16
+ });
17
+ }
18
+
19
+ // ── localStorage helpers ──────────────────────────────────────────────────────
20
+
21
+ function lsGet(key: string): string | null {
22
+ try {
23
+ return typeof localStorage !== 'undefined' ? localStorage.getItem(key) : null;
24
+ } catch (_) {
25
+ return null;
26
+ }
27
+ }
28
+
29
+ function lsSet(key: string, value: string): void {
30
+ try {
31
+ if (typeof localStorage !== 'undefined') {
32
+ localStorage.setItem(key, value);
33
+ }
34
+ } catch (_) {
35
+ /* quota exceeded or private mode — silently ignore */
36
+ }
37
+ }
38
+
39
+ // ── Cookie helpers ────────────────────────────────────────────────────────────
40
+
41
+ function cookieGet(name: string): string | null {
42
+ try {
43
+ if (typeof document === 'undefined') return null;
44
+ var pairs = document.cookie.split(';');
45
+ for (var i = 0; i < pairs.length; i++) {
46
+ var pair = pairs[i].trim().split('=');
47
+ if (pair[0] === name) {
48
+ return decodeURIComponent(pair[1] || '');
49
+ }
50
+ }
51
+ } catch (_) {
52
+ /* no cookie access */
53
+ }
54
+ return null;
55
+ }
56
+
57
+ function cookieSet(
58
+ name: string,
59
+ value: string,
60
+ maxAgeDays: number,
61
+ sameSite: string,
62
+ secure: boolean
63
+ ): void {
64
+ try {
65
+ if (typeof document === 'undefined') return;
66
+ var maxAge = maxAgeDays * 24 * 60 * 60;
67
+ var parts = [
68
+ name + '=' + encodeURIComponent(value),
69
+ 'Max-Age=' + maxAge,
70
+ 'SameSite=' + sameSite,
71
+ 'Path=/',
72
+ ];
73
+ if (secure) parts.push('Secure');
74
+ document.cookie = parts.join('; ');
75
+ } catch (_) {
76
+ /* silently ignore */
77
+ }
78
+ }
79
+
80
+ // ── Public API ────────────────────────────────────────────────────────────────
81
+
82
+ /**
83
+ * Resolves the session-id for this browser session.
84
+ * Creates a new one and persists it if none is found.
85
+ */
86
+ export function resolveSessionId(config: ConsolePatchConfig): string {
87
+ var key = config.sessionKey || '__cpoly_sid';
88
+ var storageType = config.storageType || 'localStorage';
89
+
90
+ var existing: string | null = null;
91
+
92
+ if (storageType === 'localStorage') {
93
+ existing = lsGet(key);
94
+ } else if (storageType === 'cookie') {
95
+ existing = cookieGet(key);
96
+ }
97
+ // 'none' → always generate ephemeral id (not persisted)
98
+
99
+ if (existing && existing.length > 0) {
100
+ return existing;
101
+ }
102
+
103
+ var id = generateId();
104
+
105
+ if (storageType === 'localStorage') {
106
+ lsSet(key, id);
107
+ } else if (storageType === 'cookie') {
108
+ var opts = config.cookieOptions || {};
109
+ cookieSet(
110
+ key,
111
+ id,
112
+ opts.maxAgeDays !== undefined ? opts.maxAgeDays : 365,
113
+ opts.sameSite || 'Strict',
114
+ opts.secure || false
115
+ );
116
+ }
117
+
118
+ return id;
119
+ }
package/transport.ts ADDED
@@ -0,0 +1,96 @@
1
+ // ─────────────────────────────────────────────
2
+ // log-inject — transport.ts
3
+ // Batched HTTP transport with exponential retry.
4
+ // Uses fetch() with a synchronous XHR fallback.
5
+ // ─────────────────────────────────────────────
6
+
7
+ import { LogEntry, ConsolePatchConfig } from './types';
8
+
9
+ /**
10
+ * Sends a batch of entries to the configured endpoint.
11
+ * Returns a promise that resolves when the request completes.
12
+ * Never rejects — failures are surfaced via onFlushError callback.
13
+ */
14
+ export function sendBatch(
15
+ entries: LogEntry[],
16
+ config: ConsolePatchConfig
17
+ ): Promise<void> {
18
+ var endpoint = config.endpoint;
19
+ if (!endpoint) return Promise.resolve();
20
+
21
+ var body = JSON.stringify({ logs: entries });
22
+ var headers: Record<string, string> = Object.assign(
23
+ { 'Content-Type': 'application/json' },
24
+ config.headers || {}
25
+ );
26
+
27
+ // ── Prefer fetch() ────────────────────────────────────────────────────────
28
+ if (typeof fetch !== 'undefined') {
29
+ return fetch(endpoint, {
30
+ method: 'POST',
31
+ headers: headers,
32
+ body: body,
33
+ // keepalive allows the request to outlive the page — useful for
34
+ // capturing errors that happen just before navigation.
35
+ keepalive: true,
36
+ })
37
+ .then(function (res) {
38
+ if (!res.ok) {
39
+ throw new Error('HTTP ' + res.status + ' from ' + endpoint);
40
+ }
41
+ if (config.onFlush) config.onFlush(entries);
42
+ })
43
+ .catch(function (err: Error) {
44
+ if (config.onFlushError) config.onFlushError(err, entries);
45
+ });
46
+ }
47
+
48
+ // ── XHR fallback (IE 11 / very old browsers) ──────────────────────────────
49
+ return new Promise<void>(function (resolve) {
50
+ try {
51
+ var xhr = new XMLHttpRequest();
52
+ xhr.open('POST', endpoint as string, true);
53
+ for (var key in headers) {
54
+ if (Object.prototype.hasOwnProperty.call(headers, key)) {
55
+ xhr.setRequestHeader(key, headers[key]);
56
+ }
57
+ }
58
+ xhr.onreadystatechange = function () {
59
+ if (xhr.readyState !== 4) return;
60
+ if (xhr.status >= 200 && xhr.status < 300) {
61
+ if (config.onFlush) config.onFlush(entries);
62
+ } else {
63
+ var err = new Error('XHR ' + xhr.status + ' from ' + endpoint);
64
+ if (config.onFlushError) config.onFlushError(err, entries);
65
+ }
66
+ resolve();
67
+ };
68
+ xhr.send(body);
69
+ } catch (e) {
70
+ if (config.onFlushError) {
71
+ config.onFlushError(e instanceof Error ? e : new Error(String(e)), entries);
72
+ }
73
+ resolve();
74
+ }
75
+ });
76
+ }
77
+
78
+ /**
79
+ * Uses sendBeacon as a last-resort flush during page unload.
80
+ * sendBeacon doesn't support custom headers, so this is purely
81
+ * a best-effort delivery with no auth.
82
+ */
83
+ export function sendBeaconFallback(
84
+ entries: LogEntry[],
85
+ endpoint: string
86
+ ): boolean {
87
+ if (typeof navigator === 'undefined' || !navigator.sendBeacon) return false;
88
+ try {
89
+ var blob = new Blob([JSON.stringify({ logs: entries })], {
90
+ type: 'application/json',
91
+ });
92
+ return navigator.sendBeacon(endpoint, blob);
93
+ } catch (_) {
94
+ return false;
95
+ }
96
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,30 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES5",
4
+ "lib": [
5
+ "ES5",
6
+ "ES6",
7
+ "DOM"
8
+ ],
9
+ "module": "CommonJS",
10
+ "outDir": "./dist",
11
+ "rootDir": "./",
12
+ "strict": true,
13
+ "noImplicitAny": true,
14
+ "strictNullChecks": true,
15
+ "declaration": true,
16
+ "declarationMap": true,
17
+ "sourceMap": true,
18
+ "removeComments": false,
19
+ "forceConsistentCasingInFileNames": true,
20
+ "esModuleInterop": true,
21
+ "skipLibCheck": true
22
+ },
23
+ "include": [
24
+ "**/*.ts"
25
+ ],
26
+ "exclude": [
27
+ "node_modules",
28
+ "dist"
29
+ ]
30
+ }
package/types.ts ADDED
@@ -0,0 +1,169 @@
1
+ // ─────────────────────────────────────────────
2
+ // log-inject — types.ts
3
+ // All shared type definitions for the polyfill
4
+ // ─────────────────────────────────────────────
5
+
6
+ /** Every method name that exists on the Console API */
7
+ export type ConsoleMethod =
8
+ | 'assert'
9
+ | 'clear'
10
+ | 'count'
11
+ | 'countReset'
12
+ | 'debug'
13
+ | 'dir'
14
+ | 'dirxml'
15
+ | 'error'
16
+ | 'group'
17
+ | 'groupCollapsed'
18
+ | 'groupEnd'
19
+ | 'info'
20
+ | 'log'
21
+ | 'profile'
22
+ | 'profileEnd'
23
+ | 'table'
24
+ | 'time'
25
+ | 'timeEnd'
26
+ | 'timeLog'
27
+ | 'timeStamp'
28
+ | 'trace'
29
+ | 'warn';
30
+
31
+ /** Severity bucket — used for filtering and colouring in the UI */
32
+ export type LogLevel = 'debug' | 'info' | 'warn' | 'error' | 'log';
33
+
34
+ /** A single captured console call */
35
+ export interface LogEntry {
36
+ /** Monotonically-increasing id (generated by the polyfill) */
37
+ id: string;
38
+ /** Which console method was invoked */
39
+ method: ConsoleMethod;
40
+ /** Normalised severity level */
41
+ level: LogLevel;
42
+ /** Human-readable timestamp (ISO-8601) */
43
+ timestamp: string;
44
+ /** Unix epoch in ms */
45
+ timestampMs: number;
46
+ /** Page URL at the time of the call */
47
+ url: string;
48
+ /** User-agent string */
49
+ userAgent: string;
50
+ /** Unique session identifier (persisted in localStorage or a cookie) */
51
+ sessionId: string;
52
+ /**
53
+ * Serialised arguments.
54
+ * We intentionally keep these as strings so JSON serialisation is safe.
55
+ */
56
+ args: string[];
57
+ /** Stack-trace, when available */
58
+ stack?: string;
59
+ /** For console.group / groupCollapsed — current nesting depth */
60
+ groupDepth?: number;
61
+ /** For console.assert — whether the assertion passed */
62
+ assertionPassed?: boolean;
63
+ /** For console.count — current counter value */
64
+ counterValue?: number;
65
+ /** For console.time / timeEnd / timeLog — timer label */
66
+ timerLabel?: string;
67
+ /** For console.timeEnd / timeLog — elapsed ms */
68
+ timerElapsed?: number;
69
+ }
70
+
71
+ /** Configuration accepted by ConsolePatch.install() */
72
+ export interface ConsolePatchConfig {
73
+ /**
74
+ * Endpoint that receives POST requests with batches of LogEntry objects.
75
+ * Set to `null` / omit to disable remote shipping entirely.
76
+ * @default '/api/console-logs'
77
+ */
78
+ endpoint?: string | null;
79
+
80
+ /**
81
+ * HTTP headers added to every POST request.
82
+ * Useful for adding an Authorization header.
83
+ */
84
+ headers?: Record<string, string>;
85
+
86
+ /**
87
+ * Methods to intercept. Defaults to ALL known console methods.
88
+ * Useful to limit noise — e.g. `['error', 'warn']` for production.
89
+ */
90
+ methods?: ConsoleMethod[];
91
+
92
+ /**
93
+ * Whether to also forward intercepted calls to the original native
94
+ * console so they still appear in DevTools.
95
+ * @default true
96
+ */
97
+ passthrough?: boolean;
98
+
99
+ /**
100
+ * Milliseconds to wait before flushing the queue to the backend.
101
+ * Entries are batched in this window for efficiency.
102
+ * @default 2000
103
+ */
104
+ flushInterval?: number;
105
+
106
+ /**
107
+ * Maximum entries in the in-memory queue before an immediate flush
108
+ * is triggered (safety valve for burst logging).
109
+ * @default 50
110
+ */
111
+ maxQueueSize?: number;
112
+
113
+ /**
114
+ * Storage key used to persist the session-id.
115
+ * @default '__cpoly_sid'
116
+ */
117
+ sessionKey?: string;
118
+
119
+ /**
120
+ * Storage mechanism for the session-id.
121
+ * 'localStorage' | 'cookie' | 'none'
122
+ * @default 'localStorage'
123
+ */
124
+ storageType?: 'localStorage' | 'cookie' | 'none';
125
+
126
+ /**
127
+ * Cookie options — only relevant when storageType === 'cookie'.
128
+ */
129
+ cookieOptions?: {
130
+ /** Days until the cookie expires. @default 365 */
131
+ maxAgeDays?: number;
132
+ /** SameSite attribute. @default 'Strict' */
133
+ sameSite?: 'Strict' | 'Lax' | 'None';
134
+ /** Whether to add the Secure flag. @default false */
135
+ secure?: boolean;
136
+ /** Whether to add the HttpOnly flag (not possible via JS — included for docs clarity). */
137
+ httpOnly?: false;
138
+ };
139
+
140
+ /**
141
+ * Maximum length (chars) of any single serialised argument.
142
+ * Longer strings are truncated with a `[truncated]` suffix.
143
+ * @default 2000
144
+ */
145
+ maxArgLength?: number;
146
+
147
+ /**
148
+ * Called after every successful flush to the backend.
149
+ * Receives the entries that were flushed.
150
+ */
151
+ onFlush?: (entries: LogEntry[]) => void;
152
+
153
+ /**
154
+ * Called when a flush fails.
155
+ */
156
+ onFlushError?: (error: Error, entries: LogEntry[]) => void;
157
+ }
158
+
159
+ /** Internal state managed by the patch */
160
+ export interface PatchState {
161
+ installed: boolean;
162
+ sessionId: string;
163
+ queue: LogEntry[];
164
+ flushTimer: ReturnType<typeof setTimeout> | null;
165
+ counters: Record<string, number>;
166
+ timers: Record<string, number>;
167
+ groupDepth: number;
168
+ originalMethods: Partial<Record<ConsoleMethod, (...args: unknown[]) => void>>;
169
+ }