@obtrace/browser 2.3.0 → 2.4.1

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.
@@ -29548,6 +29548,121 @@ var Zone$1 = loadZone();
29548
29548
  patchCommon(Zone$1);
29549
29549
  patchBrowser(Zone$1);
29550
29550
 
29551
+ // src/browser/supabase.ts
29552
+ var SUPABASE_DOMAIN = ".supabase.co";
29553
+ function isSupabaseURL(url) {
29554
+ try {
29555
+ const u = new URL(url);
29556
+ return u.hostname.endsWith(SUPABASE_DOMAIN);
29557
+ } catch {
29558
+ return false;
29559
+ }
29560
+ }
29561
+ function parseSupabaseURL(url, method) {
29562
+ let u;
29563
+ try {
29564
+ u = new URL(url);
29565
+ } catch {
29566
+ return null;
29567
+ }
29568
+ if (!u.hostname.endsWith(SUPABASE_DOMAIN)) return null;
29569
+ const ref = u.hostname.replace(SUPABASE_DOMAIN, "");
29570
+ const path = u.pathname;
29571
+ const segments = path.split("/").filter(Boolean);
29572
+ if (segments[0] === "rest" && segments[1] === "v1" && segments[2]) {
29573
+ const table = segments[2];
29574
+ const op = restMethodToOp(method);
29575
+ const select = u.searchParams.get("select") || "*";
29576
+ const filters = extractFilters(u.searchParams);
29577
+ const detail = op === "SELECT" ? `${op} ${select} FROM ${table}` : `${op} ${table}`;
29578
+ return { ref, service: "postgrest", operation: op, table, detail: filters ? `${detail} WHERE ${filters}` : detail };
29579
+ }
29580
+ if (segments[0] === "auth" && segments[1] === "v1") {
29581
+ const action = segments[2] || "session";
29582
+ const op = authActionToOp(action);
29583
+ return { ref, service: "auth", operation: op, table: "", detail: `AUTH ${op}` };
29584
+ }
29585
+ if (segments[0] === "storage" && segments[1] === "v1") {
29586
+ const subCmd = segments[2] || "object";
29587
+ const bucket = segments[3] || "";
29588
+ const filePath = segments.slice(4).join("/");
29589
+ const op = `${method.toUpperCase()} ${subCmd}`;
29590
+ return { ref, service: "storage", operation: op, table: "", detail: bucket ? `STORAGE ${op} ${bucket}/${filePath}` : `STORAGE ${op}` };
29591
+ }
29592
+ if (segments[0] === "realtime") {
29593
+ return { ref, service: "realtime", operation: "subscribe", table: "", detail: "REALTIME subscribe" };
29594
+ }
29595
+ if (segments[0] === "functions" && segments[1] === "v1" && segments[2]) {
29596
+ const fnName = segments[2];
29597
+ return { ref, service: "edge-function", operation: `invoke:${fnName}`, table: "", detail: `EDGE FUNCTION ${fnName}` };
29598
+ }
29599
+ return { ref, service: "unknown", operation: method.toUpperCase(), table: "", detail: `${method.toUpperCase()} ${path}` };
29600
+ }
29601
+ function restMethodToOp(method) {
29602
+ switch (method.toUpperCase()) {
29603
+ case "GET":
29604
+ return "SELECT";
29605
+ case "POST":
29606
+ return "INSERT";
29607
+ case "PATCH":
29608
+ return "UPDATE";
29609
+ case "PUT":
29610
+ return "UPSERT";
29611
+ case "DELETE":
29612
+ return "DELETE";
29613
+ default:
29614
+ return method.toUpperCase();
29615
+ }
29616
+ }
29617
+ function authActionToOp(action) {
29618
+ switch (action) {
29619
+ case "token":
29620
+ return "login";
29621
+ case "signup":
29622
+ return "signup";
29623
+ case "logout":
29624
+ return "logout";
29625
+ case "recover":
29626
+ return "recover";
29627
+ case "magiclink":
29628
+ return "magic_link";
29629
+ case "otp":
29630
+ return "otp";
29631
+ case "user":
29632
+ return "get_user";
29633
+ case "callback":
29634
+ return "oauth_callback";
29635
+ default:
29636
+ return action;
29637
+ }
29638
+ }
29639
+ function extractFilters(params) {
29640
+ const filters = [];
29641
+ for (const [key, value] of params.entries()) {
29642
+ if (key === "select" || key === "apikey" || key === "order" || key === "limit" || key === "offset") continue;
29643
+ const match = value.match(/^(eq|neq|gt|gte|lt|lte|like|ilike|is|in|cs|cd|not)\.(.+)/);
29644
+ if (match) {
29645
+ filters.push(`${key} ${match[1]} ${match[2]}`);
29646
+ }
29647
+ }
29648
+ return filters.join(" AND ");
29649
+ }
29650
+ function enrichSupabaseSpan(span, url, method) {
29651
+ const parsed = parseSupabaseURL(url, method);
29652
+ if (!parsed) return;
29653
+ span.setAttribute("supabase.ref", parsed.ref);
29654
+ span.setAttribute("supabase.service", parsed.service);
29655
+ span.setAttribute("supabase.operation", parsed.operation);
29656
+ span.setAttribute("supabase.detail", parsed.detail);
29657
+ span.setAttribute("peer.service", `supabase.${parsed.service}`);
29658
+ if (parsed.service === "postgrest") {
29659
+ span.setAttribute("db.system", "postgresql");
29660
+ span.setAttribute("db.operation", parsed.operation);
29661
+ if (parsed.table) span.setAttribute("db.sql.table", parsed.table);
29662
+ }
29663
+ span.updateName(`supabase.${parsed.service} ${parsed.detail}`);
29664
+ }
29665
+
29551
29666
  // src/core/otel-web-setup.ts
