@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.
- package/dist/browser/console.js +7 -0
- package/dist/browser/errors.js +13 -0
- package/dist/browser/index.js +4 -2
- 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 +280 -13
- package/dist/browser_entry.bundle.js.map +4 -4
- package/dist/core/otel-web-setup.js +14 -2
- package/dist/types/browser/supabase-intercept.d.ts +2 -0
- package/dist/types/browser/supabase.d.ts +12 -0
- package/package.json +1 -1
package/dist/browser/console.js
CHANGED
|
@@ -17,6 +17,7 @@ const SEVERITY_MAP = {
|
|
|
17
17
|
};
|
|
18
18
|
let patched = false;
|
|
19
19
|
let originals = {};
|
|
20
|
+
let emitting = false;
|
|
20
21
|
export function installConsoleCapture(tracer, logger, sessionId) {
|
|
21
22
|
if (patched || typeof console === "undefined")
|
|
22
23
|
return () => { };
|
|
@@ -29,7 +30,10 @@ export function installConsoleCapture(tracer, logger, sessionId) {
|
|
|
29
30
|
const level = LEVEL_MAP[method];
|
|
30
31
|
console[method] = (...args) => {
|
|
31
32
|
original(...args);
|
|
33
|
+
if (emitting)
|
|
34
|
+
return;
|
|
32
35
|
try {
|
|
36
|
+
emitting = true;
|
|
33
37
|
let message;
|
|
34
38
|
let attrs = {};
|
|
35
39
|
const safeStringify = (v) => {
|
|
@@ -104,6 +108,9 @@ export function installConsoleCapture(tracer, logger, sessionId) {
|
|
|
104
108
|
}
|
|
105
109
|
}
|
|
106
110
|
catch { }
|
|
111
|
+
finally {
|
|
112
|
+
emitting = false;
|
|
113
|
+
}
|
|
107
114
|
};
|
|
108
115
|
}
|
|
109
116
|
return () => {
|
package/dist/browser/errors.js
CHANGED
|
@@ -1,14 +1,18 @@
|
|
|
1
1
|
import { SpanStatusCode } from "@opentelemetry/api";
|
|
2
2
|
import { SeverityNumber } from "@opentelemetry/api-logs";
|
|
3
3
|
import { addBreadcrumb, getBreadcrumbs } from "./breadcrumbs";
|
|
4
|
+
let processing = false;
|
|
4
5
|
export function installBrowserErrorHooks(tracer, logger, sessionId) {
|
|
5
6
|
if (typeof window === "undefined") {
|
|
6
7
|
return () => undefined;
|
|
7
8
|
}
|
|
8
9
|
const onError = (ev) => {
|
|
10
|
+
if (processing)
|
|
11
|
+
return;
|
|
9
12
|
const message = ev.message || "window.error";
|
|
10
13
|
addBreadcrumb({ timestamp: Date.now(), category: "error", message, level: "error" });
|
|
11
14
|
try {
|
|
15
|
+
processing = true;
|
|
12
16
|
const breadcrumbs = getBreadcrumbs();
|
|
13
17
|
const stack = ev.error instanceof Error ? ev.error.stack || "" : "";
|
|
14
18
|
const errorType = ev.error?.constructor?.name || "Error";
|
|
@@ -40,8 +44,13 @@ export function installBrowserErrorHooks(tracer, logger, sessionId) {
|
|
|
40
44
|
span.end();
|
|
41
45
|
}
|
|
42
46
|
catch { }
|
|
47
|
+
finally {
|
|
48
|
+
processing = false;
|
|
49
|
+
}
|
|
43
50
|
};
|
|
44
51
|
const onRejection = (ev) => {
|
|
52
|
+
if (processing)
|
|
53
|
+
return;
|
|
45
54
|
let reason;
|
|
46
55
|
let stack = "";
|
|
47
56
|
let errorType = "UnhandledRejection";
|
|
@@ -55,6 +64,7 @@ export function installBrowserErrorHooks(tracer, logger, sessionId) {
|
|
|
55
64
|
}
|
|
56
65
|
addBreadcrumb({ timestamp: Date.now(), category: "error", message: reason, level: "error" });
|
|
57
66
|
try {
|
|
67
|
+
processing = true;
|
|
58
68
|
const breadcrumbs = getBreadcrumbs();
|
|
59
69
|
const attrs = {
|
|
60
70
|
"error.message": reason,
|
|
@@ -81,6 +91,9 @@ export function installBrowserErrorHooks(tracer, logger, sessionId) {
|
|
|
81
91
|
span.end();
|
|
82
92
|
}
|
|
83
93
|
catch { }
|
|
94
|
+
finally {
|
|
95
|
+
processing = false;
|
|
96
|
+
}
|
|
84
97
|
};
|
|
85
98
|
window.addEventListener("error", onError);
|
|
86
99
|
window.addEventListener("unhandledrejection", onRejection);
|
package/dist/browser/index.js
CHANGED
|
@@ -13,6 +13,7 @@ import { installResourceTiming } from "./resources";
|
|
|
13
13
|
import { installLongTaskDetection } from "./longtasks";
|
|
14
14
|
import { installMemoryTracking } from "./memory";
|
|
15
15
|
import { installOfflineSupport } from "./offline";
|
|
16
|
+
import { installSupabaseFetchInterceptor } from "./supabase-intercept";
|
|
16
17
|
const instances = new Set();
|
|
17
18
|
const replayBuffers = new Set();
|
|
18
19
|
let currentUser = null;
|
|
@@ -166,7 +167,7 @@ export function initBrowserSDK(config) {
|
|
|
166
167
|
const otel = setupOtelWeb({ ...config, tracesSampleRate: sampleRate, sessionId: replay.sessionId });
|
|
167
168
|
const tracer = otel.tracer;
|
|
168
169
|
const meter = otel.meter;
|
|
169
|
-
const logger = otel.loggerProvider.getLogger("@obtrace/sdk-browser", "2.
|
|
170
|
+
const logger = otel.loggerProvider.getLogger("@obtrace/sdk-browser", "2.4.0");
|
|
170
171
|
const client = new ObtraceClient({
|
|
171
172
|
...config,
|
|
172
173
|
replay: {
|
|
@@ -215,6 +216,7 @@ export function initBrowserSDK(config) {
|
|
|
215
216
|
cleanups.push(installResourceTiming(meter));
|
|
216
217
|
cleanups.push(installLongTaskDetection(tracer));
|
|
217
218
|
cleanups.push(installMemoryTracking(meter));
|
|
219
|
+
cleanups.push(installSupabaseFetchInterceptor(tracer, replay.sessionId));
|
|
218
220
|
if (config.captureConsole !== false) {
|
|
219
221
|
cleanups.push(installConsoleCapture(tracer, logger, replay.sessionId));
|
|
220
222
|
}
|
|
@@ -236,7 +238,7 @@ export function initBrowserSDK(config) {
|
|
|
236
238
|
}
|
|
237
239
|
}, config.replay?.flushIntervalMs ?? 5000);
|
|
238
240
|
const sendViaBeacon = () => {
|
|
239
|
-
const url = `${config.ingestBaseUrl?.replace(/\/$/, "")}/ingest/replay/chunk`;
|
|
241
|
+
const url = `${config.ingestBaseUrl?.replace(/\/$/, "")}/ingest/replay/chunk?token=${encodeURIComponent(config.apiKey)}`;
|
|
240
242
|
const freshChunk = replay.flush();
|
|
241
243
|
if (freshChunk) {
|
|
242
244
|
navigator.sendBeacon(url, new Blob([JSON.stringify(freshChunk)], { type: "application/json" }));
|
|
@@ -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
|
+
}
|