@orbitai/orbitrace 1.2.0 → 1.3.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/README.md +47 -9
- package/dist/browser.cjs +520 -0
- package/dist/browser.mjs +525 -0
- package/dist/index.js +480 -503
- package/package.json +16 -4
- package/dist/index.mjs +0 -131
package/README.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# @orbitai/orbitrace
|
|
2
2
|
|
|
3
|
-
Error tracking and monitoring for
|
|
3
|
+
Error tracking and monitoring for Node.js services and browser apps.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
The package has two entries. `@orbitai/orbitrace` runs on servers with the project's API key, which is a secret. `@orbitai/orbitrace/browser` runs in browsers with the project's public key, which is accepted only from the origins allowed in the Orbitrace dashboard. Never put the API key in a browser bundle; the server entry throws if it is loaded in a browser.
|
|
6
6
|
|
|
7
7
|
## Installation
|
|
8
8
|
|
|
@@ -47,7 +47,7 @@ const orbitrace = getOrbitraceInstance({ service: "api" });
|
|
|
47
47
|
try {
|
|
48
48
|
// Your code here
|
|
49
49
|
} catch (error) {
|
|
50
|
-
|
|
50
|
+
orbitrace.captureException(error, {
|
|
51
51
|
endpoint: "/users",
|
|
52
52
|
action: "createUser",
|
|
53
53
|
});
|
|
@@ -57,15 +57,16 @@ try {
|
|
|
57
57
|
### 3. Capture Messages
|
|
58
58
|
|
|
59
59
|
```javascript
|
|
60
|
-
|
|
60
|
+
orbitrace.captureMessage("User registered", {
|
|
61
61
|
userId: "123",
|
|
62
62
|
});
|
|
63
63
|
```
|
|
64
64
|
|
|
65
65
|
### 4. Delivery Behavior
|
|
66
66
|
|
|
67
|
-
Orbitrace never throws and never blocks your application
|
|
67
|
+
Orbitrace never throws and never blocks your application:
|
|
68
68
|
|
|
69
|
+
- `captureException` and `captureMessage` return as soon as the event is queued. The request is sent in the background, so a request handler that captures an error does not wait for Orbitrace before answering its own client. Awaiting them is harmless but waits for nothing; use `flush()` when you need to know the events were delivered.
|
|
69
70
|
- Each request times out after 5 seconds. Failures are logged to the console and swallowed.
|
|
70
71
|
- Repeats of the same event (same message and stack) within 10 seconds are counted locally and sent as one request with an occurrence count, so an error raised in a loop costs at most two requests per 10 seconds.
|
|
71
72
|
- At most 10 requests are in flight at once; events beyond that are dropped.
|
|
@@ -79,7 +80,7 @@ await orbitrace.flush();
|
|
|
79
80
|
|
|
80
81
|
### 5. Automatic Capture
|
|
81
82
|
|
|
82
|
-
Pass `autoCapture: true` to `getInstance` (SDK 1.2.
|
|
83
|
+
Pass `autoCapture: true` to `getInstance` (SDK 1.2.1 or newer) to capture what nobody handled and record breadcrumbs automatically. It is off unless you turn it on, so upgrading never changes how an application crashes or logs.
|
|
83
84
|
|
|
84
85
|
```javascript
|
|
85
86
|
orbitrace.getInstance({ ...config, autoCapture: true });
|
|
@@ -130,6 +131,43 @@ exports.checkout = onRequest(orbitrace.wrap(async (req, res) => {
|
|
|
130
131
|
}));
|
|
131
132
|
```
|
|
132
133
|
|
|
134
|
+
## Browser Setup
|
|
135
|
+
|
|
136
|
+
Install the same package and import the browser entry. It needs a browser with `fetch`, so every current browser.
|
|
137
|
+
|
|
138
|
+
```javascript
|
|
139
|
+
import Orbitrace from "@orbitai/orbitrace/browser";
|
|
140
|
+
|
|
141
|
+
Orbitrace.init({
|
|
142
|
+
publicKey: "the project's public key",
|
|
143
|
+
orgId: "your-org-id",
|
|
144
|
+
projectId: "your-project-id",
|
|
145
|
+
endpoint: "your-endpoint",
|
|
146
|
+
environment: import.meta.env.MODE,
|
|
147
|
+
version: "1.0.0",
|
|
148
|
+
service: "webapp",
|
|
149
|
+
});
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
The public key can be read by anyone who opens your site. That is fine: it only creates events, the API accepts it only from the origins allowed for the project (dashboard, Documentation, Browser apps), and the project's rate limit and your organization's quota bound what it can do. `init` throws if it is given an `apiKey`.
|
|
153
|
+
|
|
154
|
+
Automatic capture is on unless `autoCapture: false`. Uncaught errors and unhandled promise rejections are reported; breadcrumbs are recorded from `console` calls, `fetch` requests, clicks (the element, never its text) and navigation. Switch parts off with `autoCapture: { console: false, clicks: false }`; the parts are `exceptions`, `console`, `fetch`, `clicks` and `navigation`.
|
|
155
|
+
|
|
156
|
+
`captureException`, `captureMessage`, `addBreadcrumb`, `flush` and `close` work as on servers. There are no scopes: one tab is one scope.
|
|
157
|
+
|
|
158
|
+
```javascript
|
|
159
|
+
// After sign-in, so issues show who was affected. Send a stable id, never personal data.
|
|
160
|
+
Orbitrace.setUser({ id: user.uid });
|
|
161
|
+
// On sign-out:
|
|
162
|
+
Orbitrace.setUser(null);
|
|
163
|
+
|
|
164
|
+
// Vue 3: errors Vue catches inside components are reported with the component name,
|
|
165
|
+
// then passed to the app's own errorHandler, or logged as Vue would.
|
|
166
|
+
app.use(Orbitrace.vue);
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
Every browser event carries `page`: the URL (without query string or fragment), referrer, viewport and language. Requests use `keepalive`, so an error reported just before the page closes still arrives. Production stacks are minified; issues still group correctly across deploys because file hashes and line numbers are ignored, so keep `version` set to your release to see which build introduced a problem.
|
|
170
|
+
|
|
133
171
|
## Data Collected
|
|
134
172
|
|
|
135
173
|
Each event stores:
|
|
@@ -137,7 +175,7 @@ Each event stores:
|
|
|
137
175
|
- The error message (truncated to 200 characters) and stack trace, or the message text.
|
|
138
176
|
- The `metadata` object you pass, plus `env`, `version` and `service` from your configuration.
|
|
139
177
|
- Breadcrumbs, when there are any: the last 30 recorded before the event.
|
|
140
|
-
- The IP address and user agent of the server that sent the request, with an approximate location derived from that IP.
|
|
178
|
+
- The IP address and user agent of the server that sent the request, with an approximate location derived from that IP. For browser events, the user agent and approximate location of the user; their IP address is not stored.
|
|
141
179
|
|
|
142
180
|
Before sending, metadata values whose key contains `password`, `passwd`, `secret`, `token`, `authorization`, `cookie`, `apikey`, `privatekey`, `credential` or `session` (ignoring case, `-` and `_`) are replaced with `[Filtered]`, at any depth. Error messages and stack traces are sent as they are.
|
|
143
181
|
|
|
@@ -157,6 +195,6 @@ ORBITRACE_DISABLED=false
|
|
|
157
195
|
|
|
158
196
|
## Security Model
|
|
159
197
|
|
|
160
|
-
|
|
198
|
+
Each project has two keys. The API key authenticates servers: keep it only in backend environments such as servers, workers, functions, and CI jobs, and never in `VITE_`, `NEXT_PUBLIC_` or other public frontend environment variables. The public key authenticates browsers: it may ship in bundles, and the API accepts it only from the project's allowed origins. Both keys can only create events.
|
|
161
199
|
|
|
162
|
-
|
|
200
|
+
The server entry throws if it is initialized in a browser, and the browser entry throws if it is given an API key.
|
package/dist/browser.cjs
ADDED
|
@@ -0,0 +1,520 @@
|
|
|
1
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
2
|
+
var __typeError = (msg) => {
|
|
3
|
+
throw TypeError(msg);
|
|
4
|
+
};
|
|
5
|
+
var __commonJS = (cb, mod) => function __require() {
|
|
6
|
+
return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
|
|
7
|
+
};
|
|
8
|
+
var __accessCheck = (obj, member, msg) => member.has(obj) || __typeError("Cannot " + msg);
|
|
9
|
+
var __privateGet = (obj, member, getter) => (__accessCheck(obj, member, "read from private field"), getter ? getter.call(obj) : member.get(obj));
|
|
10
|
+
var __privateAdd = (obj, member, value) => member.has(obj) ? __typeError("Cannot add the same private member more than once") : member instanceof WeakSet ? member.add(obj) : member.set(obj, value);
|
|
11
|
+
var __privateSet = (obj, member, value, setter) => (__accessCheck(obj, member, "write to private field"), setter ? setter.call(obj, value) : member.set(obj, value), value);
|
|
12
|
+
var __privateMethod = (obj, member, method) => (__accessCheck(obj, member, "access private method"), method);
|
|
13
|
+
|
|
14
|
+
// src/shared.js
|
|
15
|
+
var require_shared = __commonJS({
|
|
16
|
+
"src/shared.js"(exports2, module2) {
|
|
17
|
+
var REQUEST_TIMEOUT_MS = 5e3;
|
|
18
|
+
var DEDUPE_WINDOW_MS = 1e4;
|
|
19
|
+
var MAX_IN_FLIGHT = 10;
|
|
20
|
+
var DEFAULT_RETRY_AFTER_SECONDS = 60;
|
|
21
|
+
var MAX_BREADCRUMBS = 30;
|
|
22
|
+
var MAX_BREADCRUMB_MESSAGE_LENGTH = 200;
|
|
23
|
+
var BREADCRUMB_LEVELS = ["debug", "info", "warning", "error"];
|
|
24
|
+
var CONSOLE_LEVELS = { debug: "debug", log: "info", info: "info", warn: "warning", error: "error" };
|
|
25
|
+
var SENSITIVE_KEY_PARTS = [
|
|
26
|
+
"password",
|
|
27
|
+
"passwd",
|
|
28
|
+
"secret",
|
|
29
|
+
"token",
|
|
30
|
+
"authorization",
|
|
31
|
+
"cookie",
|
|
32
|
+
"apikey",
|
|
33
|
+
"privatekey",
|
|
34
|
+
"credential",
|
|
35
|
+
"session"
|
|
36
|
+
];
|
|
37
|
+
var FILTERED_VALUE = "[Filtered]";
|
|
38
|
+
function isSensitiveKey(key) {
|
|
39
|
+
const normalized = key.toLowerCase().replace(/[-_]/g, "");
|
|
40
|
+
return SENSITIVE_KEY_PARTS.some((part) => normalized.includes(part));
|
|
41
|
+
}
|
|
42
|
+
function filterSensitive2(value, ancestors = /* @__PURE__ */ new WeakSet()) {
|
|
43
|
+
if (value === null || typeof value !== "object") {
|
|
44
|
+
return value;
|
|
45
|
+
}
|
|
46
|
+
if (ancestors.has(value)) {
|
|
47
|
+
return "[Circular]";
|
|
48
|
+
}
|
|
49
|
+
const isArray = Array.isArray(value);
|
|
50
|
+
const prototype = Object.getPrototypeOf(value);
|
|
51
|
+
if (!isArray && prototype !== Object.prototype && prototype !== null) {
|
|
52
|
+
return value;
|
|
53
|
+
}
|
|
54
|
+
ancestors.add(value);
|
|
55
|
+
const filtered = isArray ? value.map((item) => filterSensitive2(item, ancestors)) : Object.fromEntries(
|
|
56
|
+
Object.entries(value).map(([key, item]) => [
|
|
57
|
+
key,
|
|
58
|
+
isSensitiveKey(key) ? FILTERED_VALUE : filterSensitive2(item, ancestors)
|
|
59
|
+
])
|
|
60
|
+
);
|
|
61
|
+
ancestors.delete(value);
|
|
62
|
+
return filtered;
|
|
63
|
+
}
|
|
64
|
+
function truncate2(value, maxLength) {
|
|
65
|
+
return value.length > maxLength ? `${value.slice(0, maxLength)}...` : value;
|
|
66
|
+
}
|
|
67
|
+
function cleanUrl2(url) {
|
|
68
|
+
try {
|
|
69
|
+
const parsed = new URL(url);
|
|
70
|
+
return `${parsed.origin}${parsed.pathname}`;
|
|
71
|
+
} catch {
|
|
72
|
+
return String(url).split(/[?#]/)[0];
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
function pushBreadcrumb2(breadcrumbs, { category = "manual", message = "", level = "info", data } = {}) {
|
|
76
|
+
try {
|
|
77
|
+
breadcrumbs.push({
|
|
78
|
+
timestamp: Date.now(),
|
|
79
|
+
category: String(category).slice(0, 64),
|
|
80
|
+
level: BREADCRUMB_LEVELS.includes(level) ? level : "info",
|
|
81
|
+
message: truncate2(String(message), MAX_BREADCRUMB_MESSAGE_LENGTH),
|
|
82
|
+
...data !== void 0 && { data: filterSensitive2(data) }
|
|
83
|
+
});
|
|
84
|
+
if (breadcrumbs.length > MAX_BREADCRUMBS) {
|
|
85
|
+
breadcrumbs.shift();
|
|
86
|
+
}
|
|
87
|
+
} catch {
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
function httpBreadcrumb({ method, url, status, error, started }) {
|
|
91
|
+
const data = { method, url: cleanUrl2(url), durationMs: Date.now() - started };
|
|
92
|
+
if (error) {
|
|
93
|
+
return { category: "http", level: "error", message: `${method} ${data.url} failed`, data: { ...data, error } };
|
|
94
|
+
}
|
|
95
|
+
return {
|
|
96
|
+
category: "http",
|
|
97
|
+
level: status >= 500 ? "error" : status >= 400 ? "warning" : "info",
|
|
98
|
+
message: `${method} ${data.url} ${status}`,
|
|
99
|
+
data: { ...data, status }
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
function recordFetch2({ endpoint, addBreadcrumb, restore }) {
|
|
103
|
+
if (typeof globalThis.fetch !== "function") {
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
const originalFetch = globalThis.fetch;
|
|
107
|
+
globalThis.fetch = async function orbitraceFetch(input, init) {
|
|
108
|
+
const url = typeof input === "string" || input instanceof URL ? String(input) : input?.url;
|
|
109
|
+
if (!url || url.startsWith(endpoint)) {
|
|
110
|
+
return originalFetch.call(this, input, init);
|
|
111
|
+
}
|
|
112
|
+
const method = String(init?.method || input?.method || "GET").toUpperCase();
|
|
113
|
+
const started = Date.now();
|
|
114
|
+
try {
|
|
115
|
+
const response = await originalFetch.call(this, input, init);
|
|
116
|
+
addBreadcrumb(httpBreadcrumb({ method, url, status: response.status, started }));
|
|
117
|
+
return response;
|
|
118
|
+
} catch (error) {
|
|
119
|
+
addBreadcrumb(httpBreadcrumb({ method, url, error: error?.message, started }));
|
|
120
|
+
throw error;
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
restore.push(() => {
|
|
124
|
+
globalThis.fetch = originalFetch;
|
|
125
|
+
});
|
|
126
|
+
return originalFetch;
|
|
127
|
+
}
|
|
128
|
+
function recordConsole2({ format, addBreadcrumb, restore }) {
|
|
129
|
+
let recording = false;
|
|
130
|
+
for (const [method, level] of Object.entries(CONSOLE_LEVELS)) {
|
|
131
|
+
const original = console[method];
|
|
132
|
+
if (typeof original !== "function") {
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
console[method] = function orbitraceConsole(...args) {
|
|
136
|
+
if (!recording) {
|
|
137
|
+
recording = true;
|
|
138
|
+
try {
|
|
139
|
+
const message = format(args);
|
|
140
|
+
if (!message.startsWith("Orbitrace:")) {
|
|
141
|
+
addBreadcrumb({ category: "console", level, message });
|
|
142
|
+
}
|
|
143
|
+
} catch {
|
|
144
|
+
} finally {
|
|
145
|
+
recording = false;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return original.apply(this, args);
|
|
149
|
+
};
|
|
150
|
+
restore.push(() => {
|
|
151
|
+
console[method] = original;
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
var _endpoint, _headers, _getFetch, _requestOptions, _pending, _inFlight, _pausedUntil, _Sender_instances, flushPending_fn, canSend_fn, send_fn, post_fn;
|
|
156
|
+
var Sender2 = class {
|
|
157
|
+
// `getFetch` returns the fetch to use at send time, so a wrapped global fetch is never recorded.
|
|
158
|
+
// `requestOptions(body)` adds fetch options for one request, such as keepalive in browsers.
|
|
159
|
+
constructor({ endpoint, headers, getFetch = () => globalThis.fetch, requestOptions = () => ({}) }) {
|
|
160
|
+
__privateAdd(this, _Sender_instances);
|
|
161
|
+
__privateAdd(this, _endpoint);
|
|
162
|
+
__privateAdd(this, _headers);
|
|
163
|
+
__privateAdd(this, _getFetch);
|
|
164
|
+
__privateAdd(this, _requestOptions);
|
|
165
|
+
__privateAdd(this, _pending, /* @__PURE__ */ new Map());
|
|
166
|
+
__privateAdd(this, _inFlight, /* @__PURE__ */ new Set());
|
|
167
|
+
__privateAdd(this, _pausedUntil, 0);
|
|
168
|
+
__privateSet(this, _endpoint, endpoint);
|
|
169
|
+
__privateSet(this, _headers, headers);
|
|
170
|
+
__privateSet(this, _getFetch, getFetch);
|
|
171
|
+
__privateSet(this, _requestOptions, requestOptions);
|
|
172
|
+
}
|
|
173
|
+
// Queues the event and returns at once; it is sent in the background. flush() waits for delivery.
|
|
174
|
+
capture(event, payload) {
|
|
175
|
+
const key = JSON.stringify([event, payload.message, payload.stack ?? null]);
|
|
176
|
+
const pending = __privateGet(this, _pending).get(key);
|
|
177
|
+
if (pending) {
|
|
178
|
+
pending.count++;
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
if (!__privateMethod(this, _Sender_instances, canSend_fn).call(this)) {
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
const entry = { event, payload, count: 0 };
|
|
185
|
+
entry.timer = setTimeout(() => __privateMethod(this, _Sender_instances, flushPending_fn).call(this, key), DEDUPE_WINDOW_MS);
|
|
186
|
+
entry.timer.unref?.();
|
|
187
|
+
__privateGet(this, _pending).set(key, entry);
|
|
188
|
+
__privateMethod(this, _Sender_instances, send_fn).call(this, event, payload, 1);
|
|
189
|
+
}
|
|
190
|
+
// Sends counts still held by the dedupe window and waits for in-flight requests.
|
|
191
|
+
async flush() {
|
|
192
|
+
await Promise.all([...__privateGet(this, _pending).keys()].map((key) => __privateMethod(this, _Sender_instances, flushPending_fn).call(this, key)));
|
|
193
|
+
await Promise.all(__privateGet(this, _inFlight));
|
|
194
|
+
}
|
|
195
|
+
};
|
|
196
|
+
_endpoint = new WeakMap();
|
|
197
|
+
_headers = new WeakMap();
|
|
198
|
+
_getFetch = new WeakMap();
|
|
199
|
+
_requestOptions = new WeakMap();
|
|
200
|
+
_pending = new WeakMap();
|
|
201
|
+
_inFlight = new WeakMap();
|
|
202
|
+
_pausedUntil = new WeakMap();
|
|
203
|
+
_Sender_instances = new WeakSet();
|
|
204
|
+
flushPending_fn = async function(key) {
|
|
205
|
+
const entry = __privateGet(this, _pending).get(key);
|
|
206
|
+
if (!entry) {
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
__privateGet(this, _pending).delete(key);
|
|
210
|
+
clearTimeout(entry.timer);
|
|
211
|
+
if (entry.count > 0 && __privateMethod(this, _Sender_instances, canSend_fn).call(this)) {
|
|
212
|
+
await __privateMethod(this, _Sender_instances, send_fn).call(this, entry.event, entry.payload, entry.count);
|
|
213
|
+
}
|
|
214
|
+
};
|
|
215
|
+
canSend_fn = function() {
|
|
216
|
+
return Date.now() >= __privateGet(this, _pausedUntil) && __privateGet(this, _inFlight).size < MAX_IN_FLIGHT;
|
|
217
|
+
};
|
|
218
|
+
send_fn = function(event, payload, count) {
|
|
219
|
+
const request = __privateMethod(this, _Sender_instances, post_fn).call(this, event, payload, count).finally(() => __privateGet(this, _inFlight).delete(request));
|
|
220
|
+
__privateGet(this, _inFlight).add(request);
|
|
221
|
+
return request;
|
|
222
|
+
};
|
|
223
|
+
post_fn = async function(event, payload, count) {
|
|
224
|
+
try {
|
|
225
|
+
const body = JSON.stringify({ event, ...count > 1 && { count }, payload });
|
|
226
|
+
const response = await __privateGet(this, _getFetch).call(this)(__privateGet(this, _endpoint), {
|
|
227
|
+
method: "POST",
|
|
228
|
+
headers: { "Content-Type": "application/json", ...__privateGet(this, _headers) },
|
|
229
|
+
body,
|
|
230
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
231
|
+
...__privateGet(this, _requestOptions).call(this, body)
|
|
232
|
+
});
|
|
233
|
+
if (response.status === 429) {
|
|
234
|
+
const retryAfterSeconds = Number(response.headers.get("retry-after")) || DEFAULT_RETRY_AFTER_SECONDS;
|
|
235
|
+
__privateSet(this, _pausedUntil, Date.now() + retryAfterSeconds * 1e3);
|
|
236
|
+
console.warn(`Orbitrace: rate limited, dropping events for ${retryAfterSeconds}s`);
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
if (!response.ok) {
|
|
240
|
+
console.error(`Orbitrace: API responded with status ${response.status}`);
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
return await response.json();
|
|
244
|
+
} catch (error) {
|
|
245
|
+
console.error("Orbitrace: Error sending data to API", error.message);
|
|
246
|
+
}
|
|
247
|
+
};
|
|
248
|
+
module2.exports = {
|
|
249
|
+
MAX_BREADCRUMB_MESSAGE_LENGTH,
|
|
250
|
+
Sender: Sender2,
|
|
251
|
+
cleanUrl: cleanUrl2,
|
|
252
|
+
filterSensitive: filterSensitive2,
|
|
253
|
+
pushBreadcrumb: pushBreadcrumb2,
|
|
254
|
+
recordConsole: recordConsole2,
|
|
255
|
+
recordFetch: recordFetch2,
|
|
256
|
+
truncate: truncate2
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
// src/browser.js
|
|
262
|
+
var { Sender, cleanUrl, filterSensitive, pushBreadcrumb, recordConsole, recordFetch, truncate } = require_shared();
|
|
263
|
+
var MAX_KEEPALIVE_BODY_BYTES = 60 * 1024;
|
|
264
|
+
var MAX_CONSOLE_ARG_LENGTH = 200;
|
|
265
|
+
function formatConsoleArgs(args) {
|
|
266
|
+
return args.map((arg) => {
|
|
267
|
+
if (typeof arg === "string") {
|
|
268
|
+
return arg;
|
|
269
|
+
}
|
|
270
|
+
if (arg instanceof Error) {
|
|
271
|
+
return arg.stack || arg.message;
|
|
272
|
+
}
|
|
273
|
+
try {
|
|
274
|
+
return truncate(JSON.stringify(arg), MAX_CONSOLE_ARG_LENGTH);
|
|
275
|
+
} catch {
|
|
276
|
+
return String(arg);
|
|
277
|
+
}
|
|
278
|
+
}).join(" ");
|
|
279
|
+
}
|
|
280
|
+
function describeElement(element) {
|
|
281
|
+
if (!element || typeof element.tagName !== "string") {
|
|
282
|
+
return "unknown";
|
|
283
|
+
}
|
|
284
|
+
const tag = element.tagName.toLowerCase();
|
|
285
|
+
const id = element.id ? `#${element.id}` : "";
|
|
286
|
+
const classes = typeof element.className === "string" && element.className.trim() ? `.${element.className.trim().split(/\s+/).slice(0, 3).join(".")}` : "";
|
|
287
|
+
return `${tag}${id}${classes}`;
|
|
288
|
+
}
|
|
289
|
+
var _config, _sender, _breadcrumbs, _user, _restore, _fetch, _Orbitrace_static, autoCaptureOptions_fn, installAutoCapture_fn, capture_fn;
|
|
290
|
+
var _Orbitrace = class _Orbitrace {
|
|
291
|
+
// Automatic capture is on unless autoCapture is false or parts of it are switched off.
|
|
292
|
+
static init(config) {
|
|
293
|
+
this.assertBrowserRuntime();
|
|
294
|
+
this.validateConfig(config);
|
|
295
|
+
this.close();
|
|
296
|
+
__privateSet(this, _config, {
|
|
297
|
+
orgId: config.orgId,
|
|
298
|
+
projectId: config.projectId,
|
|
299
|
+
endpoint: config.endpoint,
|
|
300
|
+
environment: config.environment || "production",
|
|
301
|
+
version: config.version || "1.0.0",
|
|
302
|
+
service: config.service || "browser",
|
|
303
|
+
disabled: config.disabled || false
|
|
304
|
+
});
|
|
305
|
+
__privateSet(this, _sender, new Sender({
|
|
306
|
+
endpoint: config.endpoint,
|
|
307
|
+
headers: {
|
|
308
|
+
"x-orbitrace-api-key": config.publicKey,
|
|
309
|
+
"x-orbitrace-org-id": config.orgId,
|
|
310
|
+
"x-orbitrace-project-id": config.projectId
|
|
311
|
+
},
|
|
312
|
+
// Orbitrace's own requests use the original fetch, so they are never recorded.
|
|
313
|
+
getFetch: () => __privateGet(this, _fetch) || window.fetch.bind(window),
|
|
314
|
+
requestOptions: (body) => ({ keepalive: body.length <= MAX_KEEPALIVE_BODY_BYTES })
|
|
315
|
+
}));
|
|
316
|
+
const autoCapture = __privateMethod(this, _Orbitrace_static, autoCaptureOptions_fn).call(this, config.autoCapture);
|
|
317
|
+
if (autoCapture && !__privateGet(this, _config).disabled) {
|
|
318
|
+
__privateMethod(this, _Orbitrace_static, installAutoCapture_fn).call(this, autoCapture);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
static validateConfig(config) {
|
|
322
|
+
if (config.apiKey) {
|
|
323
|
+
throw new Error(
|
|
324
|
+
"Orbitrace: apiKey is a server secret and must not ship in a browser bundle; use the project's publicKey"
|
|
325
|
+
);
|
|
326
|
+
}
|
|
327
|
+
const requiredFields = ["publicKey", "orgId", "projectId", "endpoint"];
|
|
328
|
+
const missingFields = requiredFields.filter((field) => !config[field]);
|
|
329
|
+
if (missingFields.length > 0) {
|
|
330
|
+
throw new Error(`Missing required configuration fields: ${missingFields.join(", ")}`);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
static assertBrowserRuntime() {
|
|
334
|
+
if (typeof window === "undefined" || typeof document === "undefined") {
|
|
335
|
+
throw new Error("Orbitrace: @orbitai/orbitrace/browser runs in browsers; use @orbitai/orbitrace on servers");
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
// Queues the event and returns at once; it is sent in the background. flush() waits for delivery.
|
|
339
|
+
static captureException(error, metadata = {}) {
|
|
340
|
+
const message = error?.message || error?.name || String(error);
|
|
341
|
+
__privateMethod(this, _Orbitrace_static, capture_fn).call(this, "capture_exception", {
|
|
342
|
+
message,
|
|
343
|
+
stack: error?.stack || message,
|
|
344
|
+
metadata
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
static captureMessage(message, metadata = {}) {
|
|
348
|
+
__privateMethod(this, _Orbitrace_static, capture_fn).call(this, "capture_message", { message, metadata });
|
|
349
|
+
}
|
|
350
|
+
// Records something that happened, sent with the next event.
|
|
351
|
+
static addBreadcrumb(breadcrumb) {
|
|
352
|
+
pushBreadcrumb(__privateGet(this, _breadcrumbs), breadcrumb);
|
|
353
|
+
}
|
|
354
|
+
// Identifies the signed-in user on every following event. Send a stable id, not personal data.
|
|
355
|
+
// Pass null when the user signs out.
|
|
356
|
+
static setUser(user) {
|
|
357
|
+
__privateSet(this, _user, user ? filterSensitive(user) : null);
|
|
358
|
+
}
|
|
359
|
+
// Sends counts still held by the dedupe window and waits for in-flight requests.
|
|
360
|
+
static async flush() {
|
|
361
|
+
await __privateGet(this, _sender)?.flush();
|
|
362
|
+
}
|
|
363
|
+
// Removes the listeners and wrappers automatic capture installed and forgets breadcrumbs and the user.
|
|
364
|
+
static close() {
|
|
365
|
+
for (const undo of __privateGet(this, _restore).splice(0).reverse()) {
|
|
366
|
+
try {
|
|
367
|
+
undo();
|
|
368
|
+
} catch {
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
__privateSet(this, _fetch, null);
|
|
372
|
+
__privateSet(this, _breadcrumbs, []);
|
|
373
|
+
__privateSet(this, _user, null);
|
|
374
|
+
}
|
|
375
|
+
// Vue 3 plugin: app.use(Orbitrace.vue). Errors Vue catches in components are captured with the component
|
|
376
|
+
// name and where Vue caught them, then passed to the handler the app had, or logged as Vue would.
|
|
377
|
+
static get vue() {
|
|
378
|
+
return {
|
|
379
|
+
install(app) {
|
|
380
|
+
const previous = app.config.errorHandler;
|
|
381
|
+
app.config.errorHandler = (error, instance, info) => {
|
|
382
|
+
_Orbitrace.captureException(error, {
|
|
383
|
+
mechanism: "vue",
|
|
384
|
+
component: instance?.$options?.name || instance?.$?.type?.__name || "anonymous",
|
|
385
|
+
info
|
|
386
|
+
});
|
|
387
|
+
if (previous) {
|
|
388
|
+
previous(error, instance, info);
|
|
389
|
+
} else {
|
|
390
|
+
console.error(error);
|
|
391
|
+
}
|
|
392
|
+
};
|
|
393
|
+
}
|
|
394
|
+
};
|
|
395
|
+
}
|
|
396
|
+
};
|
|
397
|
+
_config = new WeakMap();
|
|
398
|
+
_sender = new WeakMap();
|
|
399
|
+
_breadcrumbs = new WeakMap();
|
|
400
|
+
_user = new WeakMap();
|
|
401
|
+
_restore = new WeakMap();
|
|
402
|
+
_fetch = new WeakMap();
|
|
403
|
+
_Orbitrace_static = new WeakSet();
|
|
404
|
+
autoCaptureOptions_fn = function(value) {
|
|
405
|
+
if (value === false) {
|
|
406
|
+
return null;
|
|
407
|
+
}
|
|
408
|
+
const options = value === true || value === void 0 ? {} : value;
|
|
409
|
+
return {
|
|
410
|
+
exceptions: options.exceptions !== false,
|
|
411
|
+
console: options.console !== false,
|
|
412
|
+
fetch: options.fetch !== false,
|
|
413
|
+
clicks: options.clicks !== false,
|
|
414
|
+
navigation: options.navigation !== false
|
|
415
|
+
};
|
|
416
|
+
};
|
|
417
|
+
installAutoCapture_fn = function(options) {
|
|
418
|
+
const addBreadcrumb = (breadcrumb) => this.addBreadcrumb(breadcrumb);
|
|
419
|
+
const listen = (target, type, listener, capture = false) => {
|
|
420
|
+
target.addEventListener(type, listener, capture);
|
|
421
|
+
__privateGet(this, _restore).push(() => target.removeEventListener(type, listener, capture));
|
|
422
|
+
};
|
|
423
|
+
if (options.exceptions) {
|
|
424
|
+
listen(window, "error", (event) => {
|
|
425
|
+
const error = event.error ?? new Error(event.message || "Unknown error");
|
|
426
|
+
this.captureException(error, {
|
|
427
|
+
mechanism: "onerror",
|
|
428
|
+
...event.filename && { source: `${cleanUrl(event.filename)}:${event.lineno}:${event.colno}` }
|
|
429
|
+
});
|
|
430
|
+
});
|
|
431
|
+
listen(window, "unhandledrejection", (event) => {
|
|
432
|
+
const reason = event.reason;
|
|
433
|
+
const error = reason instanceof Error ? reason : new Error(`Unhandled rejection: ${formatConsoleArgs([reason])}`);
|
|
434
|
+
this.captureException(error, { mechanism: "unhandledrejection" });
|
|
435
|
+
});
|
|
436
|
+
}
|
|
437
|
+
if (options.console) {
|
|
438
|
+
recordConsole({ addBreadcrumb, restore: __privateGet(this, _restore), format: formatConsoleArgs });
|
|
439
|
+
}
|
|
440
|
+
if (options.fetch) {
|
|
441
|
+
__privateSet(this, _fetch, recordFetch({ endpoint: __privateGet(this, _config).endpoint, addBreadcrumb, restore: __privateGet(this, _restore) }));
|
|
442
|
+
}
|
|
443
|
+
if (options.clicks) {
|
|
444
|
+
listen(
|
|
445
|
+
document,
|
|
446
|
+
"click",
|
|
447
|
+
(event) => addBreadcrumb({ category: "ui.click", message: describeElement(event.target) }),
|
|
448
|
+
true
|
|
449
|
+
);
|
|
450
|
+
}
|
|
451
|
+
if (options.navigation) {
|
|
452
|
+
let from = cleanUrl(window.location.href);
|
|
453
|
+
const navigated = () => {
|
|
454
|
+
const to = cleanUrl(window.location.href);
|
|
455
|
+
if (to !== from) {
|
|
456
|
+
addBreadcrumb({ category: "navigation", message: `${from} -> ${to}`, data: { from, to } });
|
|
457
|
+
from = to;
|
|
458
|
+
}
|
|
459
|
+
};
|
|
460
|
+
for (const method of ["pushState", "replaceState"]) {
|
|
461
|
+
const original = window.history[method];
|
|
462
|
+
if (typeof original !== "function") {
|
|
463
|
+
continue;
|
|
464
|
+
}
|
|
465
|
+
window.history[method] = function orbitraceHistory(...args) {
|
|
466
|
+
const result = original.apply(this, args);
|
|
467
|
+
navigated();
|
|
468
|
+
return result;
|
|
469
|
+
};
|
|
470
|
+
__privateGet(this, _restore).push(() => {
|
|
471
|
+
window.history[method] = original;
|
|
472
|
+
});
|
|
473
|
+
}
|
|
474
|
+
listen(window, "popstate", navigated);
|
|
475
|
+
listen(window, "hashchange", navigated);
|
|
476
|
+
}
|
|
477
|
+
listen(window, "pagehide", () => __privateGet(this, _sender).flush());
|
|
478
|
+
};
|
|
479
|
+
capture_fn = function(event, { metadata, ...fields }) {
|
|
480
|
+
if (!__privateGet(this, _config)) {
|
|
481
|
+
console.error("Orbitrace: call Orbitrace.init() before capturing events");
|
|
482
|
+
return;
|
|
483
|
+
}
|
|
484
|
+
if (__privateGet(this, _config).disabled) {
|
|
485
|
+
return;
|
|
486
|
+
}
|
|
487
|
+
try {
|
|
488
|
+
const breadcrumbs = [...__privateGet(this, _breadcrumbs)];
|
|
489
|
+
__privateGet(this, _sender).capture(event, {
|
|
490
|
+
...fields,
|
|
491
|
+
metadata: {
|
|
492
|
+
...filterSensitive(metadata),
|
|
493
|
+
env: __privateGet(this, _config).environment,
|
|
494
|
+
version: __privateGet(this, _config).version,
|
|
495
|
+
service: __privateGet(this, _config).service,
|
|
496
|
+
...__privateGet(this, _user) && { user: __privateGet(this, _user) },
|
|
497
|
+
// Where it happened. URLs lose their query string and fragment.
|
|
498
|
+
page: {
|
|
499
|
+
url: cleanUrl(window.location.href),
|
|
500
|
+
...document.referrer && { referrer: cleanUrl(document.referrer) },
|
|
501
|
+
viewport: `${window.innerWidth}x${window.innerHeight}`,
|
|
502
|
+
language: window.navigator?.language
|
|
503
|
+
}
|
|
504
|
+
},
|
|
505
|
+
...breadcrumbs.length > 0 && { breadcrumbs }
|
|
506
|
+
});
|
|
507
|
+
} catch (error) {
|
|
508
|
+
console.error("Orbitrace: could not capture the event", error?.message);
|
|
509
|
+
}
|
|
510
|
+
};
|
|
511
|
+
__privateAdd(_Orbitrace, _Orbitrace_static);
|
|
512
|
+
__privateAdd(_Orbitrace, _config, null);
|
|
513
|
+
__privateAdd(_Orbitrace, _sender, null);
|
|
514
|
+
__privateAdd(_Orbitrace, _breadcrumbs, []);
|
|
515
|
+
__privateAdd(_Orbitrace, _user, null);
|
|
516
|
+
// Undo functions for everything automatic capture installed; close() runs them.
|
|
517
|
+
__privateAdd(_Orbitrace, _restore, []);
|
|
518
|
+
__privateAdd(_Orbitrace, _fetch, null);
|
|
519
|
+
var Orbitrace = _Orbitrace;
|
|
520
|
+
module.exports = Orbitrace;
|