@obtrace/browser 2.5.0 → 2.5.2

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.
@@ -16,6 +16,13 @@ export function installClickTracking(tracer, sessionId) {
16
16
  return;
17
17
  const selector = getElementSelector(target);
18
18
  const now = Date.now();
19
+ addBreadcrumb({
20
+ timestamp: now,
21
+ category: "ui.click",
22
+ message: selector,
23
+ level: "info",
24
+ data: { x: ev.clientX, y: ev.clientY },
25
+ });
19
26
  recent.push({ selector, timestamp: now });
20
27
  while (recent.length > 0 && now - recent[0].timestamp > RAGE_WINDOW_MS) {
21
28
  recent.shift();
@@ -24,7 +24,7 @@ export function installBrowserErrorHooks(tracer, logger, sessionId) {
24
24
  "error.stack": stack.slice(0, 4096),
25
25
  "error.type": errorType,
26
26
  "breadcrumbs.count": breadcrumbs.length,
27
- "breadcrumbs.json": JSON.stringify(breadcrumbs.slice(-20)),
27
+ "breadcrumbs.json": JSON.stringify(breadcrumbs.slice(-5)),
28
28
  ...(sessionId ? { "session.id": sessionId } : {}),
29
29
  };
30
30
  logger.emit({
@@ -71,7 +71,7 @@ export function installBrowserErrorHooks(tracer, logger, sessionId) {
71
71
  "error.stack": stack.slice(0, 4096),
72
72
  "error.type": errorType,
73
73
  "breadcrumbs.count": breadcrumbs.length,
74
- "breadcrumbs.json": JSON.stringify(breadcrumbs.slice(-20)),
74
+ "breadcrumbs.json": JSON.stringify(breadcrumbs.slice(-5)),
75
75
  ...(sessionId ? { "session.id": sessionId } : {}),
76
76
  };
77
77
  logger.emit({
@@ -6,7 +6,7 @@ import { setupOtelWeb } from "../core/otel-web-setup";
6
6
  import { installBrowserErrorHooks } from "./errors";
7
7
  import { BrowserReplayBuffer } from "./replay";
8
8
  import { installWebVitals } from "./vitals";
9
- import { addBreadcrumb as addCrumb, getBreadcrumbs, installClickBreadcrumbs } from "./breadcrumbs";
9
+ import { addBreadcrumb as addCrumb, getBreadcrumbs } from "./breadcrumbs";
10
10
  import { installConsoleCapture } from "./console";
11
11
  import { installClickTracking } from "./clicks";
12
12
  import { installResourceTiming } from "./resources";
@@ -210,7 +210,6 @@ export function initBrowserSDK(config) {
210
210
  if (config.vitals?.enabled !== false)
211
211
  cleanups.push(installWebVitals(meter, !!config.vitals?.reportAllChanges));
212
212
  cleanups.push(installBrowserErrorHooks(tracer, logger, replay.sessionId));
213
- cleanups.push(installClickBreadcrumbs());
214
213
  cleanups.push(installClickTracking(tracer, replay.sessionId));
215
214
  cleanups.push(installResourceTiming(meter));
216
215
  cleanups.push(installLongTaskDetection(tracer));
@@ -227,11 +226,16 @@ export function initBrowserSDK(config) {
227
226
  installSharedNavigationTracker();
228
227
  }
229
228
  let pendingBeaconBlob = null;
229
+ const scheduleIdle = typeof requestIdleCallback === "function"
230
+ ? (fn) => requestIdleCallback(fn, { timeout: 2000 })
231
+ : (fn) => setTimeout(fn, 0);
230
232
  client.replayTimer = setInterval(() => {
231
233
  const chunk = replay.flush();
232
234
  if (chunk) {
233
- const json = JSON.stringify(chunk);
234
- pendingBeaconBlob = new Blob([json], { type: "application/json" });
235
+ scheduleIdle(() => {
236
+ const json = JSON.stringify(chunk);
237
+ pendingBeaconBlob = new Blob([json], { type: "application/json" });
238
+ });
235
239
  client.replayChunk(chunk);
236
240
  }
237
241
  else {
@@ -303,8 +307,14 @@ export function initBrowserSDK(config) {
303
307
  span.end();
304
308
  }
305
309
  };
310
+ const gaugeCache = new Map();
306
311
  const metricFn = (name, value, unit, context) => {
307
- const gauge = meter.createGauge(name, { unit: unit ?? "1" });
312
+ const key = `${name}\0${unit ?? "1"}`;
313
+ let gauge = gaugeCache.get(key);
314
+ if (!gauge) {
315
+ gauge = meter.createGauge(name, { unit: unit ?? "1" });
316
+ gaugeCache.set(key, gauge);
317
+ }
308
318
  gauge.record(value, { ...userAttrs(), ...context?.attrs });
309
319
  };
310
320
  const captureException = (error, context) => {
@@ -1,8 +1,8 @@
1
1
  import { EventType } from "@rrweb/types";
2
2
  import { sanitizeHeaders, stripQuery, toBase64 } from "../shared/utils";
3
3
  const KEY_OVERHEAD = 6;
4
- function estimateObjectBytes(value) {
5
- if (value === null || value === undefined)
4
+ function estimateObjectBytes(value, depth = 0) {
5
+ if (depth > 8 || value === null || value === undefined)
6
6
  return 4;
7
7
  switch (typeof value) {
8
8
  case "string":
@@ -14,14 +14,14 @@ function estimateObjectBytes(value) {
14
14
  if (Array.isArray(value)) {
15
15
  let sum = 2;
16
16
  for (let i = 0; i < value.length; i++) {
17
- sum += estimateObjectBytes(value[i]) + 1;
17
+ sum += estimateObjectBytes(value[i], depth + 1) + 1;
18
18
  }
19
19
  return sum;
20
20
  }
21
21
  let sum = 2;
22
22
  const keys = Object.keys(value);
23
23
  for (let i = 0; i < keys.length; i++) {
24
- sum += keys[i].length + KEY_OVERHEAD + estimateObjectBytes(value[keys[i]]);
24
+ sum += keys[i].length + KEY_OVERHEAD + estimateObjectBytes(value[keys[i]], depth + 1);
25
25
  }
26
26
  return sum;
27
27
  }
@@ -2,19 +2,32 @@ export function installResourceTiming(meter) {
2
2
  if (typeof window === "undefined" || typeof PerformanceObserver === "undefined")
3
3
  return () => { };
4
4
  const gauge = meter.createGauge("browser.resource.duration", { unit: "ms" });
5
+ let pendingEntries = [];
6
+ let flushTimer = null;
7
+ const flushEntries = () => {
8
+ flushTimer = null;
9
+ const batch = pendingEntries;
10
+ pendingEntries = [];
11
+ for (const res of batch) {
12
+ const name = typeof res.name === "string" ? res.name : "";
13
+ const shortName = name.split("?")[0].split("/").pop() || name.slice(0, 80) || "unknown";
14
+ gauge.record(res.duration, {
15
+ "resource.type": String(res.initiatorType || "other"),
16
+ "resource.name": String(shortName),
17
+ "resource.transfer_size": Number(res.transferSize) || 0,
18
+ });
19
+ }
20
+ };
5
21
  const observer = new PerformanceObserver((list) => {
6
22
  try {
7
23
  for (const entry of list.getEntries()) {
8
24
  const res = entry;
9
25
  if (res.duration < 100)
10
26
  continue;
11
- const name = typeof res.name === "string" ? res.name : "";
12
- const shortName = name.split("?")[0].split("/").pop() || name.slice(0, 80) || "unknown";
13
- gauge.record(res.duration, {
14
- "resource.type": String(res.initiatorType || "other"),
15
- "resource.name": String(shortName),
16
- "resource.transfer_size": Number(res.transferSize) || 0,
17
- });
27
+ pendingEntries.push(res);
28
+ }
29
+ if (pendingEntries.length > 0 && !flushTimer) {
30
+ flushTimer = setTimeout(flushEntries, 1000);
18
31
  }
19
32
  }
20
33
  catch { }
@@ -25,5 +38,9 @@ export function installResourceTiming(meter) {
25
38
  catch {
26
39
  return () => { };
27
40
  }
28
- return () => observer.disconnect();
41
+ return () => {
42
+ observer.disconnect();
43
+ if (flushTimer)
44
+ clearTimeout(flushTimer);
45
+ };
29
46
  }
@@ -59,7 +59,13 @@ export function installWebVitals(meter, reportAllChanges) {
59
59
  if (ev.duration > existing) {
60
60
  interactionDurations.set(ev.interactionId, ev.duration);
61
61
  }
62
- if (interactionDurations.size > 50) {
62
+ if (interactionDurations.size > 100) {
63
+ const entries = [...interactionDurations.entries()].sort((a, b) => b[1] - a[1]);
64
+ interactionDurations.clear();
65
+ for (const [k, v] of entries.slice(0, 50))
66
+ interactionDurations.set(k, v);
67
+ }
68
+ if (interactionDurations.size > 10) {
63
69
  const sorted = [...interactionDurations.values()].sort((a, b) => b - a);
64
70
  const p98Index = Math.max(0, Math.ceil(sorted.length * 0.02) - 1);
65
71
  inpGauge.record(sorted[p98Index], { vital: "inp" });
@@ -29733,14 +29733,14 @@ function setupOtelWeb(config) {
29733
29733
  const parsed = parseSupabaseURL(url, method);
29734
29734
  if (!parsed) return;
29735
29735
  const status = typeof result2?.status === "number" ? result2.status : 0;
29736
- const t = trace.getTracer("@obtrace/sdk-browser", "2.4.0");
29736
+ const t = trace.getTracer("@obtrace/sdk-browser", "2.5.1");
29737
29737
  const synth = { "session.id": sessionId, "supabase.ref": parsed.ref, "span.synthetic": "true" };
29738
- const parentCtx = trace.setSpan(context.active(), parentSpan);
29738
+ const parentCtx = trace.setSpan(ROOT_CONTEXT, parentSpan);
29739
29739
  context.with(parentCtx, () => {
29740
29740
  const gw = t.startSpan("supabase.gateway", {
29741
29741
  attributes: { ...synth, "http.method": method.toUpperCase(), "http.status_code": status, "peer.service": "supabase.kong" }
29742
29742
  });
29743
- const gwCtx = trace.setSpan(context.active(), gw);
29743
+ const gwCtx = trace.setSpan(ROOT_CONTEXT, gw);
29744
29744
  context.with(gwCtx, () => {
29745
29745
  if (parsed.service === "postgrest") {
29746
29746
  const db2 = t.startSpan("supabase.db.query", {
@@ -29849,22 +29849,6 @@ function getElementSelector(el) {
29849
29849
  }
29850
29850
  return sel;
29851
29851
  }
29852
- function installClickBreadcrumbs() {
29853
- if (typeof document === "undefined") return () => {
29854
- };
29855
- const handler = (ev) => {
29856
- const target = ev.target;
29857
- addBreadcrumb({
29858
- timestamp: Date.now(),
29859
- category: "ui.click",
29860
- message: getElementSelector(target),
29861
- level: "info",
29862
- data: { x: ev.clientX, y: ev.clientY }
29863
- });
29864
- };
29865
- document.addEventListener("click", handler, true);
29866
- return () => document.removeEventListener("click", handler, true);
29867
- }
29868
29852
 
29869
29853
  // src/browser/errors.ts
29870
29854
  var processing = false;
@@ -29889,7 +29873,7 @@ function installBrowserErrorHooks(tracer, logger, sessionId) {
29889
29873
  "error.stack": stack.slice(0, 4096),
29890
29874
  "error.type": errorType,
29891
29875
  "breadcrumbs.count": breadcrumbs.length,
29892
- "breadcrumbs.json": JSON.stringify(breadcrumbs.slice(-20)),
29876
+ "breadcrumbs.json": JSON.stringify(breadcrumbs.slice(-5)),
29893
29877
  ...sessionId ? { "session.id": sessionId } : {}
29894
29878
  };
29895
29879
  logger.emit({
@@ -29933,7 +29917,7 @@ function installBrowserErrorHooks(tracer, logger, sessionId) {
29933
29917
  "error.stack": stack.slice(0, 4096),
29934
29918
  "error.type": errorType,
29935
29919
  "breadcrumbs.count": breadcrumbs.length,
29936
- "breadcrumbs.json": JSON.stringify(breadcrumbs.slice(-20)),
29920
+ "breadcrumbs.json": JSON.stringify(breadcrumbs.slice(-5)),
29937
29921
  ...sessionId ? { "session.id": sessionId } : {}
29938
29922
  };
29939
29923
  logger.emit({
@@ -30022,8 +30006,8 @@ function stripQuery(url) {
30022
30006
 
30023
30007
  // src/browser/replay.ts
30024
30008
  var KEY_OVERHEAD = 6;
30025
- function estimateObjectBytes(value) {
30026
- if (value === null || value === void 0) return 4;
30009
+ function estimateObjectBytes(value, depth = 0) {
30010
+ if (depth > 8 || value === null || value === void 0) return 4;
30027
30011
  switch (typeof value) {
30028
30012
  case "string":
30029
30013
  return value.length + 2;
@@ -30034,14 +30018,14 @@ function estimateObjectBytes(value) {
30034
30018
  if (Array.isArray(value)) {
30035
30019
  let sum2 = 2;
30036
30020
  for (let i = 0; i < value.length; i++) {
30037
- sum2 += estimateObjectBytes(value[i]) + 1;
30021
+ sum2 += estimateObjectBytes(value[i], depth + 1) + 1;
30038
30022
  }
30039
30023
  return sum2;
30040
30024
  }
30041
30025
  let sum = 2;
30042
30026
  const keys = Object.keys(value);
30043
30027
  for (let i = 0; i < keys.length; i++) {
30044
- sum += keys[i].length + KEY_OVERHEAD + estimateObjectBytes(value[keys[i]]);
30028
+ sum += keys[i].length + KEY_OVERHEAD + estimateObjectBytes(value[keys[i]], depth + 1);
30045
30029
  }
30046
30030
  return sum;
30047
30031
  }
@@ -30202,7 +30186,12 @@ function installWebVitals(meter, reportAllChanges) {
30202
30186
  if (ev.duration > existing) {
30203
30187
  interactionDurations.set(ev.interactionId, ev.duration);
30204
30188
  }
30205
- if (interactionDurations.size > 50) {
30189
+ if (interactionDurations.size > 100) {
30190
+ const entries = [...interactionDurations.entries()].sort((a, b) => b[1] - a[1]);
30191
+ interactionDurations.clear();
30192
+ for (const [k, v] of entries.slice(0, 50)) interactionDurations.set(k, v);
30193
+ }
30194
+ if (interactionDurations.size > 10) {
30206
30195
  const sorted = [...interactionDurations.values()].sort((a, b) => b - a);
30207
30196
  const p98Index = Math.max(0, Math.ceil(sorted.length * 0.02) - 1);
30208
30197
  inpGauge.record(sorted[p98Index], { vital: "inp" });
@@ -30355,6 +30344,13 @@ function installClickTracking(tracer, sessionId) {
30355
30344
  if (!target) return;
30356
30345
  const selector = getElementSelector(target);
30357
30346
  const now = Date.now();
30347
+ addBreadcrumb({
30348
+ timestamp: now,
30349
+ category: "ui.click",
30350
+ message: selector,
30351
+ level: "info",
30352
+ data: { x: ev.clientX, y: ev.clientY }
30353
+ });
30358
30354
  recent.push({ selector, timestamp: now });
30359
30355
  while (recent.length > 0 && now - recent[0].timestamp > RAGE_WINDOW_MS) {
30360
30356
  recent.shift();
@@ -30405,18 +30401,31 @@ function installResourceTiming(meter) {
30405
30401
  if (typeof window === "undefined" || typeof PerformanceObserver === "undefined") return () => {
30406
30402
  };
30407
30403
  const gauge = meter.createGauge("browser.resource.duration", { unit: "ms" });
30404
+ let pendingEntries = [];
30405
+ let flushTimer = null;
30406
+ const flushEntries = () => {
30407
+ flushTimer = null;
30408
+ const batch = pendingEntries;
30409
+ pendingEntries = [];
30410
+ for (const res of batch) {
30411
+ const name = typeof res.name === "string" ? res.name : "";
30412
+ const shortName = name.split("?")[0].split("/").pop() || name.slice(0, 80) || "unknown";
30413
+ gauge.record(res.duration, {
30414
+ "resource.type": String(res.initiatorType || "other"),
30415
+ "resource.name": String(shortName),
30416
+ "resource.transfer_size": Number(res.transferSize) || 0
30417
+ });
30418
+ }
30419
+ };
30408
30420
  const observer = new PerformanceObserver((list2) => {
30409
30421
  try {
30410
30422
  for (const entry of list2.getEntries()) {
30411
30423
  const res = entry;
30412
30424
  if (res.duration < 100) continue;
30413
- const name = typeof res.name === "string" ? res.name : "";
30414
- const shortName = name.split("?")[0].split("/").pop() || name.slice(0, 80) || "unknown";
30415
- gauge.record(res.duration, {
30416
- "resource.type": String(res.initiatorType || "other"),
30417
- "resource.name": String(shortName),
30418
- "resource.transfer_size": Number(res.transferSize) || 0
30419
- });
30425
+ pendingEntries.push(res);
30426
+ }
30427
+ if (pendingEntries.length > 0 && !flushTimer) {
30428
+ flushTimer = setTimeout(flushEntries, 1e3);
30420
30429
  }
30421
30430
  } catch {
30422
30431
  }
@@ -30427,7 +30436,10 @@ function installResourceTiming(meter) {
30427
30436
  return () => {
30428
30437
  };
30429
30438
  }
30430
- return () => observer.disconnect();
30439
+ return () => {
30440
+ observer.disconnect();
30441
+ if (flushTimer) clearTimeout(flushTimer);
30442
+ };
30431
30443
  }
30432
30444
 
30433
30445
  // src/browser/longtasks.ts
@@ -30647,7 +30659,6 @@ function initBrowserSDK(config) {
30647
30659
  });
30648
30660
  if (config.vitals?.enabled !== false) cleanups.push(installWebVitals(meter, !!config.vitals?.reportAllChanges));
30649
30661
  cleanups.push(installBrowserErrorHooks(tracer, logger, replay.sessionId));
30650
- cleanups.push(installClickBreadcrumbs());
30651
30662
  cleanups.push(installClickTracking(tracer, replay.sessionId));
30652
30663
  cleanups.push(installResourceTiming(meter));
30653
30664
  cleanups.push(installLongTaskDetection(tracer));
@@ -30661,11 +30672,14 @@ function initBrowserSDK(config) {
30661
30672
  installSharedNavigationTracker();
30662
30673
  }
30663
30674
  let pendingBeaconBlob = null;
30675
+ const scheduleIdle = typeof requestIdleCallback === "function" ? (fn) => requestIdleCallback(fn, { timeout: 2e3 }) : (fn) => setTimeout(fn, 0);
30664
30676
  client.replayTimer = setInterval(() => {
30665
30677
  const chunk = replay.flush();
30666
30678
  if (chunk) {
30667
- const json = JSON.stringify(chunk);
30668
- pendingBeaconBlob = new Blob([json], { type: "application/json" });
30679
+ scheduleIdle(() => {
30680
+ const json = JSON.stringify(chunk);
30681
+ pendingBeaconBlob = new Blob([json], { type: "application/json" });
30682
+ });
30669
30683
  client.replayChunk(chunk);
30670
30684
  } else {
30671
30685
  pendingBeaconBlob = null;
@@ -30735,8 +30749,14 @@ function initBrowserSDK(config) {
30735
30749
  span.end();
30736
30750
  }
30737
30751
  };
30752
+ const gaugeCache = /* @__PURE__ */ new Map();
30738
30753
  const metricFn = (name, value, unit, context2) => {
30739
- const gauge = meter.createGauge(name, { unit: unit ?? "1" });
30754
+ const key = `${name}\0${unit ?? "1"}`;
30755
+ let gauge = gaugeCache.get(key);
30756
+ if (!gauge) {
30757
+ gauge = meter.createGauge(name, { unit: unit ?? "1" });
30758
+ gaugeCache.set(key, gauge);
30759
+ }
30740
30760
  gauge.record(value, { ...userAttrs(), ...context2?.attrs });
30741
30761
  };
30742
30762
  const captureException = (error, context2) => {