@obtrace/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.
Files changed (51) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +85 -0
  3. package/dist/auto.js +126 -0
  4. package/dist/browser/breadcrumbs.js +41 -0
  5. package/dist/browser/clicks.js +68 -0
  6. package/dist/browser/console.js +58 -0
  7. package/dist/browser/errors.js +51 -0
  8. package/dist/browser/index.js +352 -0
  9. package/dist/browser/longtasks.js +24 -0
  10. package/dist/browser/memory.js +19 -0
  11. package/dist/browser/offline.js +115 -0
  12. package/dist/browser/replay.js +127 -0
  13. package/dist/browser/resources.js +24 -0
  14. package/dist/browser/vitals.js +81 -0
  15. package/dist/browser_entry.bundle.js +28780 -0
  16. package/dist/browser_entry.bundle.js.map +7 -0
  17. package/dist/browser_entry.js +3 -0
  18. package/dist/core/client.js +80 -0
  19. package/dist/core/otel-web-setup.js +93 -0
  20. package/dist/index.js +6 -0
  21. package/dist/shared/semantic_metrics.js +28 -0
  22. package/dist/shared/types.js +1 -0
  23. package/dist/shared/utils.js +112 -0
  24. package/dist/sourcemaps.js +32 -0
  25. package/dist/types/auto.d.ts +13 -0
  26. package/dist/types/browser/breadcrumbs.d.ts +12 -0
  27. package/dist/types/browser/clicks.d.ts +2 -0
  28. package/dist/types/browser/console.d.ts +2 -0
  29. package/dist/types/browser/errors.d.ts +2 -0
  30. package/dist/types/browser/index.d.ts +25 -0
  31. package/dist/types/browser/longtasks.d.ts +2 -0
  32. package/dist/types/browser/memory.d.ts +2 -0
  33. package/dist/types/browser/offline.d.ts +3 -0
  34. package/dist/types/browser/replay.d.ts +24 -0
  35. package/dist/types/browser/resources.d.ts +2 -0
  36. package/dist/types/browser/vitals.d.ts +2 -0
  37. package/dist/types/browser_entry.d.ts +6 -0
  38. package/dist/types/core/client.d.ts +19 -0
  39. package/dist/types/core/otel-web-setup.d.ts +8 -0
  40. package/dist/types/index.d.ts +9 -0
  41. package/dist/types/shared/semantic_metrics.d.ts +26 -0
  42. package/dist/types/shared/types.d.ts +104 -0
  43. package/dist/types/shared/utils.d.ts +21 -0
  44. package/dist/types/sourcemaps.d.ts +14 -0
  45. package/dist/types/wrappers/frontend/next.d.ts +4 -0
  46. package/dist/types/wrappers/frontend/react.d.ts +8 -0
  47. package/dist/types/wrappers/frontend/vite.d.ts +5 -0
  48. package/dist/wrappers/frontend/next.js +12 -0
  49. package/dist/wrappers/frontend/react.js +20 -0
  50. package/dist/wrappers/frontend/vite.js +19 -0
  51. package/package.json +82 -0
