@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.
- package/dist/browser/console.js +30 -6
- package/dist/browser/errors.js +51 -18
- package/dist/browser/index.js +45 -8
- package/dist/browser/resources.js +14 -9
- package/dist/browser/supabase-intercept.js +133 -0
- package/dist/browser/supabase.js +104 -0
- package/dist/browser_entry.bundle.js +2535 -419
- package/dist/browser_entry.bundle.js.map +4 -4
- package/dist/core/otel-web-setup.js +30 -3
- package/dist/types/browser/console.d.ts +2 -1
- package/dist/types/browser/errors.d.ts +2 -1
- package/dist/types/browser/supabase-intercept.d.ts +2 -0
- package/dist/types/browser/supabase.d.ts +12 -0
- package/dist/types/core/otel-web-setup.d.ts +2 -0
- package/package.json +3 -1
package/dist/browser/console.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
74
|
-
|
|
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 () => {
|
package/dist/browser/errors.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
-
"
|
|
16
|
-
|
|
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
|
|
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
|
-
"
|
|
50
|
-
|
|
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);
|
package/dist/browser/index.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
7
|
-
const
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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
|
+
}
|