@overcastsre/browser 1.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/README.md +36 -0
- package/index.js +845 -0
- package/package.json +13 -0
package/README.md
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# @overcast/browser
|
|
2
|
+
|
|
3
|
+
Overcast SRE monitoring SDK for browsers. One line — captures everything client-side.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @overcast/browser
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
```js
|
|
14
|
+
import Overcast from '@overcast/browser';
|
|
15
|
+
Overcast.init({ apiKey: 'oc_...', serviceName: 'my-app' });
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Or via CDN:
|
|
19
|
+
|
|
20
|
+
```html
|
|
21
|
+
<script src="https://cdn.overcastsre.com/sdk/browser/1.0.0/overcast.min.js"></script>
|
|
22
|
+
<script>Overcast.init({ apiKey: 'oc_...', serviceName: 'my-app' });</script>
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Automatically captures:
|
|
26
|
+
|
|
27
|
+
- All console output (log, info, debug, warn, error)
|
|
28
|
+
- JavaScript errors and unhandled promise rejections
|
|
29
|
+
- Resource loading failures (images, scripts, CSS)
|
|
30
|
+
- All fetch() and XMLHttpRequest calls
|
|
31
|
+
- Web Vitals (LCP, FID, CLS, INP)
|
|
32
|
+
- Slow page loads and high memory
|
|
33
|
+
- Rage clicks and dead clicks
|
|
34
|
+
- SPA navigation (pushState, popstate, hashchange)
|
|
35
|
+
- CSP violations
|
|
36
|
+
- Online/offline connectivity changes
|
package/index.js
ADDED
|
@@ -0,0 +1,845 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @overcast/browser — Overcast SRE Monitoring SDK for Browsers
|
|
3
|
+
*
|
|
4
|
+
* Captures EVERYTHING from the client side:
|
|
5
|
+
* - All console output (log, info, debug, warn, error)
|
|
6
|
+
* - JavaScript errors (uncaught exceptions, promise rejections)
|
|
7
|
+
* - Resource loading failures (images, scripts, stylesheets)
|
|
8
|
+
* - All fetch() and XMLHttpRequest calls (status, latency, bodies)
|
|
9
|
+
* - Web Vitals (LCP, FID, CLS, TTFB, INP)
|
|
10
|
+
* - Performance (slow page loads, high memory)
|
|
11
|
+
* - UX issues (rage clicks, dead clicks)
|
|
12
|
+
* - Navigation / SPA route changes
|
|
13
|
+
* - Content Security Policy violations
|
|
14
|
+
*
|
|
15
|
+
* Usage — two lines:
|
|
16
|
+
* import Overcast from '@overcast/browser';
|
|
17
|
+
* Overcast.init({ apiKey: 'oc_...', serviceName: 'my-app' });
|
|
18
|
+
*
|
|
19
|
+
* @module @overcast/browser
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
(function (root, factory) {
|
|
23
|
+
if (typeof module !== 'undefined' && module.exports) {
|
|
24
|
+
module.exports = factory();
|
|
25
|
+
} else if (typeof define === 'function' && define.amd) {
|
|
26
|
+
define(factory);
|
|
27
|
+
} else {
|
|
28
|
+
root.Overcast = factory();
|
|
29
|
+
}
|
|
30
|
+
})(typeof window !== 'undefined' ? window : typeof globalThis !== 'undefined' ? globalThis : this, function () {
|
|
31
|
+
'use strict';
|
|
32
|
+
|
|
33
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
34
|
+
// STATE
|
|
35
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
36
|
+
|
|
37
|
+
var _config = {
|
|
38
|
+
apiKey: '',
|
|
39
|
+
serviceName: 'web-app',
|
|
40
|
+
environment: 'production',
|
|
41
|
+
baseUrl: 'https://platform.overcastsre.com',
|
|
42
|
+
version: '',
|
|
43
|
+
|
|
44
|
+
captureConsole: true,
|
|
45
|
+
captureErrors: true,
|
|
46
|
+
captureRejections: true,
|
|
47
|
+
captureResources: true,
|
|
48
|
+
captureNetwork: true,
|
|
49
|
+
capturePerformance: true,
|
|
50
|
+
captureWebVitals: true,
|
|
51
|
+
captureUX: true,
|
|
52
|
+
captureCSP: true,
|
|
53
|
+
captureNavigation: true,
|
|
54
|
+
|
|
55
|
+
slowPageLoadThreshold: 5000,
|
|
56
|
+
slowApiThreshold: 5000,
|
|
57
|
+
clickSamplingRate: 0.1,
|
|
58
|
+
logSamplingRate: 1.0,
|
|
59
|
+
|
|
60
|
+
batchSize: 50,
|
|
61
|
+
flushInterval: 5000,
|
|
62
|
+
maxBufferSize: 500,
|
|
63
|
+
|
|
64
|
+
encryptPayload: false,
|
|
65
|
+
encryptionKey: '',
|
|
66
|
+
sanitize: true,
|
|
67
|
+
|
|
68
|
+
debug: false,
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
var _initialized = false;
|
|
72
|
+
var _sending = false;
|
|
73
|
+
var _buffer = [];
|
|
74
|
+
var _flushTimer = null;
|
|
75
|
+
var _originals = {};
|
|
76
|
+
var INGEST_ENDPOINT = '/api/v1/ingest/logs';
|
|
77
|
+
|
|
78
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
79
|
+
// PUBLIC API
|
|
80
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
81
|
+
|
|
82
|
+
function init(options) {
|
|
83
|
+
if (_initialized) return Overcast;
|
|
84
|
+
if (typeof window === 'undefined') return Overcast;
|
|
85
|
+
|
|
86
|
+
if (!options || !options.apiKey) {
|
|
87
|
+
console.error('[Overcast] apiKey is required. Get yours at https://overcastsre.com/settings');
|
|
88
|
+
return Overcast;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
_assign(_config, options);
|
|
92
|
+
_initialized = true;
|
|
93
|
+
|
|
94
|
+
if (window.__OVERCAST_INSTALLED__) return Overcast;
|
|
95
|
+
window.__OVERCAST_INSTALLED__ = true;
|
|
96
|
+
|
|
97
|
+
if (_config.captureConsole) _installConsoleCapture();
|
|
98
|
+
if (_config.captureErrors) _installErrorCapture();
|
|
99
|
+
if (_config.captureRejections) _installRejectionCapture();
|
|
100
|
+
if (_config.captureResources) _installResourceCapture();
|
|
101
|
+
if (_config.captureNetwork) _installNetworkCapture();
|
|
102
|
+
if (_config.capturePerformance) _installPerformanceCapture();
|
|
103
|
+
if (_config.captureWebVitals) _installWebVitals();
|
|
104
|
+
if (_config.captureUX) _installUXCapture();
|
|
105
|
+
if (_config.captureCSP) _installCSPCapture();
|
|
106
|
+
if (_config.captureNavigation) _installNavigationCapture();
|
|
107
|
+
|
|
108
|
+
// Flush before page unload
|
|
109
|
+
window.addEventListener('beforeunload', function () { _flush(); });
|
|
110
|
+
document.addEventListener('visibilitychange', function () {
|
|
111
|
+
if (document.visibilityState === 'hidden') _flush();
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
// Start flush timer
|
|
115
|
+
_flushTimer = setInterval(_flush, _config.flushInterval);
|
|
116
|
+
|
|
117
|
+
_enqueue('INFO', 'Overcast SDK initialized', {
|
|
118
|
+
type: 'sdk_init',
|
|
119
|
+
sdkVersion: '1.0.0',
|
|
120
|
+
userAgent: navigator.userAgent,
|
|
121
|
+
url: location.href,
|
|
122
|
+
screenWidth: screen.width,
|
|
123
|
+
screenHeight: screen.height,
|
|
124
|
+
language: navigator.language,
|
|
125
|
+
online: navigator.onLine,
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
return Overcast;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
132
|
+
// CONSOLE CAPTURE
|
|
133
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
134
|
+
|
|
135
|
+
function _installConsoleCapture() {
|
|
136
|
+
var methods = ['log', 'info', 'debug', 'warn', 'error', 'trace'];
|
|
137
|
+
var levelMap = { log: 'INFO', info: 'INFO', debug: 'DEBUG', warn: 'WARNING', error: 'ERROR', trace: 'DEBUG' };
|
|
138
|
+
|
|
139
|
+
methods.forEach(function (method) {
|
|
140
|
+
_originals['console_' + method] = console[method];
|
|
141
|
+
console[method] = function () {
|
|
142
|
+
var args = Array.prototype.slice.call(arguments);
|
|
143
|
+
_originals['console_' + method].apply(console, args);
|
|
144
|
+
|
|
145
|
+
if (_sending) return;
|
|
146
|
+
if (Math.random() > _config.logSamplingRate) return;
|
|
147
|
+
|
|
148
|
+
var parsed = _formatArgs(args);
|
|
149
|
+
var meta = { type: 'console_' + method };
|
|
150
|
+
if (parsed.errorObj) {
|
|
151
|
+
meta.error = { name: parsed.errorObj.name, message: parsed.errorObj.message, stack: parsed.errorObj.stack };
|
|
152
|
+
}
|
|
153
|
+
_enqueue(levelMap[method] || 'INFO', parsed.message, meta);
|
|
154
|
+
};
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
159
|
+
// ERROR CAPTURE — uncaught JS errors
|
|
160
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
161
|
+
|
|
162
|
+
function _installErrorCapture() {
|
|
163
|
+
window.addEventListener('error', function (event) {
|
|
164
|
+
if (event.target !== window) return; // Resource errors handled separately
|
|
165
|
+
_enqueue('ERROR', event.message || 'Unknown JS error', {
|
|
166
|
+
type: 'javascript_error',
|
|
167
|
+
error: event.error ? { name: event.error.name, message: event.error.message, stack: event.error.stack } : null,
|
|
168
|
+
filename: event.filename,
|
|
169
|
+
lineno: event.lineno,
|
|
170
|
+
colno: event.colno,
|
|
171
|
+
url: location.href,
|
|
172
|
+
});
|
|
173
|
+
}, true);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
177
|
+
// PROMISE REJECTION CAPTURE
|
|
178
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
179
|
+
|
|
180
|
+
function _installRejectionCapture() {
|
|
181
|
+
window.addEventListener('unhandledrejection', function (event) {
|
|
182
|
+
var err = event.reason instanceof Error ? event.reason : new Error(String(event.reason));
|
|
183
|
+
_enqueue('ERROR', 'Unhandled Promise Rejection: ' + err.message, {
|
|
184
|
+
type: 'unhandled_rejection',
|
|
185
|
+
error: { name: err.name, message: err.message, stack: err.stack },
|
|
186
|
+
url: location.href,
|
|
187
|
+
});
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
192
|
+
// RESOURCE FAILURE CAPTURE — images, scripts, CSS that fail to load
|
|
193
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
194
|
+
|
|
195
|
+
function _installResourceCapture() {
|
|
196
|
+
window.addEventListener('error', function (event) {
|
|
197
|
+
if (event.target === window) return; // JS errors handled above
|
|
198
|
+
var tag = event.target.tagName || '';
|
|
199
|
+
var src = event.target.src || event.target.href || '';
|
|
200
|
+
_enqueue('WARNING', 'Failed to load ' + tag + ': ' + src, {
|
|
201
|
+
type: 'resource_failure',
|
|
202
|
+
resourceType: tag,
|
|
203
|
+
resourceUrl: src,
|
|
204
|
+
url: location.href,
|
|
205
|
+
});
|
|
206
|
+
}, true);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
210
|
+
// NETWORK CAPTURE — wraps fetch() and XMLHttpRequest
|
|
211
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
212
|
+
|
|
213
|
+
function _installNetworkCapture() {
|
|
214
|
+
// ── Wrap fetch ──
|
|
215
|
+
if (typeof window.fetch === 'function') {
|
|
216
|
+
var origFetch = window.fetch;
|
|
217
|
+
window.fetch = function () {
|
|
218
|
+
var args = arguments;
|
|
219
|
+
var requestUrl = typeof args[0] === 'string' ? args[0] : (args[0] && args[0].url ? args[0].url : '');
|
|
220
|
+
|
|
221
|
+
// Skip Overcast's own calls
|
|
222
|
+
if (requestUrl.indexOf(_config.baseUrl) !== -1) return origFetch.apply(this, args);
|
|
223
|
+
|
|
224
|
+
var method = 'GET';
|
|
225
|
+
var requestBody = null;
|
|
226
|
+
try {
|
|
227
|
+
var opts = args[1] || {};
|
|
228
|
+
method = opts.method || 'GET';
|
|
229
|
+
if (opts.body) {
|
|
230
|
+
if (typeof opts.body === 'string') {
|
|
231
|
+
try { requestBody = JSON.parse(opts.body); } catch (e) { requestBody = opts.body.substring(0, 2000); }
|
|
232
|
+
} else if (opts.body instanceof FormData) {
|
|
233
|
+
requestBody = '[FormData]';
|
|
234
|
+
} else {
|
|
235
|
+
requestBody = '[Body]';
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
} catch (e) { /* ignore */ }
|
|
239
|
+
|
|
240
|
+
var startTime = Date.now();
|
|
241
|
+
|
|
242
|
+
return origFetch.apply(this, args).then(function (response) {
|
|
243
|
+
var duration = Date.now() - startTime;
|
|
244
|
+
var clone = response.clone();
|
|
245
|
+
|
|
246
|
+
// Read response body (async)
|
|
247
|
+
clone.text().then(function (text) {
|
|
248
|
+
var responseBody = text.substring(0, 2000);
|
|
249
|
+
try { responseBody = JSON.parse(text); } catch (e) { /* keep as string */ }
|
|
250
|
+
|
|
251
|
+
var meta = {
|
|
252
|
+
type: 'http_outgoing',
|
|
253
|
+
method: method,
|
|
254
|
+
url: requestUrl,
|
|
255
|
+
statusCode: response.status,
|
|
256
|
+
statusText: response.statusText,
|
|
257
|
+
duration: duration,
|
|
258
|
+
requestBody: requestBody,
|
|
259
|
+
responseBody: responseBody,
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
if (duration > _config.slowApiThreshold) {
|
|
263
|
+
_enqueue('WARNING', 'Slow request: ' + method + ' ' + requestUrl + ' (' + duration + 'ms)', _assign({}, meta, { type: 'slow_http' }));
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
if (response.status >= 500) {
|
|
267
|
+
_enqueue('ERROR', 'HTTP ' + response.status + ': ' + method + ' ' + requestUrl, meta);
|
|
268
|
+
} else if (response.status >= 400) {
|
|
269
|
+
_enqueue('WARNING', 'HTTP ' + response.status + ': ' + method + ' ' + requestUrl, meta);
|
|
270
|
+
} else {
|
|
271
|
+
_enqueue('DEBUG', 'HTTP ' + response.status + ': ' + method + ' ' + requestUrl + ' (' + duration + 'ms)', meta);
|
|
272
|
+
}
|
|
273
|
+
}).catch(function () { /* ignore body parse failures */ });
|
|
274
|
+
|
|
275
|
+
return response;
|
|
276
|
+
}).catch(function (err) {
|
|
277
|
+
var duration = Date.now() - startTime;
|
|
278
|
+
_enqueue('ERROR', 'Network error: ' + method + ' ' + requestUrl + ' — ' + err.message, {
|
|
279
|
+
type: 'http_network_error',
|
|
280
|
+
method: method,
|
|
281
|
+
url: requestUrl,
|
|
282
|
+
duration: duration,
|
|
283
|
+
error: { name: err.name, message: err.message, stack: err.stack },
|
|
284
|
+
});
|
|
285
|
+
throw err;
|
|
286
|
+
});
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// ── Wrap XMLHttpRequest ──
|
|
291
|
+
if (typeof XMLHttpRequest !== 'undefined') {
|
|
292
|
+
var origOpen = XMLHttpRequest.prototype.open;
|
|
293
|
+
var origSend = XMLHttpRequest.prototype.send;
|
|
294
|
+
|
|
295
|
+
XMLHttpRequest.prototype.open = function (method, url) {
|
|
296
|
+
this._overcast = { method: method, url: url, startTime: 0 };
|
|
297
|
+
return origOpen.apply(this, arguments);
|
|
298
|
+
};
|
|
299
|
+
|
|
300
|
+
XMLHttpRequest.prototype.send = function (body) {
|
|
301
|
+
var xhr = this;
|
|
302
|
+
var meta = xhr._overcast || {};
|
|
303
|
+
|
|
304
|
+
// Skip Overcast's own calls
|
|
305
|
+
if (meta.url && meta.url.indexOf(_config.baseUrl) !== -1) return origSend.apply(this, arguments);
|
|
306
|
+
|
|
307
|
+
meta.startTime = Date.now();
|
|
308
|
+
meta.requestBody = body ? (typeof body === 'string' ? body.substring(0, 2000) : '[Body]') : null;
|
|
309
|
+
|
|
310
|
+
xhr.addEventListener('loadend', function () {
|
|
311
|
+
var duration = Date.now() - meta.startTime;
|
|
312
|
+
var responseBody = null;
|
|
313
|
+
try { responseBody = xhr.responseText ? xhr.responseText.substring(0, 2000) : null; } catch (e) { /* ignore */ }
|
|
314
|
+
|
|
315
|
+
var info = {
|
|
316
|
+
type: 'xhr_outgoing',
|
|
317
|
+
method: meta.method,
|
|
318
|
+
url: meta.url,
|
|
319
|
+
statusCode: xhr.status,
|
|
320
|
+
duration: duration,
|
|
321
|
+
requestBody: meta.requestBody,
|
|
322
|
+
responseBody: responseBody,
|
|
323
|
+
};
|
|
324
|
+
|
|
325
|
+
if (duration > _config.slowApiThreshold) {
|
|
326
|
+
_enqueue('WARNING', 'Slow XHR: ' + meta.method + ' ' + meta.url + ' (' + duration + 'ms)', _assign({}, info, { type: 'slow_xhr' }));
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
if (xhr.status >= 500) {
|
|
330
|
+
_enqueue('ERROR', 'XHR ' + xhr.status + ': ' + meta.method + ' ' + meta.url, info);
|
|
331
|
+
} else if (xhr.status >= 400) {
|
|
332
|
+
_enqueue('WARNING', 'XHR ' + xhr.status + ': ' + meta.method + ' ' + meta.url, info);
|
|
333
|
+
}
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
xhr.addEventListener('error', function () {
|
|
337
|
+
_enqueue('ERROR', 'XHR network error: ' + meta.method + ' ' + meta.url, {
|
|
338
|
+
type: 'xhr_network_error',
|
|
339
|
+
method: meta.method,
|
|
340
|
+
url: meta.url,
|
|
341
|
+
duration: Date.now() - meta.startTime,
|
|
342
|
+
});
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
xhr.addEventListener('timeout', function () {
|
|
346
|
+
_enqueue('ERROR', 'XHR timeout: ' + meta.method + ' ' + meta.url, {
|
|
347
|
+
type: 'xhr_timeout',
|
|
348
|
+
method: meta.method,
|
|
349
|
+
url: meta.url,
|
|
350
|
+
duration: Date.now() - meta.startTime,
|
|
351
|
+
});
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
return origSend.apply(this, arguments);
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
360
|
+
// PERFORMANCE — page load, memory
|
|
361
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
362
|
+
|
|
363
|
+
function _installPerformanceCapture() {
|
|
364
|
+
window.addEventListener('load', function () {
|
|
365
|
+
setTimeout(function () {
|
|
366
|
+
// Navigation timing
|
|
367
|
+
if (performance.getEntriesByType) {
|
|
368
|
+
var nav = performance.getEntriesByType('navigation')[0];
|
|
369
|
+
if (nav) {
|
|
370
|
+
var loadTime = Math.round(nav.loadEventEnd - nav.startTime);
|
|
371
|
+
if (loadTime > _config.slowPageLoadThreshold) {
|
|
372
|
+
_enqueue('WARNING', 'Slow page load: ' + loadTime + 'ms', {
|
|
373
|
+
type: 'slow_page_load',
|
|
374
|
+
loadTime: loadTime,
|
|
375
|
+
domContentLoaded: Math.round(nav.domContentLoadedEventEnd - nav.startTime),
|
|
376
|
+
ttfb: Math.round(nav.responseStart - nav.requestStart),
|
|
377
|
+
domInteractive: Math.round(nav.domInteractive - nav.startTime),
|
|
378
|
+
url: location.href,
|
|
379
|
+
});
|
|
380
|
+
} else {
|
|
381
|
+
_enqueue('INFO', 'Page loaded in ' + loadTime + 'ms', {
|
|
382
|
+
type: 'page_load',
|
|
383
|
+
loadTime: loadTime,
|
|
384
|
+
domContentLoaded: Math.round(nav.domContentLoadedEventEnd - nav.startTime),
|
|
385
|
+
ttfb: Math.round(nav.responseStart - nav.requestStart),
|
|
386
|
+
url: location.href,
|
|
387
|
+
});
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// Memory (Chrome only)
|
|
393
|
+
if (performance.memory) {
|
|
394
|
+
var usedMB = (performance.memory.usedJSHeapSize / 1048576).toFixed(1);
|
|
395
|
+
var totalMB = (performance.memory.jsHeapSizeLimit / 1048576).toFixed(1);
|
|
396
|
+
var pct = ((performance.memory.usedJSHeapSize / performance.memory.jsHeapSizeLimit) * 100).toFixed(1);
|
|
397
|
+
|
|
398
|
+
if (+pct > 85) {
|
|
399
|
+
_enqueue('WARNING', 'High memory: ' + pct + '% (' + usedMB + 'MB)', {
|
|
400
|
+
type: 'performance_memory',
|
|
401
|
+
usedMB: +usedMB,
|
|
402
|
+
totalMB: +totalMB,
|
|
403
|
+
percent: +pct,
|
|
404
|
+
});
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
}, 100);
|
|
408
|
+
});
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
412
|
+
// WEB VITALS — LCP, FID, CLS, INP
|
|
413
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
414
|
+
|
|
415
|
+
function _installWebVitals() {
|
|
416
|
+
// Largest Contentful Paint (LCP)
|
|
417
|
+
if (typeof PerformanceObserver !== 'undefined') {
|
|
418
|
+
try {
|
|
419
|
+
new PerformanceObserver(function (list) {
|
|
420
|
+
var entries = list.getEntries();
|
|
421
|
+
var last = entries[entries.length - 1];
|
|
422
|
+
if (last) {
|
|
423
|
+
var lcp = Math.round(last.startTime);
|
|
424
|
+
_enqueue(lcp > 2500 ? 'WARNING' : 'INFO', 'LCP: ' + lcp + 'ms', {
|
|
425
|
+
type: 'web_vital_lcp',
|
|
426
|
+
value: lcp,
|
|
427
|
+
rating: lcp <= 2500 ? 'good' : lcp <= 4000 ? 'needs-improvement' : 'poor',
|
|
428
|
+
element: last.element ? last.element.tagName : null,
|
|
429
|
+
url: location.href,
|
|
430
|
+
});
|
|
431
|
+
}
|
|
432
|
+
}).observe({ type: 'largest-contentful-paint', buffered: true });
|
|
433
|
+
} catch (e) { /* browser doesn't support */ }
|
|
434
|
+
|
|
435
|
+
// First Input Delay (FID) / Interaction to Next Paint (INP)
|
|
436
|
+
try {
|
|
437
|
+
new PerformanceObserver(function (list) {
|
|
438
|
+
var entries = list.getEntries();
|
|
439
|
+
entries.forEach(function (entry) {
|
|
440
|
+
var delay = Math.round(entry.processingStart - entry.startTime);
|
|
441
|
+
if (delay > 100) {
|
|
442
|
+
_enqueue('WARNING', 'Slow interaction: ' + delay + 'ms delay', {
|
|
443
|
+
type: 'web_vital_fid',
|
|
444
|
+
value: delay,
|
|
445
|
+
rating: delay <= 100 ? 'good' : delay <= 300 ? 'needs-improvement' : 'poor',
|
|
446
|
+
eventType: entry.name,
|
|
447
|
+
url: location.href,
|
|
448
|
+
});
|
|
449
|
+
}
|
|
450
|
+
});
|
|
451
|
+
}).observe({ type: 'first-input', buffered: true });
|
|
452
|
+
} catch (e) { /* ignore */ }
|
|
453
|
+
|
|
454
|
+
// Cumulative Layout Shift (CLS)
|
|
455
|
+
try {
|
|
456
|
+
var clsValue = 0;
|
|
457
|
+
new PerformanceObserver(function (list) {
|
|
458
|
+
list.getEntries().forEach(function (entry) {
|
|
459
|
+
if (!entry.hadRecentInput) {
|
|
460
|
+
clsValue += entry.value;
|
|
461
|
+
}
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
if (clsValue > 0.1) {
|
|
465
|
+
_enqueue('WARNING', 'High CLS: ' + clsValue.toFixed(3), {
|
|
466
|
+
type: 'web_vital_cls',
|
|
467
|
+
value: +clsValue.toFixed(3),
|
|
468
|
+
rating: clsValue <= 0.1 ? 'good' : clsValue <= 0.25 ? 'needs-improvement' : 'poor',
|
|
469
|
+
url: location.href,
|
|
470
|
+
});
|
|
471
|
+
}
|
|
472
|
+
}).observe({ type: 'layout-shift', buffered: true });
|
|
473
|
+
} catch (e) { /* ignore */ }
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
478
|
+
// UX CAPTURE — rage clicks, dead clicks
|
|
479
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
480
|
+
|
|
481
|
+
function _installUXCapture() {
|
|
482
|
+
var clickCounts = {};
|
|
483
|
+
var clickTimestamps = {};
|
|
484
|
+
|
|
485
|
+
document.addEventListener('click', function (event) {
|
|
486
|
+
if (Math.random() > _config.clickSamplingRate) return;
|
|
487
|
+
|
|
488
|
+
var target = event.target;
|
|
489
|
+
var xpath = _getXPath(target);
|
|
490
|
+
var now = Date.now();
|
|
491
|
+
var lastClick = clickTimestamps[xpath] || 0;
|
|
492
|
+
|
|
493
|
+
// Rage click: 3+ clicks in 1s on same element
|
|
494
|
+
if (now - lastClick < 1000) {
|
|
495
|
+
clickCounts[xpath] = (clickCounts[xpath] || 0) + 1;
|
|
496
|
+
if (clickCounts[xpath] >= 3) {
|
|
497
|
+
_enqueue('WARNING', 'Rage click on ' + (target.tagName || 'unknown'), {
|
|
498
|
+
type: 'ux_rage_click',
|
|
499
|
+
element: target.tagName,
|
|
500
|
+
id: target.id,
|
|
501
|
+
className: target.className,
|
|
502
|
+
xpath: xpath,
|
|
503
|
+
clickCount: clickCounts[xpath],
|
|
504
|
+
url: location.href,
|
|
505
|
+
});
|
|
506
|
+
clickCounts[xpath] = 0;
|
|
507
|
+
}
|
|
508
|
+
} else {
|
|
509
|
+
clickCounts[xpath] = 1;
|
|
510
|
+
}
|
|
511
|
+
clickTimestamps[xpath] = now;
|
|
512
|
+
|
|
513
|
+
// Dead click detection
|
|
514
|
+
var isInteractive = target.tagName === 'A' || target.tagName === 'BUTTON' ||
|
|
515
|
+
target.onclick !== null || target.getAttribute('role') === 'button' ||
|
|
516
|
+
target.closest('a, button, [role="button"]');
|
|
517
|
+
|
|
518
|
+
if (isInteractive) {
|
|
519
|
+
var url = location.href;
|
|
520
|
+
setTimeout(function () {
|
|
521
|
+
if (location.href === url && (!document.activeElement || document.activeElement === document.body)) {
|
|
522
|
+
_enqueue('INFO', 'Potential dead click on ' + target.tagName, {
|
|
523
|
+
type: 'ux_dead_click',
|
|
524
|
+
element: target.tagName,
|
|
525
|
+
id: target.id,
|
|
526
|
+
text: (target.textContent || '').substring(0, 50),
|
|
527
|
+
xpath: xpath,
|
|
528
|
+
url: location.href,
|
|
529
|
+
});
|
|
530
|
+
}
|
|
531
|
+
}, 500);
|
|
532
|
+
}
|
|
533
|
+
}, true);
|
|
534
|
+
|
|
535
|
+
// Track long tasks (> 50ms)
|
|
536
|
+
if (typeof PerformanceObserver !== 'undefined') {
|
|
537
|
+
try {
|
|
538
|
+
new PerformanceObserver(function (list) {
|
|
539
|
+
list.getEntries().forEach(function (entry) {
|
|
540
|
+
if (entry.duration > 100) {
|
|
541
|
+
_enqueue('WARNING', 'Long task: ' + Math.round(entry.duration) + 'ms', {
|
|
542
|
+
type: 'ux_long_task',
|
|
543
|
+
duration: Math.round(entry.duration),
|
|
544
|
+
url: location.href,
|
|
545
|
+
});
|
|
546
|
+
}
|
|
547
|
+
});
|
|
548
|
+
}).observe({ type: 'longtask', buffered: true });
|
|
549
|
+
} catch (e) { /* ignore */ }
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
554
|
+
// CSP VIOLATION CAPTURE
|
|
555
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
556
|
+
|
|
557
|
+
function _installCSPCapture() {
|
|
558
|
+
document.addEventListener('securitypolicyviolation', function (event) {
|
|
559
|
+
_enqueue('WARNING', 'CSP violation: ' + event.violatedDirective, {
|
|
560
|
+
type: 'csp_violation',
|
|
561
|
+
violatedDirective: event.violatedDirective,
|
|
562
|
+
blockedURI: event.blockedURI,
|
|
563
|
+
originalPolicy: event.originalPolicy,
|
|
564
|
+
sourceFile: event.sourceFile,
|
|
565
|
+
lineNumber: event.lineNumber,
|
|
566
|
+
url: location.href,
|
|
567
|
+
});
|
|
568
|
+
});
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
572
|
+
// NAVIGATION CAPTURE — SPA route changes
|
|
573
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
574
|
+
|
|
575
|
+
function _installNavigationCapture() {
|
|
576
|
+
// History API
|
|
577
|
+
var origPushState = history.pushState;
|
|
578
|
+
var origReplaceState = history.replaceState;
|
|
579
|
+
|
|
580
|
+
history.pushState = function () {
|
|
581
|
+
origPushState.apply(this, arguments);
|
|
582
|
+
_onNavigation('pushState');
|
|
583
|
+
};
|
|
584
|
+
|
|
585
|
+
history.replaceState = function () {
|
|
586
|
+
origReplaceState.apply(this, arguments);
|
|
587
|
+
_onNavigation('replaceState');
|
|
588
|
+
};
|
|
589
|
+
|
|
590
|
+
window.addEventListener('popstate', function () {
|
|
591
|
+
_onNavigation('popstate');
|
|
592
|
+
});
|
|
593
|
+
|
|
594
|
+
window.addEventListener('hashchange', function () {
|
|
595
|
+
_onNavigation('hashchange');
|
|
596
|
+
});
|
|
597
|
+
|
|
598
|
+
// Online/offline
|
|
599
|
+
window.addEventListener('online', function () {
|
|
600
|
+
_enqueue('INFO', 'Network came online', { type: 'connectivity_online' });
|
|
601
|
+
});
|
|
602
|
+
window.addEventListener('offline', function () {
|
|
603
|
+
_enqueue('WARNING', 'Network went offline', { type: 'connectivity_offline' });
|
|
604
|
+
});
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
function _onNavigation(trigger) {
|
|
608
|
+
_enqueue('INFO', 'Navigation: ' + location.href, {
|
|
609
|
+
type: 'navigation',
|
|
610
|
+
trigger: trigger,
|
|
611
|
+
url: location.href,
|
|
612
|
+
title: document.title,
|
|
613
|
+
});
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
617
|
+
// DATA SANITIZATION
|
|
618
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
619
|
+
|
|
620
|
+
function _sanitize(data) {
|
|
621
|
+
if (!_config.sanitize) return data;
|
|
622
|
+
if (data === null || data === undefined) return data;
|
|
623
|
+
if (typeof data === 'string') return _sanitizeString(data);
|
|
624
|
+
if (typeof data === 'number' || typeof data === 'boolean') return data;
|
|
625
|
+
if (data instanceof Error) return { name: data.name, message: _sanitizeString(data.message), stack: data.stack ? _sanitizeString(data.stack) : undefined };
|
|
626
|
+
if (Array.isArray(data)) return data.map(_sanitize);
|
|
627
|
+
if (typeof data === 'object') {
|
|
628
|
+
var out = {};
|
|
629
|
+
for (var key in data) {
|
|
630
|
+
if (!data.hasOwnProperty(key)) continue;
|
|
631
|
+
if (_isSensitiveKey(key.toLowerCase())) { out[key] = '[REDACTED]'; }
|
|
632
|
+
else { try { out[key] = _sanitize(data[key]); } catch (e) { out[key] = '[Unserializable]'; } }
|
|
633
|
+
}
|
|
634
|
+
return out;
|
|
635
|
+
}
|
|
636
|
+
return data;
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
function _isSensitiveKey(k) {
|
|
640
|
+
return /password|passwd|pwd|secret|token|auth|apikey|api_key|access_key|private_key|credential|credit|card|ssn|social|cookie|session_id/.test(k);
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
function _sanitizeString(str) {
|
|
644
|
+
if (typeof str !== 'string') return str;
|
|
645
|
+
return str
|
|
646
|
+
.replace(/\b(password|passwd|pwd|secret|pass)\s*[:=]\s*["']?([^\s"',;}\]]+)["']?/gi, '$1=[REDACTED]')
|
|
647
|
+
.replace(/\b(Bearer)\s+[A-Za-z0-9\-._~+\/]+=*/gi, '$1 [REDACTED]')
|
|
648
|
+
.replace(/\beyJ[A-Za-z0-9\-_=]+\.eyJ[A-Za-z0-9\-_=]+\.[A-Za-z0-9\-_.+\/=]*/g, '[REDACTED_JWT]')
|
|
649
|
+
.replace(/\b(sk_|pk_|api_|key_|token_)[A-Za-z0-9]{16,}/gi, '[REDACTED_KEY]')
|
|
650
|
+
.replace(/\b(AKIA|ASIA)[A-Z0-9]{16}/g, '[REDACTED_AWS]')
|
|
651
|
+
.replace(/\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\b/g, '[REDACTED_EMAIL]')
|
|
652
|
+
.replace(/(\+\d{1,3}[-.\s]?)?\(?\d{3}\)?[-.\s]?\d{3}[-.\s]?\d{4}/g, '[REDACTED_PHONE]')
|
|
653
|
+
.replace(/\b\d{4}[\s-]?\d{4}[\s-]?\d{4}[\s-]?\d{4}\b/g, '[REDACTED_CC]')
|
|
654
|
+
.replace(/\b\d{3}-\d{2}-\d{4}\b/g, '[REDACTED_SSN]')
|
|
655
|
+
.replace(/([?&])(session|sess|sid|token|auth|key|apikey)=([^&\s]+)/gi, '$1$2=[REDACTED]')
|
|
656
|
+
.replace(/(Authorization|X-Auth-Token|X-API-Key)\s*:\s*([^\s,;]+)/gi, '$1: [REDACTED]');
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
660
|
+
// ENCRYPTION — AES-256-GCM (uses Web Crypto API)
|
|
661
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
662
|
+
|
|
663
|
+
function _encrypt(plaintext) {
|
|
664
|
+
if (!_config.encryptPayload || !_config.encryptionKey || !window.crypto?.subtle) {
|
|
665
|
+
return Promise.resolve(plaintext);
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
var keyBytes = _hexToBytes(_config.encryptionKey);
|
|
669
|
+
var iv = window.crypto.getRandomValues(new Uint8Array(12));
|
|
670
|
+
var data = new TextEncoder().encode(plaintext);
|
|
671
|
+
|
|
672
|
+
return window.crypto.subtle.importKey('raw', keyBytes, { name: 'AES-GCM' }, false, ['encrypt'])
|
|
673
|
+
.then(function (key) {
|
|
674
|
+
return window.crypto.subtle.encrypt({ name: 'AES-GCM', iv: iv }, key, data);
|
|
675
|
+
})
|
|
676
|
+
.then(function (encrypted) {
|
|
677
|
+
return JSON.stringify({
|
|
678
|
+
encrypted: true,
|
|
679
|
+
algorithm: 'aes-256-gcm',
|
|
680
|
+
iv: _bytesToBase64(iv),
|
|
681
|
+
data: _bytesToBase64(new Uint8Array(encrypted)),
|
|
682
|
+
});
|
|
683
|
+
})
|
|
684
|
+
.catch(function () { return plaintext; });
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
688
|
+
// TRANSPORT
|
|
689
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
690
|
+
|
|
691
|
+
function _enqueue(level, message, metadata) {
|
|
692
|
+
if (!_initialized || _sending) return;
|
|
693
|
+
|
|
694
|
+
var sanitizedMessage = _sanitize(message);
|
|
695
|
+
var sanitizedMeta = _sanitize(metadata || {});
|
|
696
|
+
|
|
697
|
+
var entry = {
|
|
698
|
+
timestamp: new Date().toISOString(),
|
|
699
|
+
level: level,
|
|
700
|
+
message: typeof sanitizedMessage === 'string' ? sanitizedMessage : JSON.stringify(sanitizedMessage),
|
|
701
|
+
service: _config.serviceName,
|
|
702
|
+
environment: _config.environment,
|
|
703
|
+
raw_log: typeof sanitizedMessage === 'string' ? sanitizedMessage : JSON.stringify(sanitizedMessage),
|
|
704
|
+
metadata: _assign({}, sanitizedMeta, {
|
|
705
|
+
sdkLanguage: 'browser',
|
|
706
|
+
sdkVersion: '1.0.0',
|
|
707
|
+
userAgent: navigator.userAgent,
|
|
708
|
+
url: location.href,
|
|
709
|
+
}),
|
|
710
|
+
};
|
|
711
|
+
|
|
712
|
+
if (sanitizedMeta.error && sanitizedMeta.error.stack) {
|
|
713
|
+
entry.message += '\n\nStack trace:\n' + sanitizedMeta.error.stack;
|
|
714
|
+
entry.raw_log = entry.message;
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
_buffer.push(entry);
|
|
718
|
+
|
|
719
|
+
if (_buffer.length > _config.maxBufferSize) {
|
|
720
|
+
_buffer = _buffer.slice(_buffer.length - _config.maxBufferSize);
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
if (level === 'ERROR' || _buffer.length >= _config.batchSize) {
|
|
724
|
+
_flush();
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
function _flush() {
|
|
729
|
+
if (_buffer.length === 0) return;
|
|
730
|
+
|
|
731
|
+
var logs = _buffer.splice(0, _config.batchSize);
|
|
732
|
+
var body = JSON.stringify({
|
|
733
|
+
api_key: _config.apiKey,
|
|
734
|
+
source_type: 'application',
|
|
735
|
+
source_description: _config.serviceName + ' (browser)',
|
|
736
|
+
logs: logs,
|
|
737
|
+
});
|
|
738
|
+
|
|
739
|
+
if (_config.encryptPayload) {
|
|
740
|
+
_encrypt(body).then(function (payload) { _send(payload); });
|
|
741
|
+
} else {
|
|
742
|
+
_send(body);
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
function _send(body) {
|
|
747
|
+
_sending = true;
|
|
748
|
+
var url = _config.baseUrl + INGEST_ENDPOINT;
|
|
749
|
+
|
|
750
|
+
// Use sendBeacon for unload, fetch otherwise
|
|
751
|
+
if (document.visibilityState === 'hidden' && navigator.sendBeacon) {
|
|
752
|
+
try { navigator.sendBeacon(url, body); } catch (e) { /* ignore */ }
|
|
753
|
+
_sending = false;
|
|
754
|
+
return;
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
// Use original fetch to avoid re-interception
|
|
758
|
+
var fetchFn = _originals._fetch || window.fetch;
|
|
759
|
+
fetchFn(url, {
|
|
760
|
+
method: 'POST',
|
|
761
|
+
headers: {
|
|
762
|
+
'Content-Type': 'application/json',
|
|
763
|
+
'X-Overcast-Key': _config.apiKey,
|
|
764
|
+
'X-Overcast-Service': _config.serviceName,
|
|
765
|
+
'X-Overcast-SDK': 'browser/1.0.0',
|
|
766
|
+
},
|
|
767
|
+
body: body,
|
|
768
|
+
keepalive: true,
|
|
769
|
+
}).catch(function () { /* silent fail */ }).finally(function () { _sending = false; });
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
773
|
+
// HELPERS
|
|
774
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
775
|
+
|
|
776
|
+
function _formatArgs(args) {
|
|
777
|
+
var errorObj = null;
|
|
778
|
+
var parts = [];
|
|
779
|
+
for (var i = 0; i < args.length; i++) {
|
|
780
|
+
if (args[i] instanceof Error) { if (!errorObj) errorObj = args[i]; parts.push(args[i].message); }
|
|
781
|
+
else if (typeof args[i] === 'object') { try { parts.push(JSON.stringify(args[i])); } catch (e) { parts.push('[Object]'); } }
|
|
782
|
+
else { parts.push(String(args[i])); }
|
|
783
|
+
}
|
|
784
|
+
return { message: parts.join(' '), errorObj: errorObj };
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
function _getXPath(el) {
|
|
788
|
+
if (!el) return '';
|
|
789
|
+
if (el.id) return '//*[@id="' + el.id + '"]';
|
|
790
|
+
if (el === document.body) return '/html/body';
|
|
791
|
+
var path = '';
|
|
792
|
+
while (el && el.nodeType === 1) {
|
|
793
|
+
var idx = 0, sib = el.previousSibling;
|
|
794
|
+
while (sib) { if (sib.nodeType === 1 && sib.nodeName === el.nodeName) idx++; sib = sib.previousSibling; }
|
|
795
|
+
path = '/' + el.nodeName.toLowerCase() + (idx ? '[' + (idx + 1) + ']' : '') + path;
|
|
796
|
+
el = el.parentNode;
|
|
797
|
+
}
|
|
798
|
+
return path;
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
function _assign(target) {
|
|
802
|
+
for (var i = 1; i < arguments.length; i++) {
|
|
803
|
+
var src = arguments[i];
|
|
804
|
+
if (src) { for (var k in src) { if (src.hasOwnProperty(k)) target[k] = src[k]; } }
|
|
805
|
+
}
|
|
806
|
+
return target;
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
function _hexToBytes(hex) {
|
|
810
|
+
var bytes = new Uint8Array(hex.length / 2);
|
|
811
|
+
for (var i = 0; i < hex.length; i += 2) bytes[i / 2] = parseInt(hex.substr(i, 2), 16);
|
|
812
|
+
return bytes;
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
function _bytesToBase64(bytes) {
|
|
816
|
+
var bin = '';
|
|
817
|
+
for (var i = 0; i < bytes.length; i++) bin += String.fromCharCode(bytes[i]);
|
|
818
|
+
return btoa(bin);
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
822
|
+
// PUBLIC INTERFACE
|
|
823
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
824
|
+
|
|
825
|
+
var Overcast = {
|
|
826
|
+
init: init,
|
|
827
|
+
error: function (msg, meta) { _enqueue('ERROR', msg, _assign({ type: 'manual' }, meta)); },
|
|
828
|
+
warn: function (msg, meta) { _enqueue('WARNING', msg, _assign({ type: 'manual' }, meta)); },
|
|
829
|
+
info: function (msg, meta) { _enqueue('INFO', msg, _assign({ type: 'manual' }, meta)); },
|
|
830
|
+
debug: function (msg, meta) { _enqueue('DEBUG', msg, _assign({ type: 'manual' }, meta)); },
|
|
831
|
+
captureException: function (err, meta) {
|
|
832
|
+
if (!(err instanceof Error)) err = new Error(String(err));
|
|
833
|
+
_enqueue('ERROR', err.message, _assign({ type: 'captured_exception', error: { name: err.name, message: err.message, stack: err.stack } }, meta));
|
|
834
|
+
},
|
|
835
|
+
flush: _flush,
|
|
836
|
+
getBufferSize: function () { return _buffer.length; },
|
|
837
|
+
};
|
|
838
|
+
|
|
839
|
+
// Save original fetch before we wrap it
|
|
840
|
+
if (typeof window !== 'undefined' && typeof window.fetch === 'function') {
|
|
841
|
+
_originals._fetch = window.fetch.bind(window);
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
return Overcast;
|
|
845
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@overcastsre/browser",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Overcast SRE monitoring SDK for browsers — captures everything client-side.",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"module": "index.mjs",
|
|
7
|
+
"types": "index.d.ts",
|
|
8
|
+
"license": "MIT",
|
|
9
|
+
"keywords": ["overcast", "monitoring", "error-tracking", "browser", "frontend", "observability"],
|
|
10
|
+
"files": ["index.js", "index.mjs", "index.d.ts", "README.md"],
|
|
11
|
+
"repository": { "type": "git", "url": "https://github.com/overcast/sdk-browser" },
|
|
12
|
+
"homepage": "https://overcastsre.com/docs/browser"
|
|
13
|
+
}
|