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/LICENSE +21 -0
- package/README.md +167 -0
- package/dist/log-inject.js +497 -0
- package/dist/log-inject.js.map +7 -0
- package/dist/log-inject.min.js +4 -0
- package/index.ts +71 -0
- package/package.json +34 -0
- package/patch.ts +345 -0
- package/serializer.ts +91 -0
- package/session.ts +119 -0
- package/transport.ts +96 -0
- package/tsconfig.json +30 -0
- package/types.ts +169 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Maifee Ul Asad
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
# log-inject
|
|
2
|
+
|
|
3
|
+
A **production-grade TypeScript console interceptor** that:
|
|
4
|
+
|
|
5
|
+
- Wraps **every** `console.*` method (all 22 in the MDN spec)
|
|
6
|
+
- Forwards batched log entries to your backend via `POST`
|
|
7
|
+
- Preserves native DevTools behaviour (passthrough)
|
|
8
|
+
- Persists a session-id via **localStorage** or a **non-tracking cookie** (your choice)
|
|
9
|
+
- Guards against missing methods on old browsers (backward compatibility polyfill)
|
|
10
|
+
- Survives page unloads via `sendBeacon` + synchronous XHR fallback
|
|
11
|
+
- Ships as a single **7 kB minified** browser bundle
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## Project structure
|
|
16
|
+
|
|
17
|
+
```
|
|
18
|
+
log-inject/
|
|
19
|
+
├── src/
|
|
20
|
+
│ ├── types.ts — All TypeScript interfaces & type aliases
|
|
21
|
+
│ ├── session.ts — Session-id persistence (localStorage / cookie / none)
|
|
22
|
+
│ ├── serializer.ts — Safe arg serialisation + stack-trace capture
|
|
23
|
+
│ ├── transport.ts — Batched fetch + XHR fallback + sendBeacon unload flush
|
|
24
|
+
│ ├── patch.ts — Core interception logic for all 22 console methods
|
|
25
|
+
│ └── index.ts — Public barrel + data-attribute auto-installer
|
|
26
|
+
├── dist/
|
|
27
|
+
│ ├── log-inject.js — Unminified bundle (with source map)
|
|
28
|
+
│ └── log-inject.min.js — Minified bundle (7 kB)
|
|
29
|
+
├── example.html — Interactive demo page
|
|
30
|
+
├── tsconfig.json
|
|
31
|
+
└── package.json
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
## Quick start
|
|
37
|
+
|
|
38
|
+
### Method 1 — data-attribute auto-install (zero JS)
|
|
39
|
+
|
|
40
|
+
Drop one `<script>` tag at the **top of your `<head>`**, before any other scripts:
|
|
41
|
+
|
|
42
|
+
```html
|
|
43
|
+
<script
|
|
44
|
+
src="/log-inject.min.js"
|
|
45
|
+
data-endpoint="/api/console-logs"
|
|
46
|
+
data-methods="log,info,warn,error,debug,trace"
|
|
47
|
+
data-storage="localStorage"
|
|
48
|
+
data-flush-interval="3000"
|
|
49
|
+
></script>
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
That's it. Every `console.*` call from that point is captured and batched.
|
|
53
|
+
|
|
54
|
+
### Method 2 — programmatic install
|
|
55
|
+
|
|
56
|
+
```html
|
|
57
|
+
<script src="/log-inject.min.js"></script>
|
|
58
|
+
<script>
|
|
59
|
+
ConsolePatch.install({
|
|
60
|
+
endpoint: '/api/console-logs',
|
|
61
|
+
|
|
62
|
+
// Add auth / correlation headers
|
|
63
|
+
headers: { 'Authorization': 'Bearer ' + getToken() },
|
|
64
|
+
|
|
65
|
+
// Limit to only these methods in production
|
|
66
|
+
methods: ['log', 'info', 'warn', 'error', 'assert', 'trace'],
|
|
67
|
+
|
|
68
|
+
// Keep native DevTools output
|
|
69
|
+
passthrough: true,
|
|
70
|
+
|
|
71
|
+
// Use a non-tracking session cookie instead of localStorage
|
|
72
|
+
storageType: 'cookie',
|
|
73
|
+
cookieOptions: {
|
|
74
|
+
maxAgeDays: 30,
|
|
75
|
+
sameSite: 'Strict',
|
|
76
|
+
secure: true,
|
|
77
|
+
},
|
|
78
|
+
|
|
79
|
+
flushInterval: 5000, // ms between flushes
|
|
80
|
+
maxQueueSize: 200, // immediate flush threshold
|
|
81
|
+
maxArgLength: 4000, // truncate long strings
|
|
82
|
+
|
|
83
|
+
onFlush(entries) {
|
|
84
|
+
console.debug('[polyfill] flushed', entries.length, 'entries');
|
|
85
|
+
},
|
|
86
|
+
onFlushError(err, entries) {
|
|
87
|
+
console.error('[polyfill] flush failed:', err.message);
|
|
88
|
+
},
|
|
89
|
+
});
|
|
90
|
+
</script>
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
---
|
|
94
|
+
|
|
95
|
+
## Backend payload
|
|
96
|
+
|
|
97
|
+
Each `POST` to your endpoint carries:
|
|
98
|
+
|
|
99
|
+
```json
|
|
100
|
+
{
|
|
101
|
+
"logs": [
|
|
102
|
+
{
|
|
103
|
+
"id": "1715510000000-1",
|
|
104
|
+
"method": "error",
|
|
105
|
+
"level": "error",
|
|
106
|
+
"timestamp": "2026-05-12T10:00:00.000Z",
|
|
107
|
+
"timestampMs": 1715510000000,
|
|
108
|
+
"url": "https://yourapp.com/dashboard",
|
|
109
|
+
"userAgent": "Mozilla/5.0 …",
|
|
110
|
+
"sessionId": "a1b2c3d4-…",
|
|
111
|
+
"args": ["Uncaught TypeError", "Cannot read properties of null"],
|
|
112
|
+
"stack": "TypeError: Cannot read …\n at foo (app.js:42:7)\n …",
|
|
113
|
+
"groupDepth": 0
|
|
114
|
+
}
|
|
115
|
+
]
|
|
116
|
+
}
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
---
|
|
120
|
+
|
|
121
|
+
## Configuration reference
|
|
122
|
+
|
|
123
|
+
| Option | Type | Default | Description |
|
|
124
|
+
|---|---|---|---|
|
|
125
|
+
| `endpoint` | `string \| null` | `'/api/console-logs'` | POST URL. `null` disables remote shipping. |
|
|
126
|
+
| `headers` | `Record<string,string>` | `{}` | Extra HTTP headers (e.g. auth). |
|
|
127
|
+
| `methods` | `ConsoleMethod[]` | all 22 | Methods to intercept. |
|
|
128
|
+
| `passthrough` | `boolean` | `true` | Also forward to native DevTools. |
|
|
129
|
+
| `flushInterval` | `number` | `2000` | Milliseconds between batch flushes. |
|
|
130
|
+
| `maxQueueSize` | `number` | `50` | Immediate flush when queue exceeds this. |
|
|
131
|
+
| `sessionKey` | `string` | `'__cpoly_sid'` | Storage key for session-id. |
|
|
132
|
+
| `storageType` | `'localStorage' \| 'cookie' \| 'none'` | `'localStorage'` | Where to persist session-id. |
|
|
133
|
+
| `cookieOptions.maxAgeDays` | `number` | `365` | Cookie lifetime. |
|
|
134
|
+
| `cookieOptions.sameSite` | `'Strict' \| 'Lax' \| 'None'` | `'Strict'` | Cookie SameSite attribute. |
|
|
135
|
+
| `cookieOptions.secure` | `boolean` | `false` | Add Secure flag to cookie. |
|
|
136
|
+
| `maxArgLength` | `number` | `2000` | Truncate serialised args at this length. |
|
|
137
|
+
| `onFlush` | `(entries) => void` | — | Called after successful backend flush. |
|
|
138
|
+
| `onFlushError` | `(err, entries) => void` | — | Called on flush failure. |
|
|
139
|
+
|
|
140
|
+
---
|
|
141
|
+
|
|
142
|
+
## Build commands
|
|
143
|
+
|
|
144
|
+
```bash
|
|
145
|
+
npm run build # typecheck + bundle (dev + min)
|
|
146
|
+
npm run typecheck # tsc --noEmit only
|
|
147
|
+
npm run dev # watch mode (unminified)
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
---
|
|
151
|
+
|
|
152
|
+
## Backward compatibility
|
|
153
|
+
|
|
154
|
+
- All 22 console methods are **checked for existence** before wrapping.
|
|
155
|
+
- Methods absent in old browsers get a no-op shim so call sites don't throw.
|
|
156
|
+
- The bundle targets **ES2015** and works in all evergreen browsers.
|
|
157
|
+
- `fetch` unavailable → falls back to `XMLHttpRequest`.
|
|
158
|
+
- `sendBeacon` unavailable → falls back to synchronous XHR on page unload.
|
|
159
|
+
|
|
160
|
+
---
|
|
161
|
+
|
|
162
|
+
## Session tracking (non-tracking design)
|
|
163
|
+
|
|
164
|
+
The session-id is a random UUID stored **only in your own origin**
|
|
165
|
+
(`localStorage` or a `SameSite=Strict` cookie). It carries no PII and is
|
|
166
|
+
used solely to correlate log entries from the same browser tab session.
|
|
167
|
+
It is **not** shared with third parties.
|
|
@@ -0,0 +1,497 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
(() => {
|
|
3
|
+
// session.ts
|
|
4
|
+
function generateId() {
|
|
5
|
+
var template = "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx";
|
|
6
|
+
return template.replace(/[xy]/g, function(c) {
|
|
7
|
+
var r = Math.random() * 16 | 0;
|
|
8
|
+
var v = c === "x" ? r : r & 3 | 8;
|
|
9
|
+
return v.toString(16);
|
|
10
|
+
});
|
|
11
|
+
}
|
|
12
|
+
function lsGet(key) {
|
|
13
|
+
try {
|
|
14
|
+
return typeof localStorage !== "undefined" ? localStorage.getItem(key) : null;
|
|
15
|
+
} catch (_) {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
function lsSet(key, value) {
|
|
20
|
+
try {
|
|
21
|
+
if (typeof localStorage !== "undefined") {
|
|
22
|
+
localStorage.setItem(key, value);
|
|
23
|
+
}
|
|
24
|
+
} catch (_) {
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
function cookieGet(name) {
|
|
28
|
+
try {
|
|
29
|
+
if (typeof document === "undefined") return null;
|
|
30
|
+
var pairs = document.cookie.split(";");
|
|
31
|
+
for (var i = 0; i < pairs.length; i++) {
|
|
32
|
+
var pair = pairs[i].trim().split("=");
|
|
33
|
+
if (pair[0] === name) {
|
|
34
|
+
return decodeURIComponent(pair[1] || "");
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
} catch (_) {
|
|
38
|
+
}
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
function cookieSet(name, value, maxAgeDays, sameSite, secure) {
|
|
42
|
+
try {
|
|
43
|
+
if (typeof document === "undefined") return;
|
|
44
|
+
var maxAge = maxAgeDays * 24 * 60 * 60;
|
|
45
|
+
var parts = [
|
|
46
|
+
name + "=" + encodeURIComponent(value),
|
|
47
|
+
"Max-Age=" + maxAge,
|
|
48
|
+
"SameSite=" + sameSite,
|
|
49
|
+
"Path=/"
|
|
50
|
+
];
|
|
51
|
+
if (secure) parts.push("Secure");
|
|
52
|
+
document.cookie = parts.join("; ");
|
|
53
|
+
} catch (_) {
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
function resolveSessionId(config) {
|
|
57
|
+
var key = config.sessionKey || "__cpoly_sid";
|
|
58
|
+
var storageType = config.storageType || "localStorage";
|
|
59
|
+
var existing = null;
|
|
60
|
+
if (storageType === "localStorage") {
|
|
61
|
+
existing = lsGet(key);
|
|
62
|
+
} else if (storageType === "cookie") {
|
|
63
|
+
existing = cookieGet(key);
|
|
64
|
+
}
|
|
65
|
+
if (existing && existing.length > 0) {
|
|
66
|
+
return existing;
|
|
67
|
+
}
|
|
68
|
+
var id = generateId();
|
|
69
|
+
if (storageType === "localStorage") {
|
|
70
|
+
lsSet(key, id);
|
|
71
|
+
} else if (storageType === "cookie") {
|
|
72
|
+
var opts = config.cookieOptions || {};
|
|
73
|
+
cookieSet(
|
|
74
|
+
key,
|
|
75
|
+
id,
|
|
76
|
+
opts.maxAgeDays !== void 0 ? opts.maxAgeDays : 365,
|
|
77
|
+
opts.sameSite || "Strict",
|
|
78
|
+
opts.secure || false
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
return id;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// serializer.ts
|
|
85
|
+
function serializeArg(value, maxLength) {
|
|
86
|
+
var result;
|
|
87
|
+
if (value === null) {
|
|
88
|
+
result = "null";
|
|
89
|
+
} else if (value === void 0) {
|
|
90
|
+
result = "undefined";
|
|
91
|
+
} else if (typeof value === "function") {
|
|
92
|
+
result = "[Function: " + (value.name || "anonymous") + "]";
|
|
93
|
+
} else if (typeof value === "symbol") {
|
|
94
|
+
result = value.toString();
|
|
95
|
+
} else if (typeof value === "string") {
|
|
96
|
+
result = value;
|
|
97
|
+
} else if (typeof value === "number" || typeof value === "boolean") {
|
|
98
|
+
result = String(value);
|
|
99
|
+
} else if (value instanceof Error) {
|
|
100
|
+
result = value.name + ": " + value.message;
|
|
101
|
+
if (value.stack) {
|
|
102
|
+
result += "\n" + value.stack;
|
|
103
|
+
}
|
|
104
|
+
} else if (typeof HTMLElement !== "undefined" && value instanceof HTMLElement) {
|
|
105
|
+
result = value.outerHTML ? value.outerHTML.slice(0, 200) : "[HTMLElement: " + value.tagName + "]";
|
|
106
|
+
} else {
|
|
107
|
+
result = safeStringify(value);
|
|
108
|
+
}
|
|
109
|
+
if (result.length > maxLength) {
|
|
110
|
+
result = result.slice(0, maxLength) + " \u2026 [truncated]";
|
|
111
|
+
}
|
|
112
|
+
return result;
|
|
113
|
+
}
|
|
114
|
+
function safeStringify(value) {
|
|
115
|
+
var seen = [];
|
|
116
|
+
try {
|
|
117
|
+
return JSON.stringify(value, function(_key, val) {
|
|
118
|
+
if (typeof val === "object" && val !== null) {
|
|
119
|
+
if (seen.indexOf(val) !== -1) {
|
|
120
|
+
return "[Circular]";
|
|
121
|
+
}
|
|
122
|
+
seen.push(val);
|
|
123
|
+
}
|
|
124
|
+
if (typeof val === "bigint") {
|
|
125
|
+
return val.toString() + "n";
|
|
126
|
+
}
|
|
127
|
+
if (typeof val === "undefined") {
|
|
128
|
+
return "[undefined]";
|
|
129
|
+
}
|
|
130
|
+
return val;
|
|
131
|
+
}, 2);
|
|
132
|
+
} catch (e) {
|
|
133
|
+
return "[UnserializableObject]";
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
function captureStack(frameOffset) {
|
|
137
|
+
try {
|
|
138
|
+
var err = new Error();
|
|
139
|
+
if (!err.stack) return void 0;
|
|
140
|
+
var lines = err.stack.split("\n");
|
|
141
|
+
var start = frameOffset + 2;
|
|
142
|
+
return lines.slice(start).join("\n").trim() || void 0;
|
|
143
|
+
} catch (_) {
|
|
144
|
+
return void 0;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// transport.ts
|
|
149
|
+
function sendBatch(entries, config) {
|
|
150
|
+
var endpoint = config.endpoint;
|
|
151
|
+
if (!endpoint) return Promise.resolve();
|
|
152
|
+
var body = JSON.stringify({ logs: entries });
|
|
153
|
+
var headers = Object.assign(
|
|
154
|
+
{ "Content-Type": "application/json" },
|
|
155
|
+
config.headers || {}
|
|
156
|
+
);
|
|
157
|
+
if (typeof fetch !== "undefined") {
|
|
158
|
+
return fetch(endpoint, {
|
|
159
|
+
method: "POST",
|
|
160
|
+
headers,
|
|
161
|
+
body,
|
|
162
|
+
// keepalive allows the request to outlive the page — useful for
|
|
163
|
+
// capturing errors that happen just before navigation.
|
|
164
|
+
keepalive: true
|
|
165
|
+
}).then(function(res) {
|
|
166
|
+
if (!res.ok) {
|
|
167
|
+
throw new Error("HTTP " + res.status + " from " + endpoint);
|
|
168
|
+
}
|
|
169
|
+
if (config.onFlush) config.onFlush(entries);
|
|
170
|
+
}).catch(function(err) {
|
|
171
|
+
if (config.onFlushError) config.onFlushError(err, entries);
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
return new Promise(function(resolve) {
|
|
175
|
+
try {
|
|
176
|
+
var xhr = new XMLHttpRequest();
|
|
177
|
+
xhr.open("POST", endpoint, true);
|
|
178
|
+
for (var key in headers) {
|
|
179
|
+
if (Object.prototype.hasOwnProperty.call(headers, key)) {
|
|
180
|
+
xhr.setRequestHeader(key, headers[key]);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
xhr.onreadystatechange = function() {
|
|
184
|
+
if (xhr.readyState !== 4) return;
|
|
185
|
+
if (xhr.status >= 200 && xhr.status < 300) {
|
|
186
|
+
if (config.onFlush) config.onFlush(entries);
|
|
187
|
+
} else {
|
|
188
|
+
var err = new Error("XHR " + xhr.status + " from " + endpoint);
|
|
189
|
+
if (config.onFlushError) config.onFlushError(err, entries);
|
|
190
|
+
}
|
|
191
|
+
resolve();
|
|
192
|
+
};
|
|
193
|
+
xhr.send(body);
|
|
194
|
+
} catch (e) {
|
|
195
|
+
if (config.onFlushError) {
|
|
196
|
+
config.onFlushError(e instanceof Error ? e : new Error(String(e)), entries);
|
|
197
|
+
}
|
|
198
|
+
resolve();
|
|
199
|
+
}
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
function sendBeaconFallback(entries, endpoint) {
|
|
203
|
+
if (typeof navigator === "undefined" || !navigator.sendBeacon) return false;
|
|
204
|
+
try {
|
|
205
|
+
var blob = new Blob([JSON.stringify({ logs: entries })], {
|
|
206
|
+
type: "application/json"
|
|
207
|
+
});
|
|
208
|
+
return navigator.sendBeacon(endpoint, blob);
|
|
209
|
+
} catch (_) {
|
|
210
|
+
return false;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// patch.ts
|
|
215
|
+
var ALL_METHODS = [
|
|
216
|
+
"assert",
|
|
217
|
+
"clear",
|
|
218
|
+
"count",
|
|
219
|
+
"countReset",
|
|
220
|
+
"debug",
|
|
221
|
+
"dir",
|
|
222
|
+
"dirxml",
|
|
223
|
+
"error",
|
|
224
|
+
"group",
|
|
225
|
+
"groupCollapsed",
|
|
226
|
+
"groupEnd",
|
|
227
|
+
"info",
|
|
228
|
+
"log",
|
|
229
|
+
"profile",
|
|
230
|
+
"profileEnd",
|
|
231
|
+
"table",
|
|
232
|
+
"time",
|
|
233
|
+
"timeEnd",
|
|
234
|
+
"timeLog",
|
|
235
|
+
"timeStamp",
|
|
236
|
+
"trace",
|
|
237
|
+
"warn"
|
|
238
|
+
];
|
|
239
|
+
var METHOD_TO_LEVEL = {
|
|
240
|
+
assert: "error",
|
|
241
|
+
clear: "log",
|
|
242
|
+
count: "log",
|
|
243
|
+
countReset: "log",
|
|
244
|
+
debug: "debug",
|
|
245
|
+
dir: "log",
|
|
246
|
+
dirxml: "log",
|
|
247
|
+
error: "error",
|
|
248
|
+
group: "log",
|
|
249
|
+
groupCollapsed: "log",
|
|
250
|
+
groupEnd: "log",
|
|
251
|
+
info: "info",
|
|
252
|
+
log: "log",
|
|
253
|
+
profile: "log",
|
|
254
|
+
profileEnd: "log",
|
|
255
|
+
table: "log",
|
|
256
|
+
time: "log",
|
|
257
|
+
timeEnd: "log",
|
|
258
|
+
timeLog: "log",
|
|
259
|
+
timeStamp: "log",
|
|
260
|
+
trace: "log",
|
|
261
|
+
warn: "warn"
|
|
262
|
+
};
|
|
263
|
+
var _idCounter = 0;
|
|
264
|
+
function nextId() {
|
|
265
|
+
_idCounter += 1;
|
|
266
|
+
return Date.now() + "-" + _idCounter;
|
|
267
|
+
}
|
|
268
|
+
var state = {
|
|
269
|
+
installed: false,
|
|
270
|
+
sessionId: "",
|
|
271
|
+
queue: [],
|
|
272
|
+
flushTimer: null,
|
|
273
|
+
counters: {},
|
|
274
|
+
timers: {},
|
|
275
|
+
groupDepth: 0,
|
|
276
|
+
originalMethods: {}
|
|
277
|
+
};
|
|
278
|
+
function handleMethodSemantics(method, args) {
|
|
279
|
+
switch (method) {
|
|
280
|
+
case "assert": {
|
|
281
|
+
var condition = !!args[0];
|
|
282
|
+
return { assertionPassed: condition };
|
|
283
|
+
}
|
|
284
|
+
case "count": {
|
|
285
|
+
var countLabel = String(args[0] !== void 0 ? args[0] : "default");
|
|
286
|
+
state.counters[countLabel] = (state.counters[countLabel] || 0) + 1;
|
|
287
|
+
return { counterValue: state.counters[countLabel] };
|
|
288
|
+
}
|
|
289
|
+
case "countReset": {
|
|
290
|
+
var resetLabel = String(args[0] !== void 0 ? args[0] : "default");
|
|
291
|
+
state.counters[resetLabel] = 0;
|
|
292
|
+
return { counterValue: 0 };
|
|
293
|
+
}
|
|
294
|
+
case "time": {
|
|
295
|
+
var timeLabel = String(args[0] !== void 0 ? args[0] : "default");
|
|
296
|
+
state.timers[timeLabel] = Date.now();
|
|
297
|
+
return { timerLabel: timeLabel };
|
|
298
|
+
}
|
|
299
|
+
case "timeLog":
|
|
300
|
+
case "timeEnd": {
|
|
301
|
+
var teLabel = String(args[0] !== void 0 ? args[0] : "default");
|
|
302
|
+
var start = state.timers[teLabel];
|
|
303
|
+
var elapsed = start !== void 0 ? Date.now() - start : -1;
|
|
304
|
+
if (method === "timeEnd") {
|
|
305
|
+
delete state.timers[teLabel];
|
|
306
|
+
}
|
|
307
|
+
return { timerLabel: teLabel, timerElapsed: elapsed };
|
|
308
|
+
}
|
|
309
|
+
case "timeStamp": {
|
|
310
|
+
var tsLabel = String(args[0] !== void 0 ? args[0] : "");
|
|
311
|
+
return { timerLabel: tsLabel };
|
|
312
|
+
}
|
|
313
|
+
case "group":
|
|
314
|
+
case "groupCollapsed": {
|
|
315
|
+
var depth = state.groupDepth;
|
|
316
|
+
state.groupDepth += 1;
|
|
317
|
+
return { groupDepth: depth };
|
|
318
|
+
}
|
|
319
|
+
case "groupEnd": {
|
|
320
|
+
if (state.groupDepth > 0) state.groupDepth -= 1;
|
|
321
|
+
return { groupDepth: state.groupDepth };
|
|
322
|
+
}
|
|
323
|
+
default:
|
|
324
|
+
return {};
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
function createWrapper(method, config) {
|
|
328
|
+
var maxLen = config.maxArgLength !== void 0 ? config.maxArgLength : 2e3;
|
|
329
|
+
var passthrough = config.passthrough !== false;
|
|
330
|
+
return function(...args) {
|
|
331
|
+
if (passthrough) {
|
|
332
|
+
var native = state.originalMethods[method];
|
|
333
|
+
if (typeof native === "function") {
|
|
334
|
+
try {
|
|
335
|
+
native.apply(console, args);
|
|
336
|
+
} catch (_) {
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
if (method === "assert" && args[0]) return;
|
|
341
|
+
var semantics = handleMethodSemantics(method, args);
|
|
342
|
+
var now = Date.now();
|
|
343
|
+
var serialisedArgs = args.map(function(a) {
|
|
344
|
+
return serializeArg(a, maxLen);
|
|
345
|
+
});
|
|
346
|
+
var entry = Object.assign(
|
|
347
|
+
{
|
|
348
|
+
id: nextId(),
|
|
349
|
+
method,
|
|
350
|
+
level: METHOD_TO_LEVEL[method],
|
|
351
|
+
timestamp: new Date(now).toISOString(),
|
|
352
|
+
timestampMs: now,
|
|
353
|
+
url: typeof location !== "undefined" ? location.href : "",
|
|
354
|
+
userAgent: typeof navigator !== "undefined" ? navigator.userAgent : "",
|
|
355
|
+
sessionId: state.sessionId,
|
|
356
|
+
args: serialisedArgs,
|
|
357
|
+
stack: method === "trace" || method === "error" ? captureStack(1) : void 0,
|
|
358
|
+
groupDepth: state.groupDepth
|
|
359
|
+
},
|
|
360
|
+
semantics
|
|
361
|
+
);
|
|
362
|
+
state.queue.push(entry);
|
|
363
|
+
var maxQueue = config.maxQueueSize !== void 0 ? config.maxQueueSize : 50;
|
|
364
|
+
if (state.queue.length >= maxQueue) {
|
|
365
|
+
flushNow(config);
|
|
366
|
+
}
|
|
367
|
+
};
|
|
368
|
+
}
|
|
369
|
+
function flushNow(config) {
|
|
370
|
+
if (state.queue.length === 0) return;
|
|
371
|
+
var batch = state.queue.splice(0);
|
|
372
|
+
sendBatch(batch, config);
|
|
373
|
+
}
|
|
374
|
+
function scheduleFlush(config) {
|
|
375
|
+
if (state.flushTimer !== null) return;
|
|
376
|
+
var interval = config.flushInterval !== void 0 ? config.flushInterval : 2e3;
|
|
377
|
+
state.flushTimer = setTimeout(function() {
|
|
378
|
+
state.flushTimer = null;
|
|
379
|
+
flushNow(config);
|
|
380
|
+
scheduleFlush(config);
|
|
381
|
+
}, interval);
|
|
382
|
+
}
|
|
383
|
+
function installUnloadFlush(config) {
|
|
384
|
+
var endpoint = config.endpoint;
|
|
385
|
+
if (!endpoint) return;
|
|
386
|
+
var handler = function() {
|
|
387
|
+
if (state.queue.length === 0) return;
|
|
388
|
+
var batch = state.queue.splice(0);
|
|
389
|
+
if (!sendBeaconFallback(batch, endpoint)) {
|
|
390
|
+
try {
|
|
391
|
+
var xhr = new XMLHttpRequest();
|
|
392
|
+
xhr.open(
|
|
393
|
+
"POST",
|
|
394
|
+
endpoint,
|
|
395
|
+
false
|
|
396
|
+
/* sync */
|
|
397
|
+
);
|
|
398
|
+
xhr.setRequestHeader("Content-Type", "application/json");
|
|
399
|
+
xhr.send(JSON.stringify({ logs: batch }));
|
|
400
|
+
} catch (_) {
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
};
|
|
404
|
+
if (typeof addEventListener !== "undefined") {
|
|
405
|
+
addEventListener("visibilitychange", function() {
|
|
406
|
+
if (document.visibilityState === "hidden") handler();
|
|
407
|
+
});
|
|
408
|
+
addEventListener("pagehide", handler);
|
|
409
|
+
addEventListener("beforeunload", handler);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
function install(userConfig) {
|
|
413
|
+
if (state.installed) return;
|
|
414
|
+
var config = Object.assign(
|
|
415
|
+
{
|
|
416
|
+
endpoint: "/api/console-logs",
|
|
417
|
+
methods: ALL_METHODS,
|
|
418
|
+
passthrough: true,
|
|
419
|
+
flushInterval: 2e3,
|
|
420
|
+
maxQueueSize: 50,
|
|
421
|
+
sessionKey: "__cpoly_sid",
|
|
422
|
+
storageType: "localStorage",
|
|
423
|
+
maxArgLength: 2e3
|
|
424
|
+
},
|
|
425
|
+
userConfig || {}
|
|
426
|
+
);
|
|
427
|
+
state.sessionId = resolveSessionId(config);
|
|
428
|
+
var methodsToWrap = config.methods || ALL_METHODS;
|
|
429
|
+
for (var i = 0; i < methodsToWrap.length; i++) {
|
|
430
|
+
var method = methodsToWrap[i];
|
|
431
|
+
var original = console[method];
|
|
432
|
+
if (typeof original === "function") {
|
|
433
|
+
state.originalMethods[method] = original;
|
|
434
|
+
} else {
|
|
435
|
+
state.originalMethods[method] = function() {
|
|
436
|
+
};
|
|
437
|
+
}
|
|
438
|
+
console[method] = createWrapper(method, config);
|
|
439
|
+
}
|
|
440
|
+
scheduleFlush(config);
|
|
441
|
+
installUnloadFlush(config);
|
|
442
|
+
state.installed = true;
|
|
443
|
+
}
|
|
444
|
+
function uninstall(config) {
|
|
445
|
+
if (!state.installed) return;
|
|
446
|
+
if (config) flushNow(config);
|
|
447
|
+
for (var method in state.originalMethods) {
|
|
448
|
+
if (Object.prototype.hasOwnProperty.call(state.originalMethods, method)) {
|
|
449
|
+
var orig = state.originalMethods[method];
|
|
450
|
+
if (orig !== void 0) {
|
|
451
|
+
console[method] = orig;
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
state.originalMethods = {};
|
|
456
|
+
state.installed = false;
|
|
457
|
+
if (state.flushTimer !== null) {
|
|
458
|
+
clearTimeout(state.flushTimer);
|
|
459
|
+
state.flushTimer = null;
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
function flush(config) {
|
|
463
|
+
flushNow(config);
|
|
464
|
+
}
|
|
465
|
+
function getQueue() {
|
|
466
|
+
return state.queue.slice();
|
|
467
|
+
}
|
|
468
|
+
function isInstalled() {
|
|
469
|
+
return state.installed;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
// index.ts
|
|
473
|
+
(function autoInstall() {
|
|
474
|
+
if (typeof document === "undefined") return;
|
|
475
|
+
var scripts = document.querySelectorAll("script[data-endpoint]");
|
|
476
|
+
if (!scripts || scripts.length === 0) return;
|
|
477
|
+
var tag = scripts[scripts.length - 1];
|
|
478
|
+
var endpoint = tag.getAttribute("data-endpoint");
|
|
479
|
+
if (!endpoint) return;
|
|
480
|
+
var rawMethods = tag.getAttribute("data-methods");
|
|
481
|
+
var storage = tag.getAttribute("data-storage");
|
|
482
|
+
var flushRaw = tag.getAttribute("data-flush-interval");
|
|
483
|
+
var maxQueueRaw = tag.getAttribute("data-max-queue");
|
|
484
|
+
var maxArgRaw = tag.getAttribute("data-max-arg-length");
|
|
485
|
+
var config = { endpoint };
|
|
486
|
+
if (rawMethods) {
|
|
487
|
+
config.methods = rawMethods.split(",").map(function(m) {
|
|
488
|
+
return m.trim();
|
|
489
|
+
});
|
|
490
|
+
}
|
|
491
|
+
if (storage) config.storageType = storage;
|
|
492
|
+
if (flushRaw) config.flushInterval = parseInt(flushRaw, 10);
|
|
493
|
+
if (maxQueueRaw) config.maxQueueSize = parseInt(maxQueueRaw, 10);
|
|
494
|
+
if (maxArgRaw) config.maxArgLength = parseInt(maxArgRaw, 10);
|
|
495
|
+
install(config);
|
|
496
|
+
})();
|
|
497
|
+
})();
|