@obtrace/browser 2.2.0 → 2.4.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.
@@ -1,4 +1,5 @@
1
1
  import { SpanStatusCode } from "@opentelemetry/api";
2
+ import { SeverityNumber } from "@opentelemetry/api-logs";
2
3
  import { addBreadcrumb } from "./breadcrumbs";
3
4
  const LEVEL_MAP = {
4
5
  debug: "debug",
@@ -7,9 +8,17 @@ const LEVEL_MAP = {
7
8
  warn: "warn",
8
9
  error: "error",
9
10
  };
11
+ const SEVERITY_MAP = {
12
+ debug: SeverityNumber.DEBUG,
13
+ log: SeverityNumber.INFO,
14
+ info: SeverityNumber.INFO,
15
+ warn: SeverityNumber.WARN,
16
+ error: SeverityNumber.ERROR,
17
+ };
10
18
  let patched = false;
11
19
  let originals = {};
12
- export function installConsoleCapture(tracer, sessionId) {
20
+ let emitting = false;
21
+ export function installConsoleCapture(tracer, logger, sessionId) {
13
22
  if (patched || typeof console === "undefined")
14
23
  return () => { };
15
24
  patched = true;
@@ -21,7 +30,10 @@ export function installConsoleCapture(tracer, sessionId) {
21
30
  const level = LEVEL_MAP[method];
22
31
  console[method] = (...args) => {
23
32
  original(...args);
33
+ if (emitting)
34
+ return;
24
35
  try {
36
+ emitting = true;
25
37
  let message;
26
38
  let attrs = {};
27
39
  const safeStringify = (v) => {
@@ -70,23 +82,35 @@ export function installConsoleCapture(tracer, sessionId) {
70
82
  }
71
83
  }
72
84
  addBreadcrumb({ timestamp: Date.now(), category: `console.${method}`, message, level, data: attrs });
73
- const spanName = (method === "error" && (isErrorObj || attrs["error.stack"])) ? "browser.error" : "browser.console";
74
- const span = tracer.startSpan(spanName, {
85
+ logger.emit({
86
+ severityNumber: SEVERITY_MAP[method] ?? SeverityNumber.INFO,
87
+ severityText: level.toUpperCase(),
88
+ body: message.slice(0, 4096),
75
89
  attributes: {
76
- "log.severity": level.toUpperCase(),
77
- "log.message": message.slice(0, 1024),
78
90
  "session.id": sessionId,
91
+ "log.source": "console",
79
92
  ...attrs,
80
93
  },
81
94
  });
82
95
  if (method === "error") {
96
+ const spanName = (isErrorObj || attrs["error.stack"]) ? "browser.error" : "browser.console";
97
+ const span = tracer.startSpan(spanName, {
98
+ attributes: {
99
+ "error.message": message.slice(0, 1024),
100
+ "session.id": sessionId,
101
+ ...attrs,
102
+ },
103
+ });
83
104
  span.setStatus({ code: SpanStatusCode.ERROR, message: message.slice(0, 1024) });
84
105
  if (isErrorObj)
85
106
  span.recordException(firstArg);
107
+ span.end();
86
108
  }
87
- span.end();
88
109
  }
89
110
  catch { }
111
+ finally {
112
+ emitting = false;
113
+ }
90
114
  };
91
115
  }
92
116
  return () => {
@@ -1,28 +1,42 @@
1
1
  import { SpanStatusCode } from "@opentelemetry/api";
2
+ import { SeverityNumber } from "@opentelemetry/api-logs";
2
3
  import { addBreadcrumb, getBreadcrumbs } from "./breadcrumbs";
3
- export function installBrowserErrorHooks(tracer, sessionId) {
4
+ let processing = false;
5
+ export function installBrowserErrorHooks(tracer, logger, sessionId) {
4
6
  if (typeof window === "undefined") {
5
7
  return () => undefined;
6
8
  }
7
9
  const onError = (ev) => {
10
+ if (processing)
11
+ return;
8
12
  const message = ev.message || "window.error";
9
13
  addBreadcrumb({ timestamp: Date.now(), category: "error", message, level: "error" });
10
14
  try {
15
+ processing = true;
11
16
  const breadcrumbs = getBreadcrumbs();
12
17
  const stack = ev.error instanceof Error ? ev.error.stack || "" : "";
13
- const span = tracer.startSpan("browser.error", {
18
+ const errorType = ev.error?.constructor?.name || "Error";
19
+ const attrs = {
20
+ "error.message": message,
21
+ "error.file": ev.filename || "",
22
+ "error.line": ev.lineno || 0,
23
+ "error.column": ev.colno || 0,
24
+ "error.stack": stack.slice(0, 4096),
25
+ "error.type": errorType,
26
+ "breadcrumbs.count": breadcrumbs.length,
27
+ "breadcrumbs.json": JSON.stringify(breadcrumbs.slice(-20)),
28
+ ...(sessionId ? { "session.id": sessionId } : {}),
29
+ };
30
+ logger.emit({
31
+ severityNumber: SeverityNumber.ERROR,
32
+ severityText: "ERROR",
33
+ body: message,
14
34
  attributes: {
15
- "error.message": message,
16
- "error.file": ev.filename || "",
17
- "error.line": ev.lineno || 0,
18
- "error.column": ev.colno || 0,
19
- "error.stack": stack.slice(0, 4096),
20
- "error.type": ev.error?.constructor?.name || "Error",
21
- "breadcrumbs.count": breadcrumbs.length,
22
- "breadcrumbs.json": JSON.stringify(breadcrumbs.slice(-20)),
23
- ...(sessionId ? { "session.id": sessionId } : {}),
35
+ "log.source": "window.error",
36
+ ...attrs,
24
37
  },
25
38
  });
39
+ const span = tracer.startSpan("browser.error", { attributes: attrs });
26
40
  span.setStatus({ code: SpanStatusCode.ERROR, message });
27
41
  if (ev.error instanceof Error) {
28
42
  span.recordException(ev.error);
@@ -30,30 +44,46 @@ export function installBrowserErrorHooks(tracer, sessionId) {
30
44
  span.end();
31
45
  }
32
46
  catch { }
47
+ finally {
48
+ processing = false;
49
+ }
33
50
  };
34
51
  const onRejection = (ev) => {
52
+ if (processing)
53
+ return;
35
54
  let reason;
36
55
  let stack = "";
56
+ let errorType = "UnhandledRejection";
37
57
  if (ev.reason instanceof Error) {
38
58
  reason = `${ev.reason.name}: ${ev.reason.message}`;
39
59
  stack = ev.reason.stack || "";
60
+ errorType = ev.reason.constructor?.name || "Error";
40
61
  }
41
62
  else {
42
63
  reason = typeof ev.reason === "string" ? ev.reason : JSON.stringify(ev.reason ?? {});
43
64
  }
44
65
  addBreadcrumb({ timestamp: Date.now(), category: "error", message: reason, level: "error" });
45
66
  try {
67
+ processing = true;
46
68
  const breadcrumbs = getBreadcrumbs();
47
- const span = tracer.startSpan("browser.unhandledrejection", {
69
+ const attrs = {
70
+ "error.message": reason,
71
+ "error.stack": stack.slice(0, 4096),
72
+ "error.type": errorType,
73
+ "breadcrumbs.count": breadcrumbs.length,
74
+ "breadcrumbs.json": JSON.stringify(breadcrumbs.slice(-20)),
75
+ ...(sessionId ? { "session.id": sessionId } : {}),
76
+ };
77
+ logger.emit({
78
+ severityNumber: SeverityNumber.ERROR,
79
+ severityText: "ERROR",
80
+ body: reason,
48
81
  attributes: {
49
- "error.message": reason,
50
- "error.stack": stack.slice(0, 4096),
51
- "error.type": ev.reason?.constructor?.name || "UnhandledRejection",
52
- "breadcrumbs.count": breadcrumbs.length,
53
- "breadcrumbs.json": JSON.stringify(breadcrumbs.slice(-20)),
54
- ...(sessionId ? { "session.id": sessionId } : {}),
82
+ "log.source": "unhandledrejection",
83
+ ...attrs,
55
84
  },
56
85
  });
86
+ const span = tracer.startSpan("browser.unhandledrejection", { attributes: attrs });
57
87
  span.setStatus({ code: SpanStatusCode.ERROR, message: reason });
58
88
  if (ev.reason instanceof Error) {
59
89
  span.recordException(ev.reason);
@@ -61,6 +91,9 @@ export function installBrowserErrorHooks(tracer, sessionId) {
61
91
  span.end();
62
92
  }
63
93
  catch { }
94
+ finally {
95
+ processing = false;
96
+ }
64
97
  };
65
98
  window.addEventListener("error", onError);
66
99
  window.addEventListener("unhandledrejection", onRejection);
@@ -1,5 +1,6 @@
1
1
  import { record } from "rrweb";
2
2
  import { SpanStatusCode } from "@opentelemetry/api";
3
+ import { SeverityNumber } from "@opentelemetry/api-logs";
3
4
  import { ObtraceClient } from "../core/client";
4
5
  import { setupOtelWeb } from "../core/otel-web-setup";
5
6
  import { installBrowserErrorHooks } from "./errors";
@@ -12,6 +13,7 @@ import { installResourceTiming } from "./resources";
12
13
  import { installLongTaskDetection } from "./longtasks";
13
14
  import { installMemoryTracking } from "./memory";
14
15
  import { installOfflineSupport } from "./offline";
16
+ import { installSupabaseFetchInterceptor } from "./supabase-intercept";
15
17
  const instances = new Set();
16
18
  const replayBuffers = new Set();
17
19
  let currentUser = null;
@@ -165,6 +167,7 @@ export function initBrowserSDK(config) {
165
167
  const otel = setupOtelWeb({ ...config, tracesSampleRate: sampleRate, sessionId: replay.sessionId });
166
168
  const tracer = otel.tracer;
167
169
  const meter = otel.meter;
170
+ const logger = otel.loggerProvider.getLogger("@obtrace/sdk-browser", "2.4.0");
168
171
  const client = new ObtraceClient({
169
172
  ...config,
170
173
  replay: {
@@ -207,14 +210,15 @@ export function initBrowserSDK(config) {
207
210
  }).catch(() => { });
208
211
  if (config.vitals?.enabled !== false)
209
212
  cleanups.push(installWebVitals(meter, !!config.vitals?.reportAllChanges));
210
- cleanups.push(installBrowserErrorHooks(tracer, replay.sessionId));
213
+ cleanups.push(installBrowserErrorHooks(tracer, logger, replay.sessionId));
211
214
  cleanups.push(installClickBreadcrumbs());
212
215
  cleanups.push(installClickTracking(tracer, replay.sessionId));
213
216
  cleanups.push(installResourceTiming(meter));
214
217
  cleanups.push(installLongTaskDetection(tracer));
215
218
  cleanups.push(installMemoryTracking(meter));
219
+ cleanups.push(installSupabaseFetchInterceptor(tracer, replay.sessionId));
216
220
  if (config.captureConsole !== false) {
217
- cleanups.push(installConsoleCapture(tracer, replay.sessionId));
221
+ cleanups.push(installConsoleCapture(tracer, logger, replay.sessionId));
218
222
  }
219
223
  cleanups.push(installOfflineSupport());
220
224
  if (shouldReplay && config.replay?.enabled !== false && typeof window !== "undefined") {
@@ -263,22 +267,40 @@ export function initBrowserSDK(config) {
263
267
  window.addEventListener("beforeunload", onBeforeUnload);
264
268
  cleanups.push(() => window.removeEventListener("beforeunload", onBeforeUnload));
265
269
  }
270
+ const sevToOtel = {
271
+ debug: SeverityNumber.DEBUG,
272
+ info: SeverityNumber.INFO,
273
+ warn: SeverityNumber.WARN,
274
+ error: SeverityNumber.ERROR,
275
+ fatal: SeverityNumber.FATAL,
276
+ };
266
277
  const log = (level, message, context) => {
267
278
  addCrumb({ timestamp: Date.now(), category: "log", message, level: level === "fatal" ? "error" : level });
268
- const span = tracer.startSpan("browser.log", {
279
+ logger.emit({
280
+ severityNumber: sevToOtel[level] ?? SeverityNumber.INFO,
281
+ severityText: level.toUpperCase(),
282
+ body: message.slice(0, 4096),
269
283
  attributes: {
270
- "log.severity": level.toUpperCase(),
271
- "log.severity_number": severityToNumber(level),
272
- "log.message": message,
273
284
  "session.id": replay.sessionId,
285
+ "log.source": "sdk",
274
286
  ...userAttrs(),
275
287
  ...(context?.traceId ? { "obtrace.trace_id": context.traceId } : {}),
276
288
  ...context?.attrs,
277
289
  },
278
290
  });
279
- if (level === "error" || level === "fatal")
291
+ if (level === "error" || level === "fatal") {
292
+ const span = tracer.startSpan("browser.error", {
293
+ attributes: {
294
+ "error.message": message,
295
+ "session.id": replay.sessionId,
296
+ ...userAttrs(),
297
+ ...(context?.traceId ? { "obtrace.trace_id": context.traceId } : {}),
298
+ ...context?.attrs,
299
+ },
300
+ });
280
301
  span.setStatus({ code: SpanStatusCode.ERROR, message });
281
- span.end();
302
+ span.end();
303
+ }
282
304
  };
283
305
  const metricFn = (name, value, unit, context) => {
284
306
  const gauge = meter.createGauge(name, { unit: unit ?? "1" });
@@ -286,11 +308,26 @@ export function initBrowserSDK(config) {
286
308
  };
287
309
  const captureException = (error, context) => {
288
310
  const msg = error instanceof Error ? `${error.name}: ${error.message}` : String(error);
311
+ const stack = error instanceof Error ? (error.stack || "").slice(0, 4096) : "";
289
312
  const breadcrumbs = getBreadcrumbs();
290
313
  addCrumb({ timestamp: Date.now(), category: "error", message: msg, level: "error" });
314
+ logger.emit({
315
+ severityNumber: SeverityNumber.ERROR,
316
+ severityText: "ERROR",
317
+ body: msg,
318
+ attributes: {
319
+ "session.id": replay.sessionId,
320
+ "log.source": "exception",
321
+ "error.stack": stack,
322
+ "error.type": error instanceof Error ? error.name : "Error",
323
+ ...userAttrs(),
324
+ ...context?.attrs,
325
+ },
326
+ });
291
327
  const span = tracer.startSpan("browser.exception", {
292
328
  attributes: {
293
329
  "error.message": msg,
330
+ "error.stack": stack,
294
331
  "session.id": replay.sessionId,
295
332
  "breadcrumbs.count": breadcrumbs.length,
296
333
  "breadcrumbs.json": JSON.stringify(breadcrumbs.slice(-20)),
@@ -3,16 +3,21 @@ export function installResourceTiming(meter) {
3
3
  return () => { };
4
4
  const gauge = meter.createGauge("browser.resource.duration", { unit: "ms" });
5
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
- });
6
+ try {
7
+ for (const entry of list.getEntries()) {
8
+ const res = entry;
9
+ if (res.duration < 100)
10
+ 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
+ });
18
+ }
15
19
  }
20
+ catch { }
16
21
  });
17
22
  try {
18
23
  observer.observe({ type: "resource", buffered: false });
@@ -0,0 +1,133 @@
1
+ import { SpanStatusCode, context, trace } from "@opentelemetry/api";
2
+ import { isSupabaseURL, parseSupabaseURL } from "./supabase";
3
+ import { addBreadcrumb } from "./breadcrumbs";
4
+ export function installSupabaseFetchInterceptor(tracer, sessionId) {
5
+ if (typeof window === "undefined" || typeof window.fetch === "undefined")
6
+ return () => { };
7
+ const originalFetch = window.fetch;
8
+ window.fetch = async function (input, init) {
9
+ const url = typeof input === "string" ? input : input instanceof URL ? input.href : input instanceof Request ? input.url : "";
10
+ if (!url || !isSupabaseURL(url)) {
11
+ return originalFetch.call(this, input, init);
12
+ }
13
+ const method = init?.method || (input instanceof Request ? input.method : "GET") || "GET";
14
+ const parsed = parseSupabaseURL(url, method);
15
+ if (!parsed) {
16
+ return originalFetch.call(this, input, init);
17
+ }
18
+ const rootSpan = tracer.startSpan(`supabase.${parsed.service} ${parsed.detail}`, {
19
+ attributes: {
20
+ "supabase.ref": parsed.ref,
21
+ "supabase.service": parsed.service,
22
+ "supabase.operation": parsed.operation,
23
+ "supabase.detail": parsed.detail,
24
+ "http.method": method.toUpperCase(),
25
+ "http.url": url.split("?")[0],
26
+ "peer.service": `supabase.${parsed.service}`,
27
+ "session.id": sessionId,
28
+ ...(parsed.service === "postgrest" ? {
29
+ "db.system": "postgresql",
30
+ "db.operation": parsed.operation,
31
+ "db.sql.table": parsed.table,
32
+ } : {}),
33
+ },
34
+ });
35
+ const rootCtx = trace.setSpan(context.active(), rootSpan);
36
+ const startMs = performance.now();
37
+ try {
38
+ const response = await originalFetch.call(this, input, init);
39
+ const durationMs = performance.now() - startMs;
40
+ rootSpan.setAttribute("http.status_code", response.status);
41
+ rootSpan.setAttribute("supabase.duration_ms", Math.round(durationMs));
42
+ context.with(rootCtx, () => {
43
+ createChildSpans(tracer, parsed, method, response.status, durationMs, sessionId);
44
+ });
45
+ if (response.status >= 400) {
46
+ rootSpan.setStatus({ code: SpanStatusCode.ERROR, message: `HTTP ${response.status}` });
47
+ addBreadcrumb({ timestamp: Date.now(), category: "supabase", message: `${parsed.detail} → ${response.status}`, level: "error" });
48
+ }
49
+ else {
50
+ rootSpan.setStatus({ code: SpanStatusCode.OK });
51
+ addBreadcrumb({ timestamp: Date.now(), category: "supabase", message: `${parsed.detail} → ${response.status} (${Math.round(durationMs)}ms)`, level: "info" });
52
+ }
53
+ rootSpan.end();
54
+ return response;
55
+ }
56
+ catch (err) {
57
+ rootSpan.setStatus({ code: SpanStatusCode.ERROR, message: err instanceof Error ? err.message : "fetch failed" });
58
+ if (err instanceof Error)
59
+ rootSpan.recordException(err);
60
+ rootSpan.end();
61
+ addBreadcrumb({ timestamp: Date.now(), category: "supabase", message: `${parsed.detail} → FAILED`, level: "error" });
62
+ throw err;
63
+ }
64
+ };
65
+ return () => { window.fetch = originalFetch; };
66
+ }
67
+ function createChildSpans(tracer, parsed, method, status, _durationMs, sessionId) {
68
+ const synth = { "session.id": sessionId, "supabase.ref": parsed.ref, "span.synthetic": "true" };
69
+ const gatewaySpan = tracer.startSpan("supabase.gateway", {
70
+ attributes: {
71
+ ...synth,
72
+ "http.method": method.toUpperCase(),
73
+ "http.status_code": status,
74
+ "peer.service": "supabase.kong",
75
+ },
76
+ });
77
+ const gatewayCtx = trace.setSpan(context.active(), gatewaySpan);
78
+ context.with(gatewayCtx, () => {
79
+ if (parsed.service === "postgrest") {
80
+ const dbSpan = tracer.startSpan("supabase.db.query", {
81
+ attributes: {
82
+ ...synth,
83
+ "db.system": "postgresql",
84
+ "db.operation": parsed.operation,
85
+ "db.sql.table": parsed.table,
86
+ "db.statement": parsed.detail,
87
+ "peer.service": "supabase.postgresql",
88
+ },
89
+ });
90
+ if (status >= 400)
91
+ dbSpan.setStatus({ code: SpanStatusCode.ERROR, message: `HTTP ${status}` });
92
+ else
93
+ dbSpan.setStatus({ code: SpanStatusCode.OK });
94
+ dbSpan.end();
95
+ }
96
+ if (parsed.service === "auth") {
97
+ const authSpan = tracer.startSpan("supabase.auth." + parsed.operation, {
98
+ attributes: { ...synth, "auth.operation": parsed.operation, "peer.service": "supabase.gotrue" },
99
+ });
100
+ if (status >= 400)
101
+ authSpan.setStatus({ code: SpanStatusCode.ERROR, message: `HTTP ${status}` });
102
+ else
103
+ authSpan.setStatus({ code: SpanStatusCode.OK });
104
+ authSpan.end();
105
+ }
106
+ if (parsed.service === "storage") {
107
+ const storageSpan = tracer.startSpan("supabase.storage." + parsed.operation, {
108
+ attributes: { ...synth, "storage.operation": parsed.operation, "peer.service": "supabase.storage" },
109
+ });
110
+ if (status >= 400)
111
+ storageSpan.setStatus({ code: SpanStatusCode.ERROR, message: `HTTP ${status}` });
112
+ else
113
+ storageSpan.setStatus({ code: SpanStatusCode.OK });
114
+ storageSpan.end();
115
+ }
116
+ if (parsed.service === "edge-function") {
117
+ const fnName = parsed.operation.replace("invoke:", "");
118
+ const fnSpan = tracer.startSpan("supabase.function." + fnName, {
119
+ attributes: { ...synth, "faas.name": fnName, "faas.trigger": "http", "peer.service": "supabase.edge-runtime" },
120
+ });
121
+ if (status >= 400)
122
+ fnSpan.setStatus({ code: SpanStatusCode.ERROR, message: `HTTP ${status}` });
123
+ else
124
+ fnSpan.setStatus({ code: SpanStatusCode.OK });
125
+ fnSpan.end();
126
+ }
127
+ });
128
+ if (status >= 400)
129
+ gatewaySpan.setStatus({ code: SpanStatusCode.ERROR, message: `HTTP ${status}` });
130
+ else
131
+ gatewaySpan.setStatus({ code: SpanStatusCode.OK });
132
+ gatewaySpan.end();
133
+ }
@@ -0,0 +1,104 @@
1
+ const SUPABASE_DOMAIN = ".supabase.co";
2
+ export function isSupabaseURL(url) {
3
+ try {
4
+ const u = new URL(url);
5
+ return u.hostname.endsWith(SUPABASE_DOMAIN);
6
+ }
7
+ catch {
8
+ return false;
9
+ }
10
+ }
11
+ export function parseSupabaseURL(url, method) {
12
+ let u;
13
+ try {
14
+ u = new URL(url);
15
+ }
16
+ catch {
17
+ return null;
18
+ }
19
+ if (!u.hostname.endsWith(SUPABASE_DOMAIN))
20
+ return null;
21
+ const ref = u.hostname.replace(SUPABASE_DOMAIN, "");
22
+ const path = u.pathname;
23
+ const segments = path.split("/").filter(Boolean);
24
+ if (segments[0] === "rest" && segments[1] === "v1" && segments[2]) {
25
+ const table = segments[2];
26
+ const op = restMethodToOp(method);
27
+ const select = u.searchParams.get("select") || "*";
28
+ const filters = extractFilters(u.searchParams);
29
+ const detail = op === "SELECT" ? `${op} ${select} FROM ${table}` : `${op} ${table}`;
30
+ return { ref, service: "postgrest", operation: op, table, detail: filters ? `${detail} WHERE ${filters}` : detail };
31
+ }
32
+ if (segments[0] === "auth" && segments[1] === "v1") {
33
+ const action = segments[2] || "session";
34
+ const op = authActionToOp(action);
35
+ return { ref, service: "auth", operation: op, table: "", detail: `AUTH ${op}` };
36
+ }
37
+ if (segments[0] === "storage" && segments[1] === "v1") {
38
+ const subCmd = segments[2] || "object";
39
+ const bucket = segments[3] || "";
40
+ const filePath = segments.slice(4).join("/");
41
+ const op = `${method.toUpperCase()} ${subCmd}`;
42
+ return { ref, service: "storage", operation: op, table: "", detail: bucket ? `STORAGE ${op} ${bucket}/${filePath}` : `STORAGE ${op}` };
43
+ }
44
+ if (segments[0] === "realtime") {
45
+ return { ref, service: "realtime", operation: "subscribe", table: "", detail: "REALTIME subscribe" };
46
+ }
47
+ if (segments[0] === "functions" && segments[1] === "v1" && segments[2]) {
48
+ const fnName = segments[2];
49
+ return { ref, service: "edge-function", operation: `invoke:${fnName}`, table: "", detail: `EDGE FUNCTION ${fnName}` };
50
+ }
51
+ return { ref, service: "unknown", operation: method.toUpperCase(), table: "", detail: `${method.toUpperCase()} ${path}` };
52
+ }
53
+ function restMethodToOp(method) {
54
+ switch (method.toUpperCase()) {
55
+ case "GET": return "SELECT";
56
+ case "POST": return "INSERT";
57
+ case "PATCH": return "UPDATE";
58
+ case "PUT": return "UPSERT";
59
+ case "DELETE": return "DELETE";
60
+ default: return method.toUpperCase();
61
+ }
62
+ }
63
+ function authActionToOp(action) {
64
+ switch (action) {
65
+ case "token": return "login";
66
+ case "signup": return "signup";
67
+ case "logout": return "logout";
68
+ case "recover": return "recover";
69
+ case "magiclink": return "magic_link";
70
+ case "otp": return "otp";
71
+ case "user": return "get_user";
72
+ case "callback": return "oauth_callback";
73
+ default: return action;
74
+ }
75
+ }
76
+ function extractFilters(params) {
77
+ const filters = [];
78
+ for (const [key, value] of params.entries()) {
79
+ if (key === "select" || key === "apikey" || key === "order" || key === "limit" || key === "offset")
80
+ continue;
81
+ const match = value.match(/^(eq|neq|gt|gte|lt|lte|like|ilike|is|in|cs|cd|not)\.(.+)/);
82
+ if (match) {
83
+ filters.push(`${key} ${match[1]} ${match[2]}`);
84
+ }
85
+ }
86
+ return filters.join(" AND ");
87
+ }
88
+ export function enrichSupabaseSpan(span, url, method) {
89
+ const parsed = parseSupabaseURL(url, method);
90
+ if (!parsed)
91
+ return;
92
+ span.setAttribute("supabase.ref", parsed.ref);
93
+ span.setAttribute("supabase.service", parsed.service);
94
+ span.setAttribute("supabase.operation", parsed.operation);
95
+ span.setAttribute("supabase.detail", parsed.detail);
96
+ span.setAttribute("peer.service", `supabase.${parsed.service}`);
97
+ if (parsed.service === "postgrest") {
98
+ span.setAttribute("db.system", "postgresql");
99
+ span.setAttribute("db.operation", parsed.operation);
100
+ if (parsed.table)
101
+ span.setAttribute("db.sql.table", parsed.table);
102
+ }
103
+ span.updateName(`supabase.${parsed.service} ${parsed.detail}`);
104
+ }