@@ -0,0 +1,352 @@
1
+ import { record } from "rrweb";
2
+ import { SpanStatusCode } from "@opentelemetry/api";
3
+ import { ObtraceClient } from "../core/client";
4
+ import { setupOtelWeb } from "../core/otel-web-setup";
5
+ import { installBrowserErrorHooks } from "./errors";
6
+ import { BrowserReplayBuffer } from "./replay";
7
+ import { installWebVitals } from "./vitals";
8
+ import { addBreadcrumb as addCrumb, getBreadcrumbs, installClickBreadcrumbs } from "./breadcrumbs";
9
+ import { installConsoleCapture } from "./console";
10
+ import { installClickTracking } from "./clicks";
11
+ import { installResourceTiming } from "./resources";
12
+ import { installLongTaskDetection } from "./longtasks";
13
+ import { installMemoryTracking } from "./memory";
14
+ import { installOfflineSupport } from "./offline";
15
+ const instances = new Set();
16
+ const replayBuffers = new Set();
17
+ let currentUser = null;
18
+ let navigationPatched = false;
19
+ let originalPushState = null;
20
+ let originalReplaceState = null;
21
+ let navigationPopstateHandler = null;
22
+ let navigationHashchangeHandler = null;
23
+ let rrwebRecording = false;
24
+ let stopRrwebRecording = null;
25
+ function fanOutNavigation() {
26
+ addCrumb({ timestamp: Date.now(), category: "navigation", message: window.location.href, level: "info" });
27
+ for (const entry of instances) {
28
+ const chunk = entry.replay.pushCustomEvent("navigation", {
29
+ href: window.location.href,
30
+ title: document.title,
31
+ });
32
+ if (chunk) {
33
+ entry.client.replayChunk(chunk);
34
+ }
35
+ }
36
+ }
37
+ function installSharedNavigationTracker() {
38
+ if (navigationPatched || typeof window === "undefined")
39
+ return;
40
+ navigationPatched = true;
41
+ const historyRef = window.history;
42
+ originalPushState = historyRef.pushState.bind(historyRef);
43
+ originalReplaceState = historyRef.replaceState.bind(historyRef);
44
+ const rawPush = originalPushState;
45
+ const rawReplace = originalReplaceState;
46
+ historyRef.pushState = ((...args) => {
47
+ rawPush(...args);
48
+ fanOutNavigation();
49
+ });
50
+ historyRef.replaceState = ((...args) => {
51
+ rawReplace(...args);
52
+ fanOutNavigation();
53
+ });
54
+ navigationPopstateHandler = fanOutNavigation;
55
+ navigationHashchangeHandler = fanOutNavigation;
56
+ window.addEventListener("popstate", navigationPopstateHandler);
57
+ window.addEventListener("hashchange", navigationHashchangeHandler);
58
+ }
59
+ function teardownSharedNavigationTracker() {
60
+ if (!navigationPatched || typeof window === "undefined")
61
+ return;
62
+ if (originalPushState)
63
+ window.history.pushState = originalPushState;
64
+ if (originalReplaceState)
65
+ window.history.replaceState = originalReplaceState;
66
+ if (navigationPopstateHandler)
67
+ window.removeEventListener("popstate", navigationPopstateHandler);
68
+ if (navigationHashchangeHandler)
69
+ window.removeEventListener("hashchange", navigationHashchangeHandler);
70
+ originalPushState = null;
71
+ originalReplaceState = null;
72
+ navigationPopstateHandler = null;
73
+ navigationHashchangeHandler = null;
74
+ navigationPatched = false;
75
+ }
76
+ function installSharedRrwebRecording(config) {
77
+ if (rrwebRecording || typeof window === "undefined")
78
+ return;
79
+ rrwebRecording = true;
80
+ const replayCfg = config.replay ?? { enabled: true };
81
+ const stop = record({
82
+ emit(event) {
83
+ for (const buf of replayBuffers) {
84
+ const chunk = buf.pushRrwebEvent(event);
85
+ if (chunk) {
86
+ for (const entry of instances) {
87
+ if (entry.replay === buf) {
88
+ entry.client.replayChunk(chunk);
89
+ break;
90
+ }
91
+ }
92
+ }
93
+ }
94
+ },
95
+ maskAllInputs: replayCfg.maskAllInputs ?? true,
96
+ maskInputOptions: { password: true, email: true, tel: true },
97
+ maskInputFn: (text, element) => {
98
+ const el = element;
99
+ const n = (el?.name || "").toLowerCase();
100
+ const id = (el?.id || "").toLowerCase();
101
+ if (/(pass|token|secret|key|email|cpf|ssn|credit|card)/.test(`${n} ${id}`))
102
+ return "[redacted]";
103
+ return text;
104
+ },
105
+ blockClass: replayCfg.blockClass ?? "ob-block",
106
+ maskTextClass: replayCfg.maskTextClass ?? "ob-mask",
107
+ inlineStylesheet: true,
108
+ collectFonts: false,
109
+ sampling: {
110
+ mousemove: replayCfg.sampling?.mousemove ?? false,
111
+ mouseInteraction: true,
112
+ scroll: replayCfg.sampling?.scroll ?? 150,
113
+ input: replayCfg.sampling?.input ?? "last",
114
+ },
115
+ });
116
+ if (stop)
117
+ stopRrwebRecording = stop;
118
+ }
119
+ function teardownSharedRrwebRecording() {
120
+ if (!rrwebRecording)
121
+ return;
122
+ if (stopRrwebRecording) {
123
+ stopRrwebRecording();
124
+ stopRrwebRecording = null;
125
+ }
126
+ rrwebRecording = false;
127
+ }
128
+ function severityToNumber(level) {
129
+ switch (level) {
130
+ case "debug": return 5;
131
+ case "info": return 9;
132
+ case "warn": return 13;
133
+ case "error": return 17;
134
+ case "fatal": return 21;
135
+ default: return 9;
136
+ }
137
+ }
138
+ function userAttrs() {
139
+ if (!currentUser)
140
+ return {};
141
+ const attrs = {};
142
+ if (currentUser.id)
143
+ attrs["user.id"] = String(currentUser.id);
144
+ if (currentUser.email)
145
+ attrs["user.email"] = String(currentUser.email);
146
+ if (currentUser.name)
147
+ attrs["user.name"] = String(currentUser.name);
148
+ return attrs;
149
+ }
150
+ export function initBrowserSDK(config) {
151
+ for (const entry of instances) {
152
+ if (entry.config.apiKey === config.apiKey && entry.config.ingestBaseUrl === config.ingestBaseUrl && entry.config.serviceName === config.serviceName) {
153
+ return entry.sdk;
154
+ }
155
+ }
156
+ const sampleRate = config.tracesSampleRate ?? 1;
157
+ const replaySampleRate = config.replaySampleRate ?? 1;
158
+ const shouldReplay = Math.random() < replaySampleRate;
159
+ const otel = setupOtelWeb({ ...config, tracesSampleRate: sampleRate });
160
+ const tracer = otel.tracer;
161
+ const meter = otel.meter;
162
+ const client = new ObtraceClient({
163
+ ...config,
164
+ replay: {
165
+ enabled: shouldReplay,
166
+ flushIntervalMs: 5000,
167
+ maxChunkBytes: 480_000,
168
+ captureNetworkRecipes: true,
169
+ sessionStorageKey: "obtrace_session_id",
170
+ ...config.replay,
171
+ },
172
+ vitals: { enabled: true, reportAllChanges: false, ...config.vitals },
173
+ propagation: { enabled: true, ...config.propagation },
174
+ });
175
+ const replay = new BrowserReplayBuffer({
176
+ maxChunkBytes: config.replay?.maxChunkBytes ?? 480_000,
177
+ flushIntervalMs: config.replay?.flushIntervalMs ?? 5000,
178
+ sessionStorageKey: config.replay?.sessionStorageKey ?? "obtrace_session_id",
179
+ });
180
+ const recipeSteps = [];
181
+ const cleanups = [];
182
+ const entry = { client, sessionId: replay.sessionId, replay, config, recipeSteps, otel };
183
+ instances.add(entry);
184
+ replayBuffers.add(replay);
185
+ const base = config.ingestBaseUrl?.replace(/\/$/, "") ?? "";
186
+ fetch(`${base}/v1/init`, {
187
+ method: "POST",
188
+ headers: {
189
+ "Content-Type": "application/json",
190
+ Authorization: `Bearer ${config.apiKey}`,
191
+ },
192
+ body: JSON.stringify({
193
+ sdk: "@obtrace/browser",
194
+ sdk_version: "1.0.0",
195
+ service_name: config.serviceName,
196
+ service_version: config.serviceVersion ?? "",
197
+ runtime: "browser",
198
+ runtime_version: typeof navigator !== "undefined" ? navigator.userAgent : "",
199
+ }),
200
+ signal: AbortSignal.timeout(5000),
201
+ }).then((res) => {
202
+ if (res.ok && config.debug)
203
+ console.log("[obtrace-sdk-browser] init handshake OK");
204
+ if (!res.ok)
205
+ console.error(`[obtrace-sdk-browser] init handshake failed: ${res.status}`);
206
+ }).catch(() => { });
207
+ if (config.vitals?.enabled !== false)
208
+ cleanups.push(installWebVitals(meter, !!config.vitals?.reportAllChanges));
209
+ cleanups.push(installBrowserErrorHooks(tracer, replay.sessionId));
210
+ cleanups.push(installClickBreadcrumbs());
211
+ cleanups.push(installClickTracking(tracer, replay.sessionId));
212
+ cleanups.push(installResourceTiming(meter));
213
+ cleanups.push(installLongTaskDetection(tracer));
214
+ cleanups.push(installMemoryTracking(meter));
215
+ if (config.captureConsole !== false) {
216
+ cleanups.push(installConsoleCapture(tracer, replay.sessionId));
217
+ }
218
+ cleanups.push(installOfflineSupport());
219
+ if (shouldReplay && config.replay?.enabled !== false && typeof window !== "undefined") {
220
+ installSharedRrwebRecording(config);
221
+ installSharedNavigationTracker();
222
+ }
223
+ let pendingBeaconBlob = null;
224
+ client.replayTimer = setInterval(() => {
225
+ const chunk = replay.flush();
226
+ if (chunk) {
227
+ const json = JSON.stringify(chunk);
228
+ pendingBeaconBlob = new Blob([json], { type: "application/json" });
229
+ client.replayChunk(chunk);
230
+ }
231
+ else {
232
+ pendingBeaconBlob = null;
233
+ }
234
+ }, config.replay?.flushIntervalMs ?? 5000);
235
+ const sendViaBeacon = () => {
236
+ const url = `${config.ingestBaseUrl?.replace(/\/$/, "")}/ingest/replay/chunk`;
237
+ const freshChunk = replay.flush();
238
+ if (freshChunk) {
239
+ navigator.sendBeacon(url, new Blob([JSON.stringify(freshChunk)], { type: "application/json" }));
240
+ pendingBeaconBlob = null;
241
+ }
242
+ else if (pendingBeaconBlob) {
243
+ navigator.sendBeacon(url, pendingBeaconBlob);
244
+ pendingBeaconBlob = null;
245
+ }
246
+ };
247
+ const onVisibility = () => { if (document.visibilityState === "hidden")
248
+ flushReplay(); };
249
+ const onBeforeUnload = () => sendViaBeacon();
250
+ if (typeof document !== "undefined") {
251
+ document.addEventListener("visibilitychange", onVisibility);
252
+ cleanups.push(() => document.removeEventListener("visibilitychange", onVisibility));
253
+ }
254
+ if (typeof window !== "undefined") {
255
+ window.addEventListener("beforeunload", onBeforeUnload);
256
+ cleanups.push(() => window.removeEventListener("beforeunload", onBeforeUnload));
257
+ }
258
+ const log = (level, message, context) => {
259
+ addCrumb({ timestamp: Date.now(), category: "log", message, level: level === "fatal" ? "error" : level });
260
+ const span = tracer.startSpan("browser.log", {
261
+ attributes: {
262
+ "log.severity": level.toUpperCase(),
263
+ "log.severity_number": severityToNumber(level),
264
+ "log.message": message,
265
+ "session.id": replay.sessionId,
266
+ ...userAttrs(),
267
+ ...(context?.traceId ? { "obtrace.trace_id": context.traceId } : {}),
268
+ ...context?.attrs,
269
+ },
270
+ });
271
+ if (level === "error" || level === "fatal")
272
+ span.setStatus({ code: SpanStatusCode.ERROR, message });
273
+ span.end();
274
+ };
275
+ const metricFn = (name, value, unit, context) => {
276
+ const gauge = meter.createGauge(name, { unit: unit ?? "1" });
277
+ gauge.record(value, { ...userAttrs(), ...context?.attrs });
278
+ };
279
+ const captureException = (error, context) => {
280
+ const msg = error instanceof Error ? `${error.name}: ${error.message}` : String(error);
281
+ const breadcrumbs = getBreadcrumbs();
282
+ addCrumb({ timestamp: Date.now(), category: "error", message: msg, level: "error" });
283
+ const span = tracer.startSpan("browser.exception", {
284
+ attributes: {
285
+ "error.message": msg,
286
+ "session.id": replay.sessionId,
287
+ "breadcrumbs.count": breadcrumbs.length,
288
+ "breadcrumbs.json": JSON.stringify(breadcrumbs.slice(-20)),
289
+ ...userAttrs(),
290
+ ...context?.attrs,
291
+ },
292
+ });
293
+ span.setStatus({ code: SpanStatusCode.ERROR, message: msg });
294
+ if (error instanceof Error)
295
+ span.recordException(error);
296
+ span.end();
297
+ };
298
+ const captureReplayEvent = (type, payload) => {
299
+ const chunk = replay.pushCustomEvent(type, payload);
300
+ if (chunk)
301
+ client.replayChunk(chunk);
302
+ };
303
+ const flushReplay = () => {
304
+ const chunk = replay.flush();
305
+ if (chunk)
306
+ client.replayChunk(chunk);
307
+ if (recipeSteps.length)
308
+ client.replayRecipes(recipeSteps.splice(0, recipeSteps.length));
309
+ };
310
+ const captureRecipe = (step) => {
311
+ recipeSteps.push(step);
312
+ if (recipeSteps.length >= 50)
313
+ client.replayRecipes(recipeSteps.splice(0, recipeSteps.length));
314
+ };
315
+ const instrumentFetch = () => (input, init) => fetch(input, init);
316
+ const setUser = (user) => {
317
+ currentUser = user;
318
+ addCrumb({ timestamp: Date.now(), category: "auth", message: `setUser: ${user.id || user.email || "anonymous"}`, level: "info" });
319
+ };
320
+ const shutdown = async () => {
321
+ if (client.replayTimer) {
322
+ clearInterval(client.replayTimer);
323
+ client.replayTimer = null;
324
+ }
325
+ await otel.shutdown();
326
+ try {
327
+ flushReplay();
328
+ }
329
+ catch { }
330
+ for (const c of cleanups) {
331
+ try {
332
+ c();
333
+ }
334
+ catch { }
335
+ }
336
+ instances.delete(entry);
337
+ replayBuffers.delete(replay);
338
+ if (instances.size === 0) {
339
+ teardownSharedNavigationTracker();
340
+ teardownSharedRrwebRecording();
341
+ }
342
+ await client.shutdown();
343
+ };
344
+ const sdk = {
345
+ client, sessionId: replay.sessionId, log, metric: metricFn,
346
+ captureException, captureError: captureException,
347
+ captureReplayEvent, flushReplay, captureRecipe, instrumentFetch,
348
+ shutdown, setUser, addBreadcrumb: addCrumb,
349
+ };
350
+ entry.sdk = sdk;
351
+ return sdk;
352
+ }
@@ -0,0 +1,24 @@
1
+ export function installLongTaskDetection(tracer) {
2
+ if (typeof window === "undefined" || typeof PerformanceObserver === "undefined")
3
+ return () => { };
4
+ const observer = new PerformanceObserver((list) => {
5
+ for (const entry of list.getEntries()) {
6
+ if (entry.duration < 50)
7
+ continue;
8
+ const span = tracer.startSpan("browser.longtask", {
9
+ attributes: {
10
+ "longtask.duration_ms": entry.duration,
11
+ "longtask.name": entry.name,
12
+ },
13
+ });
14
+ span.end();
15
+ }
16
+ });
17
+ try {
18
+ observer.observe({ type: "longtask", buffered: true });
19
+ }
20
+ catch {
21
+ return () => { };
22
+ }
23
+ return () => observer.disconnect();
24
+ }
@@ -0,0 +1,19 @@
1
+ export function installMemoryTracking(meter) {
2
+ if (typeof window === "undefined")
3
+ return () => { };
4
+ const perf = performance;
5
+ if (!perf.memory)
6
+ return () => { };
7
+ const usedGauge = meter.createGauge("browser.memory.used_js_heap", { unit: "By" });
8
+ const totalGauge = meter.createGauge("browser.memory.total_js_heap", { unit: "By" });
9
+ const limitGauge = meter.createGauge("browser.memory.heap_limit", { unit: "By" });
10
+ const timer = setInterval(() => {
11
+ const mem = perf.memory;
12
+ if (!mem)
13
+ return;
14
+ usedGauge.record(mem.usedJSHeapSize);
15
+ totalGauge.record(mem.totalJSHeapSize);
16
+ limitGauge.record(mem.jsHeapSizeLimit);
17
+ }, 30000);
18
+ return () => clearInterval(timer);
19
+ }
@@ -0,0 +1,115 @@
1
+ const DB_NAME = "obtrace_offline";
2
+ const STORE_NAME = "queue";
3
+ const MAX_ENTRIES = 500;
4
+ const MAX_BYTES = 5 * 1024 * 1024;
5
+ let db = null;
6
+ let draining = false;
7
+ function openDB() {
8
+ if (db)
9
+ return Promise.resolve(db);
10
+ return new Promise((resolve, reject) => {
11
+ const req = indexedDB.open(DB_NAME, 1);
12
+ req.onupgradeneeded = () => {
13
+ const store = req.result.createObjectStore(STORE_NAME, { keyPath: "id", autoIncrement: true });
14
+ store.createIndex("ts", "ts");
15
+ };
16
+ req.onsuccess = () => { db = req.result; resolve(db); };
17
+ req.onerror = () => reject(req.error);
18
+ });
19
+ }
20
+ async function countAndSize() {
21
+ const idb = await openDB();
22
+ return new Promise((resolve) => {
23
+ const tx = idb.transaction(STORE_NAME, "readonly");
24
+ const store = tx.objectStore(STORE_NAME);
25
+ const countReq = store.count();
26
+ let totalSize = 0;
27
+ let count = 0;
28
+ countReq.onsuccess = () => { count = countReq.result; };
29
+ const cursorReq = store.openCursor();
30
+ cursorReq.onsuccess = () => {
31
+ const cursor = cursorReq.result;
32
+ if (cursor) {
33
+ totalSize += cursor.value.payload.length;
34
+ cursor.continue();
35
+ }
36
+ else {
37
+ resolve({ count, size: totalSize });
38
+ }
39
+ };
40
+ cursorReq.onerror = () => resolve({ count, size: totalSize });
41
+ });
42
+ }
43
+ export async function enqueueOffline(url, payload, headers) {
44
+ try {
45
+ const { count, size } = await countAndSize();
46
+ if (count >= MAX_ENTRIES || size + payload.length > MAX_BYTES)
47
+ return false;
48
+ const idb = await openDB();
49
+ return new Promise((resolve) => {
50
+ const tx = idb.transaction(STORE_NAME, "readwrite");
51
+ tx.objectStore(STORE_NAME).add({ ts: Date.now(), url, payload, headers });
52
+ tx.oncomplete = () => resolve(true);
53
+ tx.onerror = () => resolve(false);
54
+ });
55
+ }
56
+ catch {
57
+ return false;
58
+ }
59
+ }
60
+ export async function drainOfflineQueue() {
61
+ if (draining || !navigator.onLine)
62
+ return;
63
+ draining = true;
64
+ try {
65
+ const idb = await openDB();
66
+ const entries = [];
67
+ await new Promise((resolve) => {
68
+ const tx = idb.transaction(STORE_NAME, "readonly");
69
+ const cursorReq = tx.objectStore(STORE_NAME).openCursor();
70
+ cursorReq.onsuccess = () => {
71
+ const cursor = cursorReq.result;
72
+ if (cursor) {
73
+ entries.push(cursor.value);
74
+ cursor.continue();
75
+ }
76
+ else {
77
+ resolve();
78
+ }
79
+ };
80
+ cursorReq.onerror = () => resolve();
81
+ });
82
+ for (const entry of entries) {
83
+ try {
84
+ const res = await fetch(entry.url, {
85
+ method: "POST",
86
+ headers: { "Content-Type": "application/json", ...entry.headers },
87
+ body: entry.payload,
88
+ });
89
+ if (res.ok || res.status === 400) {
90
+ const tx = idb.transaction(STORE_NAME, "readwrite");
91
+ tx.objectStore(STORE_NAME).delete(entry.id);
92
+ }
93
+ else {
94
+ break;
95
+ }
96
+ }
97
+ catch {
98
+ break;
99
+ }
100
+ }
101
+ }
102
+ catch {
103
+ }
104
+ finally {
105
+ draining = false;
106
+ }
107
+ }
108
+ export function installOfflineSupport() {
109
+ if (typeof window === "undefined" || typeof indexedDB === "undefined")
110
+ return () => { };
111
+ const onOnline = () => drainOfflineQueue();
112
+ window.addEventListener("online", onOnline);
113
+ drainOfflineQueue();
114
+ return () => window.removeEventListener("online", onOnline);
115
+ }
@@ -0,0 +1,127 @@
1
+ import { EventType } from "@rrweb/types";
2
+ import { sanitizeHeaders, stripQuery, toBase64 } from "../shared/utils";
3
+ const KEY_OVERHEAD = 6;
4
+ function estimateObjectBytes(value) {
5
+ if (value === null || value === undefined)
6
+ return 4;
7
+ switch (typeof value) {
8
+ case "string":
9
+ return value.length + 2;
10
+ case "number":
11
+ case "boolean":
12
+ return 8;
13
+ case "object": {
14
+ if (Array.isArray(value)) {
15
+ let sum = 2;
16
+ for (let i = 0; i < value.length; i++) {
17
+ sum += estimateObjectBytes(value[i]) + 1;
18
+ }
19
+ return sum;
20
+ }
21
+ let sum = 2;
22
+ const keys = Object.keys(value);
23
+ for (let i = 0; i < keys.length; i++) {
24
+ sum += keys[i].length + KEY_OVERHEAD + estimateObjectBytes(value[keys[i]]);
25
+ }
26
+ return sum;
27
+ }
28
+ default:
29
+ return 4;
30
+ }
31
+ }
32
+ export class BrowserReplayBuffer {
33
+ cfg;
34
+ replayId;
35
+ seq = 0;
36
+ events = [];
37
+ bytesEstimate = 0;
38
+ chunkStartedAt = Date.now();
39
+ constructor(cfg) {
40
+ this.cfg = cfg;
41
+ this.replayId = this.resolveReplayId();
42
+ }
43
+ get sessionId() {
44
+ return this.replayId;
45
+ }
46
+ pushRrwebEvent(event) {
47
+ this.events.push(event);
48
+ this.bytesEstimate += estimateObjectBytes(event);
49
+ if (this.bytesEstimate >= this.cfg.maxChunkBytes) {
50
+ return this.flush();
51
+ }
52
+ return null;
53
+ }
54
+ pushCustomEvent(tag, payload) {
55
+ const event = {
56
+ type: EventType.Custom,
57
+ data: { tag, payload },
58
+ timestamp: Date.now(),
59
+ };
60
+ return this.pushRrwebEvent(event);
61
+ }
62
+ flush() {
63
+ if (!this.events.length) {
64
+ return null;
65
+ }
66
+ const out = {
67
+ replay_id: this.replayId,
68
+ seq: this.seq,
69
+ started_at_ms: this.chunkStartedAt,
70
+ ended_at_ms: Date.now(),
71
+ events: this.events,
72
+ };
73
+ this.seq += 1;
74
+ this.events = [];
75
+ this.bytesEstimate = 0;
76
+ this.chunkStartedAt = Date.now();
77
+ return out;
78
+ }
79
+ toRecipeStep(index, record) {
80
+ const safeReqHeaders = sanitizeHeaders(record.req_headers);
81
+ let body_b64 = "";
82
+ if (record.req_body_b64) {
83
+ body_b64 = record.req_body_b64;
84
+ }
85
+ return {
86
+ step_id: index,
87
+ method: record.method,
88
+ url_template: stripQuery(record.url),
89
+ headers: safeReqHeaders,
90
+ body_b64: body_b64 || undefined,
91
+ };
92
+ }
93
+ asNetworkEvent(record) {
94
+ return {
95
+ method: record.method,
96
+ url: stripQuery(record.url),
97
+ status: record.status,
98
+ dur_ms: record.dur_ms,
99
+ req_headers: sanitizeHeaders(record.req_headers),
100
+ res_headers: sanitizeHeaders(record.res_headers),
101
+ req_body_b64: record.req_body_b64,
102
+ res_body_b64: record.res_body_b64,
103
+ };
104
+ }
105
+ encodeBody(body) {
106
+ if (typeof body === "string") {
107
+ return toBase64(body);
108
+ }
109
+ if (body && typeof body === "object") {
110
+ return toBase64(JSON.stringify(body));
111
+ }
112
+ return undefined;
113
+ }
114
+ resolveReplayId() {
115
+ if (typeof window === "undefined") {
116
+ return `srv-${Date.now()}`;
117
+ }
118
+ const ls = window.localStorage;
119
+ const existing = ls.getItem(this.cfg.sessionStorageKey);
120
+ if (existing) {
121
+ return existing;
122
+ }
123
+ const next = `${Date.now()}-${Math.random().toString(16).slice(2, 10)}`;
124
+ ls.setItem(this.cfg.sessionStorageKey, next);
125
+ return next;
126
+ }
127
+ }
@@ -0,0 +1,24 @@
1
+ export function installResourceTiming(meter) {
2
+ if (typeof window === "undefined" || typeof PerformanceObserver === "undefined")
3
+ return () => { };
4
+ const gauge = meter.createGauge("browser.resource.duration", { unit: "ms" });
5
+ const observer = new PerformanceObserver((list) => {
6
+ for (const entry of list.getEntries()) {
7
+ const res = entry;
8
+ if (res.duration < 100)
9
+ continue;
10
+ gauge.record(res.duration, {
11
+ "resource.type": res.initiatorType || "other",
12
+ "resource.name": res.name.split("?")[0].split("/").pop() || res.name.slice(0, 80),
13
+ "resource.transfer_size": res.transferSize || 0,
14
+ });
15
+ }
16
+ });
17
+ try {
18
+ observer.observe({ type: "resource", buffered: false });
19
+ }
20
+ catch {
21
+ return () => { };
22
+ }
23
+ return () => observer.disconnect();
24
+ }