29552
29667
  function setupOtelWeb(config) {
29553
29668
  const baseUrl = (config.ingestBaseUrl || "https://ingest.obtrace.ai").replace(/\/$/, "");
@@ -29607,12 +29722,23 @@ function setupOtelWeb(config) {
29607
29722
  });
29608
29723
  const ingestPattern = new RegExp(`^${baseUrl.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}`);
29609
29724
  const instrumentations = [];
29725
+ const applySupabaseAttrs = (span, req, _result) => {
29726
+ try {
29727
+ const url = typeof req === "string" ? req : req instanceof URL ? req.href : req?.url || "";
29728
+ const method = req?.method || "GET";
29729
+ if (url && isSupabaseURL(url)) {
29730
+ enrichSupabaseSpan(span, url, method);
29731
+ }
29732
+ } catch {
29733
+ }
29734
+ };
29610
29735
  if (config.instrumentGlobalFetch !== false) {
29611
29736
  instrumentations.push(
29612
29737
  new FetchInstrumentation({
29613
29738
  propagateTraceHeaderCorsUrls: /.*/,
29614
29739
  ignoreUrls: [ingestPattern],
29615
- clearTimingResources: true
29740
+ clearTimingResources: true,
29741
+ applyCustomAttributesOnSpan: applySupabaseAttrs
29616
29742
  })
29617
29743
  );
29618
29744
  }
@@ -29631,8 +29757,8 @@ function setupOtelWeb(config) {
29631
29757
  tracerProvider,
29632
29758
  instrumentations
29633
29759
  });
29634
- const tracer = trace.getTracer("@obtrace/sdk-browser", "2.2.0");
29635
- const meter = metrics.getMeter("@obtrace/sdk-browser", "2.2.0");
29760
+ const tracer = trace.getTracer("@obtrace/sdk-browser", "2.4.0");
29761
+ const meter = metrics.getMeter("@obtrace/sdk-browser", "2.4.0");
29636
29762
  const forceFlush = async () => {
29637
29763
  try {
29638
29764
  await tracerProvider.forceFlush();
@@ -29695,14 +29821,17 @@ function installClickBreadcrumbs() {
29695
29821
  }
29696
29822
 
29697
29823
  // src/browser/errors.ts
29824
+ var processing = false;
29698
29825
  function installBrowserErrorHooks(tracer, logger, sessionId) {
29699
29826
  if (typeof window === "undefined") {
29700
29827
  return () => void 0;
29701
29828
  }
29702
29829
  const onError = (ev) => {
29830
+ if (processing) return;
29703
29831
  const message = ev.message || "window.error";
29704
29832
  addBreadcrumb({ timestamp: Date.now(), category: "error", message, level: "error" });
29705
29833
  try {
29834
+ processing = true;
29706
29835
  const breadcrumbs = getBreadcrumbs();
29707
29836
  const stack = ev.error instanceof Error ? ev.error.stack || "" : "";
29708
29837
  const errorType = ev.error?.constructor?.name || "Error";
@@ -29733,9 +29862,12 @@ function installBrowserErrorHooks(tracer, logger, sessionId) {
29733
29862
  }
29734
29863
  span.end();
29735
29864
  } catch {
29865
+ } finally {
29866
+ processing = false;
29736
29867
  }
29737
29868
  };
29738
29869
  const onRejection = (ev) => {
29870
+ if (processing) return;
29739
29871
  let reason;
29740
29872
  let stack = "";
29741
29873
  let errorType = "UnhandledRejection";
@@ -29748,6 +29880,7 @@ function installBrowserErrorHooks(tracer, logger, sessionId) {
29748
29880
  }
29749
29881
  addBreadcrumb({ timestamp: Date.now(), category: "error", message: reason, level: "error" });
29750
29882
  try {
29883
+ processing = true;
29751
29884
  const breadcrumbs = getBreadcrumbs();
29752
29885
  const attrs = {
29753
29886
  "error.message": reason,
@@ -29773,6 +29906,8 @@ function installBrowserErrorHooks(tracer, logger, sessionId) {
29773
29906
  }
29774
29907
  span.end();
29775
29908
  } catch {
29909
+ } finally {
29910
+ processing = false;
29776
29911
  }
29777
29912
  };
29778
29913
  window.addEventListener("error", onError);
@@ -30058,6 +30193,7 @@ var SEVERITY_MAP = {
30058
30193
  };
30059
30194
  var patched = false;
30060
30195
  var originals = {};
30196
+ var emitting = false;
30061
30197
  function installConsoleCapture(tracer, logger, sessionId) {
30062
30198
  if (patched || typeof console === "undefined") return () => {
30063
30199
  };
@@ -30070,7 +30206,9 @@ function installConsoleCapture(tracer, logger, sessionId) {
30070
30206
  const level = LEVEL_MAP[method];
30071
30207
  console[method] = (...args) => {
30072
30208
  original(...args);
30209
+ if (emitting) return;
30073
30210
  try {
30211
+ emitting = true;
30074
30212
  let message;
30075
30213
  let attrs = {};
30076
30214
  const safeStringify = (v) => {
@@ -30138,6 +30276,8 @@ function installConsoleCapture(tracer, logger, sessionId) {
30138
30276
  span.end();
30139
30277
  }
30140
30278
  } catch {
30279
+ } finally {
30280
+ emitting = false;
30141
30281
  }
30142
30282
  };
30143
30283
  }
@@ -30220,14 +30360,19 @@ function installResourceTiming(meter) {
30220
30360
  };
30221
30361
  const gauge = meter.createGauge("browser.resource.duration", { unit: "ms" });
30222
30362
  const observer = new PerformanceObserver((list2) => {
30223
- for (const entry of list2.getEntries()) {
30224
- const res = entry;
30225
- if (res.duration < 100) continue;
30226
- gauge.record(res.duration, {
30227
- "resource.type": res.initiatorType || "other",
30228
- "resource.name": res.name.split("?")[0].split("/").pop() || res.name.slice(0, 80),
30229
- "resource.transfer_size": res.transferSize || 0
30230
- });
30363
+ try {
30364
+ for (const entry of list2.getEntries()) {
30365
+ const res = entry;
30366
+ if (res.duration < 100) continue;
30367
+ const name = typeof res.name === "string" ? res.name : "";
30368
+ const shortName = name.split("?")[0].split("/").pop() || name.slice(0, 80) || "unknown";
30369
+ gauge.record(res.duration, {
30370
+ "resource.type": String(res.initiatorType || "other"),
30371
+ "resource.name": String(shortName),
30372
+ "resource.transfer_size": Number(res.transferSize) || 0
30373
+ });
30374
+ }
30375
+ } catch {
30231
30376
  }
30232
30377
  });
30233
30378
  try {
@@ -30284,6 +30429,127 @@ function installMemoryTracking(meter) {
30284
30429
  return () => clearInterval(timer);
30285
30430
  }
30286
30431
 
30432
+ // src/browser/supabase-intercept.ts
30433
+ function installSupabaseFetchInterceptor(tracer, sessionId) {
30434
+ if (typeof window === "undefined" || typeof window.fetch === "undefined") return () => {
30435
+ };
30436
+ const originalFetch = window.fetch;
30437
+ window.fetch = async function(input2, init) {
30438
+ const url = typeof input2 === "string" ? input2 : input2 instanceof URL ? input2.href : input2 instanceof Request ? input2.url : "";
30439
+ if (!url || !isSupabaseURL(url)) {
30440
+ return originalFetch.call(this, input2, init);
30441
+ }
30442
+ const method = init?.method || (input2 instanceof Request ? input2.method : "GET") || "GET";
30443
+ const parsed = parseSupabaseURL(url, method);
30444
+ if (!parsed) {
30445
+ return originalFetch.call(this, input2, init);
30446
+ }
30447
+ const rootSpan = tracer.startSpan(`supabase.${parsed.service} ${parsed.detail}`, {
30448
+ attributes: {
30449
+ "supabase.ref": parsed.ref,
30450
+ "supabase.service": parsed.service,
30451
+ "supabase.operation": parsed.operation,
30452
+ "supabase.detail": parsed.detail,
30453
+ "http.method": method.toUpperCase(),
30454
+ "http.url": url.split("?")[0],
30455
+ "peer.service": `supabase.${parsed.service}`,
30456
+ "session.id": sessionId,
30457
+ ...parsed.service === "postgrest" ? {
30458
+ "db.system": "postgresql",
30459
+ "db.operation": parsed.operation,
30460
+ "db.sql.table": parsed.table
30461
+ } : {}
30462
+ }
30463
+ });
30464
+ const rootCtx = trace.setSpan(context.active(), rootSpan);
30465
+ const startMs = performance.now();
30466
+ try {
30467
+ const response = await originalFetch.call(this, input2, init);
30468
+ const durationMs = performance.now() - startMs;
30469
+ rootSpan.setAttribute("http.status_code", response.status);
30470
+ rootSpan.setAttribute("supabase.duration_ms", Math.round(durationMs));
30471
+ context.with(rootCtx, () => {
30472
+ createChildSpans(tracer, parsed, method, response.status, durationMs, sessionId);
30473
+ });
30474
+ if (response.status >= 400) {
30475
+ rootSpan.setStatus({ code: SpanStatusCode.ERROR, message: `HTTP ${response.status}` });
30476
+ addBreadcrumb({ timestamp: Date.now(), category: "supabase", message: `${parsed.detail} \u2192 ${response.status}`, level: "error" });
30477
+ } else {
30478
+ rootSpan.setStatus({ code: SpanStatusCode.OK });
30479
+ addBreadcrumb({ timestamp: Date.now(), category: "supabase", message: `${parsed.detail} \u2192 ${response.status} (${Math.round(durationMs)}ms)`, level: "info" });
30480
+ }
30481
+ rootSpan.end();
30482
+ return response;
30483
+ } catch (err) {
30484
+ rootSpan.setStatus({ code: SpanStatusCode.ERROR, message: err instanceof Error ? err.message : "fetch failed" });
30485
+ if (err instanceof Error) rootSpan.recordException(err);
30486
+ rootSpan.end();
30487
+ addBreadcrumb({ timestamp: Date.now(), category: "supabase", message: `${parsed.detail} \u2192 FAILED`, level: "error" });
30488
+ throw err;
30489
+ }
30490
+ };
30491
+ return () => {
30492
+ window.fetch = originalFetch;
30493
+ };
30494
+ }
30495
+ function createChildSpans(tracer, parsed, method, status, _durationMs, sessionId) {
30496
+ const synth = { "session.id": sessionId, "supabase.ref": parsed.ref, "span.synthetic": "true" };
30497
+ const gatewaySpan = tracer.startSpan("supabase.gateway", {
30498
+ attributes: {
30499
+ ...synth,
30500
+ "http.method": method.toUpperCase(),
30501
+ "http.status_code": status,
30502
+ "peer.service": "supabase.kong"
30503
+ }
30504
+ });
30505
+ const gatewayCtx = trace.setSpan(context.active(), gatewaySpan);
30506
+ context.with(gatewayCtx, () => {
30507
+ if (parsed.service === "postgrest") {
30508
+ const dbSpan = tracer.startSpan("supabase.db.query", {
30509
+ attributes: {
30510
+ ...synth,
30511
+ "db.system": "postgresql",
30512
+ "db.operation": parsed.operation,
30513
+ "db.sql.table": parsed.table,
30514
+ "db.statement": parsed.detail,
30515
+ "peer.service": "supabase.postgresql"
30516
+ }
30517
+ });
30518
+ if (status >= 400) dbSpan.setStatus({ code: SpanStatusCode.ERROR, message: `HTTP ${status}` });
30519
+ else dbSpan.setStatus({ code: SpanStatusCode.OK });
30520
+ dbSpan.end();
30521
+ }
30522
+ if (parsed.service === "auth") {
30523
+ const authSpan = tracer.startSpan("supabase.auth." + parsed.operation, {
30524
+ attributes: { ...synth, "auth.operation": parsed.operation, "peer.service": "supabase.gotrue" }
30525
+ });
30526
+ if (status >= 400) authSpan.setStatus({ code: SpanStatusCode.ERROR, message: `HTTP ${status}` });
30527
+ else authSpan.setStatus({ code: SpanStatusCode.OK });
30528
+ authSpan.end();
30529
+ }
30530
+ if (parsed.service === "storage") {
30531
+ const storageSpan = tracer.startSpan("supabase.storage." + parsed.operation, {
30532
+ attributes: { ...synth, "storage.operation": parsed.operation, "peer.service": "supabase.storage" }
30533
+ });
30534
+ if (status >= 400) storageSpan.setStatus({ code: SpanStatusCode.ERROR, message: `HTTP ${status}` });
30535
+ else storageSpan.setStatus({ code: SpanStatusCode.OK });
30536
+ storageSpan.end();
30537
+ }
30538
+ if (parsed.service === "edge-function") {
30539
+ const fnName = parsed.operation.replace("invoke:", "");
30540
+ const fnSpan = tracer.startSpan("supabase.function." + fnName, {
30541
+ attributes: { ...synth, "faas.name": fnName, "faas.trigger": "http", "peer.service": "supabase.edge-runtime" }
30542
+ });
30543
+ if (status >= 400) fnSpan.setStatus({ code: SpanStatusCode.ERROR, message: `HTTP ${status}` });
30544
+ else fnSpan.setStatus({ code: SpanStatusCode.OK });
30545
+ fnSpan.end();
30546
+ }
30547
+ });
30548
+ if (status >= 400) gatewaySpan.setStatus({ code: SpanStatusCode.ERROR, message: `HTTP ${status}` });
30549
+ else gatewaySpan.setStatus({ code: SpanStatusCode.OK });
30550
+ gatewaySpan.end();
30551
+ }
30552
+
30287
30553
  // src/browser/index.ts
30288
30554
  var instances = /* @__PURE__ */ new Set();
30289
30555
  var replayBuffers = /* @__PURE__ */ new Set();
@@ -30414,7 +30680,7 @@ function initBrowserSDK(config) {
30414
30680
  const otel = setupOtelWeb({ ...config, tracesSampleRate: sampleRate, sessionId: replay.sessionId });
30415
30681
  const tracer = otel.tracer;
30416
30682
  const meter = otel.meter;
30417
- const logger = otel.loggerProvider.getLogger("@obtrace/sdk-browser", "2.2.0");
30683
+ const logger = otel.loggerProvider.getLogger("@obtrace/sdk-browser", "2.4.0");
30418
30684
  const client = new ObtraceClient({
30419
30685
  ...config,
30420
30686
  replay: {
@@ -30461,6 +30727,7 @@ function initBrowserSDK(config) {
30461
30727
  cleanups.push(installResourceTiming(meter));
30462
30728
  cleanups.push(installLongTaskDetection(tracer));
30463
30729
  cleanups.push(installMemoryTracking(meter));
30730
+ cleanups.push(installSupabaseFetchInterceptor(tracer, replay.sessionId));
30464
30731
  if (config.captureConsole !== false) {
30465
30732
  cleanups.push(installConsoleCapture(tracer, logger, replay.sessionId));
30466
30733
  }
@@ -30481,7 +30748,7 @@ function initBrowserSDK(config) {
30481
30748
  }
30482
30749
  }, config.replay?.flushIntervalMs ?? 5e3);
30483
30750
  const sendViaBeacon = () => {
30484
- const url = `${config.ingestBaseUrl?.replace(/\/$/, "")}/ingest/replay/chunk`;
30751
+ const url = `${config.ingestBaseUrl?.replace(/\/$/, "")}/ingest/replay/chunk?token=${encodeURIComponent(config.apiKey)}`;
30485
30752
  const freshChunk = replay.flush();
30486
30753
  if (freshChunk) {
30487
30754
  navigator.sendBeacon(url, new Blob([JSON.stringify(freshChunk)], { type: "application/json" }));