@allstak/react 0.1.2 → 0.2.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/README.md +150 -47
- package/dist/index.d.mts +552 -1
- package/dist/index.d.ts +552 -1
- package/dist/index.js +1264 -5
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1250 -6
- package/dist/index.mjs.map +1 -1
- package/package.json +11 -10
package/dist/index.mjs
CHANGED
|
@@ -1,13 +1,1257 @@
|
|
|
1
1
|
// src/index.ts
|
|
2
|
-
import
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
2
|
+
import * as React from "react";
|
|
3
|
+
|
|
4
|
+
// src/transport.ts
|
|
5
|
+
var REQUEST_TIMEOUT = 3e3;
|
|
6
|
+
var MAX_BUFFER = 100;
|
|
7
|
+
var HttpTransport = class {
|
|
8
|
+
constructor(baseUrl, apiKey) {
|
|
9
|
+
this.baseUrl = baseUrl;
|
|
10
|
+
this.apiKey = apiKey;
|
|
11
|
+
this.buffer = [];
|
|
12
|
+
this.flushing = false;
|
|
13
|
+
}
|
|
14
|
+
async send(path, payload) {
|
|
15
|
+
try {
|
|
16
|
+
await this.doFetch(path, payload);
|
|
17
|
+
await this.flushBuffer();
|
|
18
|
+
} catch {
|
|
19
|
+
if (this.buffer.length >= MAX_BUFFER) this.buffer.shift();
|
|
20
|
+
this.buffer.push({ path, payload });
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
async doFetch(path, payload) {
|
|
24
|
+
const url = `${this.baseUrl}${path}`;
|
|
25
|
+
const controller = new AbortController();
|
|
26
|
+
const timeoutId = setTimeout(() => controller.abort(), REQUEST_TIMEOUT);
|
|
27
|
+
try {
|
|
28
|
+
const res = await fetch(url, {
|
|
29
|
+
method: "POST",
|
|
30
|
+
headers: {
|
|
31
|
+
"Content-Type": "application/json",
|
|
32
|
+
"X-AllStak-Key": this.apiKey
|
|
33
|
+
},
|
|
34
|
+
body: JSON.stringify(payload),
|
|
35
|
+
signal: controller.signal
|
|
36
|
+
});
|
|
37
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
38
|
+
} finally {
|
|
39
|
+
clearTimeout(timeoutId);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
async flushBuffer() {
|
|
43
|
+
if (this.flushing || this.buffer.length === 0) return;
|
|
44
|
+
this.flushing = true;
|
|
45
|
+
try {
|
|
46
|
+
const items = this.buffer.splice(0, this.buffer.length);
|
|
47
|
+
for (const item of items) {
|
|
48
|
+
try {
|
|
49
|
+
await this.doFetch(item.path, item.payload);
|
|
50
|
+
} catch {
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
} finally {
|
|
54
|
+
this.flushing = false;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
getBufferSize() {
|
|
58
|
+
return this.buffer.length;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Wait for the in-flight retry-buffer to drain. Resolves `true` if the
|
|
62
|
+
* buffer empties within `timeoutMs` (default 2000ms), `false` otherwise.
|
|
63
|
+
* Useful at process exit / before navigation away.
|
|
64
|
+
*/
|
|
65
|
+
async flush(timeoutMs = 2e3) {
|
|
66
|
+
const deadline = Date.now() + timeoutMs;
|
|
67
|
+
await this.flushBuffer();
|
|
68
|
+
while (this.buffer.length > 0 || this.flushing) {
|
|
69
|
+
if (Date.now() >= deadline) return false;
|
|
70
|
+
await new Promise((r) => setTimeout(r, 25));
|
|
71
|
+
await this.flushBuffer();
|
|
72
|
+
}
|
|
73
|
+
return true;
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
// src/stack.ts
|
|
78
|
+
var V8_FRAME_RE = /^\s*at\s+(?:(.+?)\s+\()?((?:.+?):(\d+):(\d+))\)?\s*$/;
|
|
79
|
+
var GECKO_FRAME_RE = /^\s*(?:(.*?)@)?(.+?):(\d+):(\d+)\s*$/;
|
|
80
|
+
var NODE_INTERNAL_RE = /^(node:|internal\/|node_modules\/)/;
|
|
81
|
+
function parseStack(stack) {
|
|
82
|
+
if (!stack || typeof stack !== "string") return [];
|
|
83
|
+
const frames = [];
|
|
84
|
+
for (const raw of stack.split("\n")) {
|
|
85
|
+
const line = raw.trim();
|
|
86
|
+
if (!line) continue;
|
|
87
|
+
let m = V8_FRAME_RE.exec(line);
|
|
88
|
+
if (m) {
|
|
89
|
+
const filename = stripQueryHash(m[2].replace(/:\d+:\d+$/, ""));
|
|
90
|
+
frames.push({
|
|
91
|
+
filename,
|
|
92
|
+
absPath: filename,
|
|
93
|
+
function: m[1] ? m[1].trim() : void 0,
|
|
94
|
+
lineno: parseInt(m[3], 10),
|
|
95
|
+
colno: parseInt(m[4], 10),
|
|
96
|
+
inApp: isInApp(filename)
|
|
97
|
+
});
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
m = GECKO_FRAME_RE.exec(line);
|
|
101
|
+
if (m && m[2]) {
|
|
102
|
+
const filename = stripQueryHash(m[2]);
|
|
103
|
+
frames.push({
|
|
104
|
+
filename,
|
|
105
|
+
absPath: filename,
|
|
106
|
+
function: m[1] ? m[1].trim() : void 0,
|
|
107
|
+
lineno: parseInt(m[3], 10),
|
|
108
|
+
colno: parseInt(m[4], 10),
|
|
109
|
+
inApp: isInApp(filename)
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return frames;
|
|
114
|
+
}
|
|
115
|
+
function stripQueryHash(url) {
|
|
116
|
+
const q = url.indexOf("?");
|
|
117
|
+
const h = url.indexOf("#");
|
|
118
|
+
let cut = url.length;
|
|
119
|
+
if (q >= 0) cut = Math.min(cut, q);
|
|
120
|
+
if (h >= 0) cut = Math.min(cut, h);
|
|
121
|
+
return url.slice(0, cut);
|
|
122
|
+
}
|
|
123
|
+
function isInApp(filename) {
|
|
124
|
+
if (!filename) return true;
|
|
125
|
+
if (NODE_INTERNAL_RE.test(filename)) return false;
|
|
126
|
+
if (filename.includes("/node_modules/")) return false;
|
|
127
|
+
return true;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// src/auto-breadcrumbs.ts
|
|
131
|
+
var FETCH_FLAG = "__allstak_fetch_patched__";
|
|
132
|
+
var CONSOLE_FLAG = "__allstak_console_patched__";
|
|
133
|
+
function instrumentFetch(addBreadcrumb, ownBaseUrl) {
|
|
134
|
+
const g = globalThis;
|
|
135
|
+
if (typeof g.fetch !== "function") return;
|
|
136
|
+
if (g.fetch[FETCH_FLAG]) return;
|
|
137
|
+
const originalFetch = g.fetch;
|
|
138
|
+
const wrapped = async function(input, init) {
|
|
139
|
+
const method = (init?.method || input && typeof input === "object" && input.method || "GET").toUpperCase();
|
|
140
|
+
let url;
|
|
141
|
+
if (typeof input === "string") url = input;
|
|
142
|
+
else if (input && typeof input.href === "string") url = input.href;
|
|
143
|
+
else if (input && typeof input.url === "string") url = input.url;
|
|
144
|
+
else url = String(input);
|
|
145
|
+
const safePath = url.split("?")[0];
|
|
146
|
+
const isOwnIngest = !!(ownBaseUrl && url.startsWith(ownBaseUrl));
|
|
147
|
+
const start = Date.now();
|
|
148
|
+
try {
|
|
149
|
+
const response = await originalFetch.call(this, input, init);
|
|
150
|
+
const durationMs = Date.now() - start;
|
|
151
|
+
if (!isOwnIngest) {
|
|
152
|
+
addBreadcrumb(
|
|
153
|
+
"http",
|
|
154
|
+
`${method} ${safePath} -> ${response.status}`,
|
|
155
|
+
response.status >= 400 ? "error" : "info",
|
|
156
|
+
{ method, url: safePath, statusCode: response.status, durationMs }
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
return response;
|
|
160
|
+
} catch (err) {
|
|
161
|
+
const durationMs = Date.now() - start;
|
|
162
|
+
if (!isOwnIngest) {
|
|
163
|
+
addBreadcrumb("http", `${method} ${safePath} -> failed`, "error", {
|
|
164
|
+
method,
|
|
165
|
+
url: safePath,
|
|
166
|
+
error: String(err),
|
|
167
|
+
durationMs
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
throw err;
|
|
171
|
+
}
|
|
172
|
+
};
|
|
173
|
+
wrapped[FETCH_FLAG] = true;
|
|
174
|
+
g.fetch = wrapped;
|
|
175
|
+
}
|
|
176
|
+
function instrumentConsole(addBreadcrumb) {
|
|
177
|
+
if (typeof console === "undefined") return;
|
|
178
|
+
if (console[CONSOLE_FLAG]) return;
|
|
179
|
+
const origWarn = console.warn;
|
|
180
|
+
const origError = console.error;
|
|
181
|
+
console.warn = function(...args) {
|
|
182
|
+
try {
|
|
183
|
+
addBreadcrumb("log", args.map(safeString).join(" "), "warn");
|
|
184
|
+
} catch {
|
|
185
|
+
}
|
|
186
|
+
return origWarn.apply(console, args);
|
|
187
|
+
};
|
|
188
|
+
console.error = function(...args) {
|
|
189
|
+
try {
|
|
190
|
+
addBreadcrumb("log", args.map(safeString).join(" "), "error");
|
|
191
|
+
} catch {
|
|
192
|
+
}
|
|
193
|
+
return origError.apply(console, args);
|
|
194
|
+
};
|
|
195
|
+
console[CONSOLE_FLAG] = true;
|
|
196
|
+
}
|
|
197
|
+
function safeString(v) {
|
|
198
|
+
if (v == null) return String(v);
|
|
199
|
+
if (typeof v === "string") return v;
|
|
200
|
+
if (v instanceof Error) return `${v.name}: ${v.message}`;
|
|
201
|
+
try {
|
|
202
|
+
return typeof v === "object" ? JSON.stringify(v) : String(v);
|
|
203
|
+
} catch {
|
|
204
|
+
return Object.prototype.toString.call(v);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// src/navigation.ts
|
|
209
|
+
var FLAG = "__allstak_history_patched__";
|
|
210
|
+
function instrumentBrowserNavigation(addBreadcrumb) {
|
|
211
|
+
if (typeof window === "undefined" || typeof history === "undefined") return;
|
|
212
|
+
if (history[FLAG]) return;
|
|
213
|
+
const emit = (from, to) => {
|
|
214
|
+
if (from === to) return;
|
|
215
|
+
try {
|
|
216
|
+
addBreadcrumb("navigation", `${from} -> ${to}`, "info", { from, to });
|
|
217
|
+
} catch {
|
|
218
|
+
}
|
|
219
|
+
};
|
|
220
|
+
const origPush = history.pushState.bind(history);
|
|
221
|
+
const origReplace = history.replaceState.bind(history);
|
|
222
|
+
history.pushState = function(state, unused, url) {
|
|
223
|
+
const from = location.pathname + location.search;
|
|
224
|
+
const ret = origPush(state, unused, url ?? null);
|
|
225
|
+
emit(from, location.pathname + location.search);
|
|
226
|
+
return ret;
|
|
227
|
+
};
|
|
228
|
+
history.replaceState = function(state, unused, url) {
|
|
229
|
+
const from = location.pathname + location.search;
|
|
230
|
+
const ret = origReplace(state, unused, url ?? null);
|
|
231
|
+
emit(from, location.pathname + location.search);
|
|
232
|
+
return ret;
|
|
233
|
+
};
|
|
234
|
+
let last = location.pathname + location.search;
|
|
235
|
+
window.addEventListener("popstate", () => {
|
|
236
|
+
const next = location.pathname + location.search;
|
|
237
|
+
emit(last, next);
|
|
238
|
+
last = next;
|
|
239
|
+
});
|
|
240
|
+
history[FLAG] = true;
|
|
241
|
+
}
|
|
242
|
+
var lastReactRouterPath;
|
|
243
|
+
function instrumentReactRouter(location2, addBreadcrumb = (...args) => __defaultBreadcrumb(...args)) {
|
|
244
|
+
const next = `${location2.pathname}${location2.search ?? ""}`;
|
|
245
|
+
if (next === lastReactRouterPath) return;
|
|
246
|
+
const from = lastReactRouterPath ?? "<initial>";
|
|
247
|
+
lastReactRouterPath = next;
|
|
248
|
+
try {
|
|
249
|
+
addBreadcrumb("navigation", `${from} -> ${next}`, "info", { router: "react-router", from, to: next });
|
|
250
|
+
} catch {
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
var lastNextPath;
|
|
254
|
+
function instrumentNextRouter(url, addBreadcrumb = (...args) => __defaultBreadcrumb(...args)) {
|
|
255
|
+
if (url === lastNextPath) return;
|
|
256
|
+
const from = lastNextPath ?? "<initial>";
|
|
257
|
+
lastNextPath = url;
|
|
258
|
+
try {
|
|
259
|
+
addBreadcrumb("navigation", `${from} -> ${url}`, "info", { router: "next", from, to: url });
|
|
260
|
+
} catch {
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
var __defaultBreadcrumb = () => {
|
|
264
|
+
};
|
|
265
|
+
function __setDefaultBreadcrumbForwarder(fn) {
|
|
266
|
+
__defaultBreadcrumb = fn;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// src/scope.ts
|
|
270
|
+
var Scope = class {
|
|
271
|
+
constructor() {
|
|
272
|
+
this.tags = {};
|
|
273
|
+
this.extras = {};
|
|
274
|
+
this.contexts = {};
|
|
275
|
+
}
|
|
276
|
+
setUser(user) {
|
|
277
|
+
this.user = user;
|
|
278
|
+
return this;
|
|
279
|
+
}
|
|
280
|
+
setTag(key, value) {
|
|
281
|
+
this.tags[key] = value;
|
|
282
|
+
return this;
|
|
283
|
+
}
|
|
284
|
+
setTags(tags) {
|
|
285
|
+
Object.assign(this.tags, tags);
|
|
286
|
+
return this;
|
|
287
|
+
}
|
|
288
|
+
setExtra(key, value) {
|
|
289
|
+
this.extras[key] = value;
|
|
290
|
+
return this;
|
|
291
|
+
}
|
|
292
|
+
setExtras(extras) {
|
|
293
|
+
Object.assign(this.extras, extras);
|
|
294
|
+
return this;
|
|
295
|
+
}
|
|
296
|
+
setContext(name, ctx) {
|
|
297
|
+
if (ctx === null) delete this.contexts[name];
|
|
298
|
+
else this.contexts[name] = ctx;
|
|
299
|
+
return this;
|
|
300
|
+
}
|
|
301
|
+
setLevel(level) {
|
|
302
|
+
this.level = level;
|
|
303
|
+
return this;
|
|
304
|
+
}
|
|
305
|
+
setFingerprint(fingerprint) {
|
|
306
|
+
this.fingerprint = fingerprint && fingerprint.length > 0 ? fingerprint : void 0;
|
|
307
|
+
return this;
|
|
308
|
+
}
|
|
309
|
+
clear() {
|
|
310
|
+
this.user = void 0;
|
|
311
|
+
this.tags = {};
|
|
312
|
+
this.extras = {};
|
|
313
|
+
this.contexts = {};
|
|
314
|
+
this.fingerprint = void 0;
|
|
315
|
+
this.level = void 0;
|
|
316
|
+
return this;
|
|
317
|
+
}
|
|
318
|
+
};
|
|
319
|
+
function mergeScopes(base, stack) {
|
|
320
|
+
const out = { ...base };
|
|
321
|
+
out.tags = { ...base.tags ?? {} };
|
|
322
|
+
out.extras = { ...base.extras ?? {} };
|
|
323
|
+
out.contexts = { ...base.contexts ?? {} };
|
|
324
|
+
for (const scope of stack) {
|
|
325
|
+
if (scope.user) out.user = scope.user;
|
|
326
|
+
Object.assign(out.tags, scope.tags);
|
|
327
|
+
Object.assign(out.extras, scope.extras);
|
|
328
|
+
Object.assign(out.contexts, scope.contexts);
|
|
329
|
+
if (scope.fingerprint) out.fingerprint = scope.fingerprint;
|
|
330
|
+
if (scope.level) out.level = scope.level;
|
|
331
|
+
}
|
|
332
|
+
return out;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// src/tracing.ts
|
|
336
|
+
var SPAN_INGEST_PATH = "/ingest/v1/spans";
|
|
337
|
+
var FLUSH_INTERVAL_MS = 5e3;
|
|
338
|
+
var BATCH_SIZE_THRESHOLD = 20;
|
|
339
|
+
function id() {
|
|
340
|
+
const hex = (n) => Math.floor(Math.random() * n).toString(16).padStart(1, "0");
|
|
341
|
+
const seg = (len) => Array.from({ length: len }, () => hex(16)).join("");
|
|
342
|
+
return `${seg(8)}-${seg(4)}-4${seg(3)}-${(8 + Math.floor(Math.random() * 4)).toString(16)}${seg(3)}-${seg(12)}`;
|
|
343
|
+
}
|
|
344
|
+
var Span = class {
|
|
345
|
+
constructor(_traceId, _spanId, _parentSpanId, _operation, _description, _service, _environment, _tags, _onFinish) {
|
|
346
|
+
this._traceId = _traceId;
|
|
347
|
+
this._spanId = _spanId;
|
|
348
|
+
this._parentSpanId = _parentSpanId;
|
|
349
|
+
this._operation = _operation;
|
|
350
|
+
this._description = _description;
|
|
351
|
+
this._service = _service;
|
|
352
|
+
this._environment = _environment;
|
|
353
|
+
this._tags = _tags;
|
|
354
|
+
this._onFinish = _onFinish;
|
|
355
|
+
this._finished = false;
|
|
356
|
+
this._data = "";
|
|
357
|
+
this._startTimeMillis = Date.now();
|
|
358
|
+
}
|
|
359
|
+
setTag(key, value) {
|
|
360
|
+
this._tags[key] = value;
|
|
361
|
+
return this;
|
|
362
|
+
}
|
|
363
|
+
setData(data) {
|
|
364
|
+
this._data = data;
|
|
365
|
+
return this;
|
|
366
|
+
}
|
|
367
|
+
setDescription(description) {
|
|
368
|
+
this._description = description;
|
|
369
|
+
return this;
|
|
370
|
+
}
|
|
371
|
+
finish(status = "ok") {
|
|
372
|
+
if (this._finished) return;
|
|
373
|
+
this._finished = true;
|
|
374
|
+
const endTimeMillis = Date.now();
|
|
375
|
+
this._onFinish({
|
|
376
|
+
traceId: this._traceId,
|
|
377
|
+
spanId: this._spanId,
|
|
378
|
+
parentSpanId: this._parentSpanId,
|
|
379
|
+
operation: this._operation,
|
|
380
|
+
description: this._description,
|
|
381
|
+
status,
|
|
382
|
+
durationMs: endTimeMillis - this._startTimeMillis,
|
|
383
|
+
startTimeMillis: this._startTimeMillis,
|
|
384
|
+
endTimeMillis,
|
|
385
|
+
service: this._service,
|
|
386
|
+
environment: this._environment,
|
|
387
|
+
tags: this._tags,
|
|
388
|
+
data: this._data
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
get traceId() {
|
|
392
|
+
return this._traceId;
|
|
393
|
+
}
|
|
394
|
+
get spanId() {
|
|
395
|
+
return this._spanId;
|
|
396
|
+
}
|
|
397
|
+
get isFinished() {
|
|
398
|
+
return this._finished;
|
|
399
|
+
}
|
|
400
|
+
};
|
|
401
|
+
var NoopSpan = class extends Span {
|
|
402
|
+
constructor(traceId, spanId) {
|
|
403
|
+
super(traceId, spanId, "", "", "", "", "", {}, () => {
|
|
404
|
+
});
|
|
405
|
+
}
|
|
406
|
+
finish() {
|
|
407
|
+
}
|
|
408
|
+
};
|
|
409
|
+
var TracingModule = class {
|
|
410
|
+
constructor(transport, opts) {
|
|
411
|
+
this.transport = transport;
|
|
412
|
+
this.opts = opts;
|
|
413
|
+
this.spans = [];
|
|
414
|
+
this.flushTimer = null;
|
|
415
|
+
this.currentTraceId = null;
|
|
416
|
+
this.spanStack = [];
|
|
417
|
+
this.destroyed = false;
|
|
418
|
+
}
|
|
419
|
+
/** Get (and lazily create) the active trace ID. */
|
|
420
|
+
getTraceId() {
|
|
421
|
+
if (!this.currentTraceId) this.currentTraceId = id();
|
|
422
|
+
return this.currentTraceId;
|
|
423
|
+
}
|
|
424
|
+
/** Override the active trace ID, e.g. from an inbound request header. */
|
|
425
|
+
setTraceId(traceId) {
|
|
426
|
+
this.currentTraceId = traceId;
|
|
427
|
+
}
|
|
428
|
+
/** Get the active span's ID, or null if no span is active. */
|
|
429
|
+
getCurrentSpanId() {
|
|
430
|
+
return this.spanStack.length > 0 ? this.spanStack[this.spanStack.length - 1].spanId : null;
|
|
431
|
+
}
|
|
432
|
+
/** Reset both the trace ID and the in-flight span stack. */
|
|
433
|
+
resetTrace() {
|
|
434
|
+
this.currentTraceId = null;
|
|
435
|
+
this.spanStack = [];
|
|
436
|
+
}
|
|
437
|
+
/**
|
|
438
|
+
* Start a new span. The returned Span automatically inherits the active
|
|
439
|
+
* span as its parent. If `tracesSampleRate` drops this trace, returns a
|
|
440
|
+
* no-op Span so the call site doesn't have to null-check.
|
|
441
|
+
*/
|
|
442
|
+
startSpan(operation, options = {}) {
|
|
443
|
+
const traceId = this.getTraceId();
|
|
444
|
+
const spanId = id();
|
|
445
|
+
const parentSpanId = this.getCurrentSpanId() ?? "";
|
|
446
|
+
if (!this.passesSampleRate()) {
|
|
447
|
+
return new NoopSpan(traceId, spanId);
|
|
448
|
+
}
|
|
449
|
+
const span = new Span(
|
|
450
|
+
traceId,
|
|
451
|
+
spanId,
|
|
452
|
+
parentSpanId,
|
|
453
|
+
operation,
|
|
454
|
+
options.description ?? "",
|
|
455
|
+
this.opts.service ?? "",
|
|
456
|
+
this.opts.environment ?? "",
|
|
457
|
+
{ ...options.tags ?? {} },
|
|
458
|
+
(data) => this.enqueue(data, span)
|
|
459
|
+
);
|
|
460
|
+
this.spanStack.push(span);
|
|
461
|
+
return span;
|
|
462
|
+
}
|
|
463
|
+
passesSampleRate() {
|
|
464
|
+
const r = this.opts.tracesSampleRate;
|
|
465
|
+
if (typeof r !== "number" || r >= 1) return true;
|
|
466
|
+
if (r <= 0) return false;
|
|
467
|
+
return Math.random() < r;
|
|
468
|
+
}
|
|
469
|
+
enqueue(data, span) {
|
|
470
|
+
if (this.destroyed) return;
|
|
471
|
+
const idx = this.spanStack.lastIndexOf(span);
|
|
472
|
+
if (idx >= 0) this.spanStack.splice(idx, 1);
|
|
473
|
+
this.spans.push(data);
|
|
474
|
+
if (this.spans.length >= BATCH_SIZE_THRESHOLD) {
|
|
475
|
+
this.flush();
|
|
476
|
+
return;
|
|
477
|
+
}
|
|
478
|
+
if (!this.flushTimer) {
|
|
479
|
+
this.flushTimer = setInterval(() => this.flush(), FLUSH_INTERVAL_MS);
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
flush() {
|
|
483
|
+
if (this.spans.length === 0) return;
|
|
484
|
+
const batch = this.spans;
|
|
485
|
+
this.spans = [];
|
|
486
|
+
this.transport.send(SPAN_INGEST_PATH, { spans: batch });
|
|
487
|
+
}
|
|
488
|
+
destroy() {
|
|
489
|
+
this.destroyed = true;
|
|
490
|
+
if (this.flushTimer) {
|
|
491
|
+
clearInterval(this.flushTimer);
|
|
492
|
+
this.flushTimer = null;
|
|
493
|
+
}
|
|
494
|
+
this.flush();
|
|
495
|
+
this.currentTraceId = null;
|
|
496
|
+
this.spanStack = [];
|
|
497
|
+
}
|
|
498
|
+
};
|
|
499
|
+
|
|
500
|
+
// src/replay.ts
|
|
501
|
+
var REPLAY_INGEST_PATH = "/ingest/v1/replay";
|
|
502
|
+
var FLUSH_INTERVAL_MS2 = 1e4;
|
|
503
|
+
var DEFAULT_MASK_ATTR = "data-allstak-mask";
|
|
504
|
+
var FLAG2 = "__allstak_replay_started__";
|
|
505
|
+
var ReplayRecorder = class {
|
|
506
|
+
constructor(transport, sessionId, addBreadcrumb, options = {}) {
|
|
507
|
+
this.transport = transport;
|
|
508
|
+
this.addBreadcrumb = addBreadcrumb;
|
|
509
|
+
this.buffer = [];
|
|
510
|
+
this.flushTimer = null;
|
|
511
|
+
this.observer = null;
|
|
512
|
+
this.inputListener = null;
|
|
513
|
+
this.destroyed = false;
|
|
514
|
+
this.sessionId = sessionId;
|
|
515
|
+
this.opts = {
|
|
516
|
+
enabled: options.enabled ?? true,
|
|
517
|
+
sampleRate: options.sampleRate ?? 0,
|
|
518
|
+
maskAllInputs: options.maskAllInputs ?? true,
|
|
519
|
+
maskAttribute: options.maskAttribute ?? DEFAULT_MASK_ATTR,
|
|
520
|
+
maxBufferedEvents: options.maxBufferedEvents ?? 200
|
|
521
|
+
};
|
|
522
|
+
}
|
|
523
|
+
start() {
|
|
524
|
+
if (typeof document === "undefined" || typeof window === "undefined") return;
|
|
525
|
+
if (!this.opts.enabled) return;
|
|
526
|
+
if (Math.random() >= this.opts.sampleRate) return;
|
|
527
|
+
if (document[FLAG2]) return;
|
|
528
|
+
document[FLAG2] = true;
|
|
529
|
+
this.push({
|
|
530
|
+
ts: Date.now(),
|
|
531
|
+
k: "snap",
|
|
532
|
+
data: { html: this.snapshotBody(), url: location.href }
|
|
533
|
+
});
|
|
534
|
+
this.observer = new MutationObserver((records) => {
|
|
535
|
+
for (const r of records) {
|
|
536
|
+
if (r.type === "childList") {
|
|
537
|
+
this.push({
|
|
538
|
+
ts: Date.now(),
|
|
539
|
+
k: "mut",
|
|
540
|
+
data: {
|
|
541
|
+
kind: "childList",
|
|
542
|
+
target: this.describePath(r.target),
|
|
543
|
+
added: Array.from(r.addedNodes).map((n) => this.describeNode(n)),
|
|
544
|
+
removed: r.removedNodes.length
|
|
545
|
+
}
|
|
546
|
+
});
|
|
547
|
+
} else if (r.type === "attributes") {
|
|
548
|
+
this.push({
|
|
549
|
+
ts: Date.now(),
|
|
550
|
+
k: "mut",
|
|
551
|
+
data: {
|
|
552
|
+
kind: "attr",
|
|
553
|
+
target: this.describePath(r.target),
|
|
554
|
+
name: r.attributeName,
|
|
555
|
+
value: this.safeAttribute(r.target, r.attributeName ?? "")
|
|
556
|
+
}
|
|
557
|
+
});
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
});
|
|
561
|
+
this.observer.observe(document.body ?? document.documentElement, {
|
|
562
|
+
childList: true,
|
|
563
|
+
attributes: true,
|
|
564
|
+
subtree: true
|
|
565
|
+
});
|
|
566
|
+
this.inputListener = (ev) => {
|
|
567
|
+
const target = ev.target;
|
|
568
|
+
if (!target || !("value" in target)) return;
|
|
569
|
+
const masked = this.maskInputValue(target);
|
|
570
|
+
this.push({
|
|
571
|
+
ts: Date.now(),
|
|
572
|
+
k: "input",
|
|
573
|
+
data: { target: this.describePath(target), value: masked, type: target.type }
|
|
574
|
+
});
|
|
575
|
+
};
|
|
576
|
+
document.addEventListener("input", this.inputListener, true);
|
|
577
|
+
this.flushTimer = setInterval(() => this.flush(), FLUSH_INTERVAL_MS2);
|
|
578
|
+
try {
|
|
579
|
+
this.addBreadcrumb("default", "Replay recording started", "info", { sessionId: this.sessionId });
|
|
580
|
+
} catch {
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
destroy() {
|
|
584
|
+
this.destroyed = true;
|
|
585
|
+
if (this.observer) {
|
|
586
|
+
this.observer.disconnect();
|
|
587
|
+
this.observer = null;
|
|
588
|
+
}
|
|
589
|
+
if (this.inputListener) {
|
|
590
|
+
document.removeEventListener("input", this.inputListener, true);
|
|
591
|
+
this.inputListener = null;
|
|
592
|
+
}
|
|
593
|
+
if (this.flushTimer) {
|
|
594
|
+
clearInterval(this.flushTimer);
|
|
595
|
+
this.flushTimer = null;
|
|
596
|
+
}
|
|
597
|
+
this.flush();
|
|
598
|
+
if (typeof document !== "undefined") document[FLAG2] = false;
|
|
599
|
+
}
|
|
600
|
+
/** @internal — exposed for tests. */
|
|
601
|
+
getBuffer() {
|
|
602
|
+
return this.buffer;
|
|
603
|
+
}
|
|
604
|
+
push(ev) {
|
|
605
|
+
if (this.destroyed) return;
|
|
606
|
+
this.buffer.push(ev);
|
|
607
|
+
if (this.buffer.length >= this.opts.maxBufferedEvents) this.flush();
|
|
608
|
+
}
|
|
609
|
+
flush() {
|
|
610
|
+
if (this.buffer.length === 0) return;
|
|
611
|
+
const events = this.buffer;
|
|
612
|
+
this.buffer = [];
|
|
613
|
+
this.transport.send(REPLAY_INGEST_PATH, {
|
|
614
|
+
sessionId: this.sessionId,
|
|
615
|
+
events
|
|
616
|
+
});
|
|
617
|
+
}
|
|
618
|
+
// ── Sanitization helpers ──────────────────────────────────────────
|
|
619
|
+
snapshotBody() {
|
|
620
|
+
if (!document.body) return "";
|
|
621
|
+
const clone = document.body.cloneNode(true);
|
|
622
|
+
this.maskTree(clone);
|
|
623
|
+
return clone.outerHTML;
|
|
624
|
+
}
|
|
625
|
+
maskTree(root) {
|
|
626
|
+
if (this.opts.maskAttribute && root.hasAttribute?.(this.opts.maskAttribute)) {
|
|
627
|
+
root.textContent = "***";
|
|
628
|
+
return;
|
|
629
|
+
}
|
|
630
|
+
const tag = root.tagName?.toLowerCase();
|
|
631
|
+
if (tag === "input" || tag === "textarea" || tag === "select") {
|
|
632
|
+
root.value = this.maskInputValue(root);
|
|
633
|
+
root.setAttribute("value", root.value);
|
|
634
|
+
}
|
|
635
|
+
for (const child of Array.from(root.children)) this.maskTree(child);
|
|
636
|
+
}
|
|
637
|
+
maskInputValue(el) {
|
|
638
|
+
if (el.type === "password") return "***";
|
|
639
|
+
if (this.opts.maskAllInputs) return "***";
|
|
640
|
+
return String(el.value ?? "");
|
|
641
|
+
}
|
|
642
|
+
describeNode(node) {
|
|
643
|
+
if (node.nodeType === 1) {
|
|
644
|
+
return { type: "element", tag: node.tagName?.toLowerCase() };
|
|
645
|
+
}
|
|
646
|
+
if (node.nodeType === 3) {
|
|
647
|
+
return { type: "text", length: (node.nodeValue ?? "").length };
|
|
648
|
+
}
|
|
649
|
+
return { type: "other" };
|
|
650
|
+
}
|
|
651
|
+
describePath(el) {
|
|
652
|
+
if (!el || el.nodeType !== 1) return "";
|
|
653
|
+
const parts = [];
|
|
654
|
+
let cur = el;
|
|
655
|
+
let depth = 0;
|
|
656
|
+
while (cur && depth < 8) {
|
|
657
|
+
let p = cur.tagName.toLowerCase();
|
|
658
|
+
if (cur.id) {
|
|
659
|
+
p += `#${cur.id}`;
|
|
660
|
+
parts.unshift(p);
|
|
661
|
+
break;
|
|
662
|
+
}
|
|
663
|
+
if (cur.classList?.length) p += "." + Array.from(cur.classList).slice(0, 2).join(".");
|
|
664
|
+
parts.unshift(p);
|
|
665
|
+
cur = cur.parentElement;
|
|
666
|
+
depth += 1;
|
|
667
|
+
}
|
|
668
|
+
return parts.join(">");
|
|
669
|
+
}
|
|
670
|
+
safeAttribute(el, name) {
|
|
671
|
+
if (name === "value" || name === "defaultValue") return "***";
|
|
672
|
+
return el.getAttribute(name) ?? "";
|
|
673
|
+
}
|
|
674
|
+
};
|
|
675
|
+
|
|
676
|
+
// src/debug-id.ts
|
|
677
|
+
function getRegistry() {
|
|
678
|
+
return globalThis._allstakDebugIds ?? {};
|
|
679
|
+
}
|
|
680
|
+
var cache = /* @__PURE__ */ new Map();
|
|
681
|
+
function resolveDebugId(filename) {
|
|
682
|
+
if (!filename) return void 0;
|
|
683
|
+
if (cache.has(filename)) return cache.get(filename);
|
|
684
|
+
const registry = getRegistry();
|
|
685
|
+
let id2 = registry[filename];
|
|
686
|
+
if (!id2) {
|
|
687
|
+
for (const key of Object.keys(registry)) {
|
|
688
|
+
if (filename.endsWith(key)) {
|
|
689
|
+
id2 = registry[key];
|
|
690
|
+
break;
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
cache.set(filename, id2);
|
|
695
|
+
return id2;
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
// src/client.ts
|
|
699
|
+
var INGEST_HOST = "https://api.allstak.sa";
|
|
700
|
+
var SDK_NAME = "allstak-react";
|
|
701
|
+
var SDK_VERSION = "0.2.1";
|
|
702
|
+
var ERRORS_PATH = "/ingest/v1/errors";
|
|
703
|
+
var LOGS_PATH = "/ingest/v1/logs";
|
|
704
|
+
var VALID_BREADCRUMB_TYPES = /* @__PURE__ */ new Set(["http", "log", "ui", "navigation", "query", "default"]);
|
|
705
|
+
var VALID_BREADCRUMB_LEVELS = /* @__PURE__ */ new Set(["info", "warn", "error", "debug"]);
|
|
706
|
+
var DEFAULT_MAX_BREADCRUMBS = 50;
|
|
707
|
+
function frameToString(f) {
|
|
708
|
+
const fn = f.function && f.function.length > 0 ? f.function : "<anonymous>";
|
|
709
|
+
const file = f.filename || f.absPath || "<anonymous>";
|
|
710
|
+
return ` at ${fn} (${file}:${f.lineno ?? 0}:${f.colno ?? 0})`;
|
|
711
|
+
}
|
|
712
|
+
function generateId() {
|
|
713
|
+
const hex = (n) => Math.floor(Math.random() * n).toString(16).padStart(1, "0");
|
|
714
|
+
const seg = (len) => Array.from({ length: len }, () => hex(16)).join("");
|
|
715
|
+
return `${seg(8)}-${seg(4)}-4${seg(3)}-${(8 + Math.floor(Math.random() * 4)).toString(16)}${seg(3)}-${seg(12)}`;
|
|
716
|
+
}
|
|
717
|
+
function browserRequestContext() {
|
|
718
|
+
if (typeof window === "undefined" || typeof location === "undefined") return void 0;
|
|
719
|
+
return {
|
|
720
|
+
method: "GET",
|
|
721
|
+
path: location.pathname || "/",
|
|
722
|
+
host: location.host || "",
|
|
723
|
+
userAgent: typeof navigator !== "undefined" ? navigator.userAgent : void 0
|
|
724
|
+
};
|
|
725
|
+
}
|
|
726
|
+
var AllStakClient = class {
|
|
727
|
+
constructor(config) {
|
|
728
|
+
this.breadcrumbs = [];
|
|
729
|
+
this.scopeStack = [];
|
|
730
|
+
this.replay = null;
|
|
731
|
+
this.onErrorHandler = null;
|
|
732
|
+
this.onRejectionHandler = null;
|
|
733
|
+
if (!config.apiKey) throw new Error("AllStak: config.apiKey is required");
|
|
734
|
+
this.config = { ...config };
|
|
735
|
+
if (!this.config.environment) this.config.environment = "production";
|
|
736
|
+
if (!this.config.sdkName) this.config.sdkName = SDK_NAME;
|
|
737
|
+
if (!this.config.sdkVersion) this.config.sdkVersion = SDK_VERSION;
|
|
738
|
+
if (!this.config.platform) this.config.platform = "browser";
|
|
739
|
+
this.sessionId = generateId();
|
|
740
|
+
this.maxBreadcrumbs = config.maxBreadcrumbs ?? DEFAULT_MAX_BREADCRUMBS;
|
|
741
|
+
const baseUrl = (config.host ?? INGEST_HOST).replace(/\/$/, "");
|
|
742
|
+
this.transport = new HttpTransport(baseUrl, config.apiKey);
|
|
743
|
+
this.tracing = new TracingModule(this.transport, {
|
|
744
|
+
service: config.service ?? config.release ?? "",
|
|
745
|
+
environment: this.config.environment ?? "production",
|
|
746
|
+
tracesSampleRate: config.tracesSampleRate
|
|
747
|
+
});
|
|
748
|
+
if (config.autoCaptureBrowserErrors !== false && typeof window !== "undefined") {
|
|
749
|
+
this.installBrowserHandlers();
|
|
750
|
+
}
|
|
751
|
+
if (config.autoBreadcrumbsFetch !== false) {
|
|
752
|
+
try {
|
|
753
|
+
instrumentFetch(safeAddBreadcrumb, baseUrl);
|
|
754
|
+
} catch {
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
if (config.autoBreadcrumbsConsole !== false) {
|
|
758
|
+
try {
|
|
759
|
+
instrumentConsole(safeAddBreadcrumb);
|
|
760
|
+
} catch {
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
if (config.autoBreadcrumbsNavigation !== false) {
|
|
764
|
+
try {
|
|
765
|
+
instrumentBrowserNavigation(safeAddBreadcrumb);
|
|
766
|
+
} catch {
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
if (config.replay && (config.replay.enabled ?? true)) {
|
|
770
|
+
try {
|
|
771
|
+
this.replay = new ReplayRecorder(this.transport, this.sessionId, safeAddBreadcrumb, config.replay);
|
|
772
|
+
this.replay.start();
|
|
773
|
+
} catch {
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
captureException(error, context) {
|
|
778
|
+
if (!this.passesSampleRate()) return;
|
|
779
|
+
const frames = parseStack(error.stack).map((f) => ({
|
|
780
|
+
...f,
|
|
781
|
+
platform: this.config.platform,
|
|
782
|
+
debugId: resolveDebugId(f.filename)
|
|
783
|
+
}));
|
|
784
|
+
const stackTrace = frames.length > 0 ? frames.map(frameToString) : void 0;
|
|
785
|
+
const currentBreadcrumbs = this.breadcrumbs.length > 0 ? [...this.breadcrumbs] : void 0;
|
|
786
|
+
this.breadcrumbs = [];
|
|
787
|
+
const exceptionClass = (error.name && error.name !== "Error" ? error.name : void 0) || error.constructor?.name || "Error";
|
|
788
|
+
const eff = this.effective();
|
|
789
|
+
const traceContext = {};
|
|
790
|
+
const traceId = this.tracing.getTraceId();
|
|
791
|
+
if (traceId) traceContext.traceId = traceId;
|
|
792
|
+
const spanId = this.tracing.getCurrentSpanId();
|
|
793
|
+
if (spanId) traceContext.spanId = spanId;
|
|
794
|
+
const payload = {
|
|
795
|
+
exceptionClass,
|
|
796
|
+
message: error.message,
|
|
797
|
+
stackTrace,
|
|
798
|
+
frames: frames.length > 0 ? frames : void 0,
|
|
799
|
+
platform: this.config.platform,
|
|
800
|
+
sdkName: this.config.sdkName,
|
|
801
|
+
sdkVersion: this.config.sdkVersion,
|
|
802
|
+
dist: this.config.dist,
|
|
803
|
+
level: eff.level ?? "error",
|
|
804
|
+
environment: this.config.environment,
|
|
805
|
+
release: this.config.release,
|
|
806
|
+
sessionId: this.sessionId,
|
|
807
|
+
user: eff.user,
|
|
808
|
+
metadata: { ...this.buildMetadata(context), ...traceContext },
|
|
809
|
+
breadcrumbs: currentBreadcrumbs,
|
|
810
|
+
requestContext: browserRequestContext(),
|
|
811
|
+
fingerprint: eff.fingerprint
|
|
812
|
+
};
|
|
813
|
+
this.sendThroughBeforeSend(payload);
|
|
814
|
+
}
|
|
815
|
+
/** Start a new span — auto-parented to any currently-active span. */
|
|
816
|
+
startSpan(operation, options) {
|
|
817
|
+
return this.tracing.startSpan(operation, options);
|
|
818
|
+
}
|
|
819
|
+
/** Get (and lazily create) the active trace ID. */
|
|
820
|
+
getTraceId() {
|
|
821
|
+
return this.tracing.getTraceId();
|
|
822
|
+
}
|
|
823
|
+
/** Override the active trace ID, e.g. from an inbound request header. */
|
|
824
|
+
setTraceId(traceId) {
|
|
825
|
+
this.tracing.setTraceId(traceId);
|
|
826
|
+
}
|
|
827
|
+
/** ID of the currently-active span, or null. */
|
|
828
|
+
getCurrentSpanId() {
|
|
829
|
+
return this.tracing.getCurrentSpanId();
|
|
830
|
+
}
|
|
831
|
+
/** Reset the trace ID and the active span stack. */
|
|
832
|
+
resetTrace() {
|
|
833
|
+
this.tracing.resetTrace();
|
|
834
|
+
}
|
|
835
|
+
captureMessage(message, level = "info", options = {}) {
|
|
836
|
+
const as = options.as ?? (level === "fatal" || level === "error" ? "both" : "log");
|
|
837
|
+
if (as === "log" || as === "both") {
|
|
838
|
+
this.sendLog(level === "warning" ? "warn" : level, message);
|
|
839
|
+
}
|
|
840
|
+
if (as === "error" || as === "both") {
|
|
841
|
+
if (!this.passesSampleRate()) return;
|
|
842
|
+
const eff = this.effective();
|
|
843
|
+
const payload = {
|
|
844
|
+
exceptionClass: "Message",
|
|
845
|
+
message,
|
|
846
|
+
platform: this.config.platform,
|
|
847
|
+
sdkName: this.config.sdkName,
|
|
848
|
+
sdkVersion: this.config.sdkVersion,
|
|
849
|
+
dist: this.config.dist,
|
|
850
|
+
level,
|
|
851
|
+
environment: this.config.environment,
|
|
852
|
+
release: this.config.release,
|
|
853
|
+
sessionId: this.sessionId,
|
|
854
|
+
user: eff.user,
|
|
855
|
+
metadata: this.buildMetadata(),
|
|
856
|
+
requestContext: browserRequestContext(),
|
|
857
|
+
fingerprint: eff.fingerprint
|
|
858
|
+
};
|
|
859
|
+
this.sendThroughBeforeSend(payload);
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
addBreadcrumb(type, message, level, data) {
|
|
863
|
+
const crumb = {
|
|
864
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
865
|
+
type: VALID_BREADCRUMB_TYPES.has(type) ? type : "default",
|
|
866
|
+
message,
|
|
867
|
+
level: level && VALID_BREADCRUMB_LEVELS.has(level) ? level : "info",
|
|
868
|
+
...data ? { data } : {}
|
|
869
|
+
};
|
|
870
|
+
if (this.breadcrumbs.length >= this.maxBreadcrumbs) this.breadcrumbs.shift();
|
|
871
|
+
this.breadcrumbs.push(crumb);
|
|
872
|
+
}
|
|
873
|
+
clearBreadcrumbs() {
|
|
874
|
+
this.breadcrumbs = [];
|
|
875
|
+
}
|
|
876
|
+
setUser(user) {
|
|
877
|
+
this.config.user = user;
|
|
878
|
+
}
|
|
879
|
+
setTag(key, value) {
|
|
880
|
+
if (!this.config.tags) this.config.tags = {};
|
|
881
|
+
this.config.tags[key] = value;
|
|
882
|
+
}
|
|
883
|
+
/** Bulk-set tags. Merges with existing tags. */
|
|
884
|
+
setTags(tags) {
|
|
885
|
+
if (!this.config.tags) this.config.tags = {};
|
|
886
|
+
Object.assign(this.config.tags, tags);
|
|
887
|
+
}
|
|
888
|
+
/** Set a single extra value. */
|
|
889
|
+
setExtra(key, value) {
|
|
890
|
+
if (!this.config.extras) this.config.extras = {};
|
|
891
|
+
this.config.extras[key] = value;
|
|
892
|
+
}
|
|
893
|
+
/** Bulk-set extras. Merges with existing extras. */
|
|
894
|
+
setExtras(extras) {
|
|
895
|
+
if (!this.config.extras) this.config.extras = {};
|
|
896
|
+
Object.assign(this.config.extras, extras);
|
|
897
|
+
}
|
|
898
|
+
/**
|
|
899
|
+
* Attach a named context bag (e.g. `app`, `device`, `runtime`) — appears
|
|
900
|
+
* under `metadata['context.<name>']` on every subsequent event. Pass
|
|
901
|
+
* `null` to remove a previously-set context.
|
|
902
|
+
*/
|
|
903
|
+
setContext(name, ctx) {
|
|
904
|
+
if (!this.config.contexts) this.config.contexts = {};
|
|
905
|
+
if (ctx === null) delete this.config.contexts[name];
|
|
906
|
+
else this.config.contexts[name] = ctx;
|
|
907
|
+
}
|
|
908
|
+
/**
|
|
909
|
+
* Wait for the in-flight retry-buffer to drain. Resolves `true` if the
|
|
910
|
+
* buffer empties within `timeoutMs` (default 2000ms), `false` otherwise.
|
|
911
|
+
*/
|
|
912
|
+
flush(timeoutMs) {
|
|
913
|
+
return this.transport.flush(timeoutMs);
|
|
914
|
+
}
|
|
915
|
+
/** Set the default severity level applied to subsequent captures. */
|
|
916
|
+
setLevel(level) {
|
|
917
|
+
this.config.level = level;
|
|
918
|
+
}
|
|
919
|
+
/**
|
|
920
|
+
* Set a custom grouping fingerprint applied to subsequent events.
|
|
921
|
+
* Pass `null` or an empty array to clear and revert to default grouping.
|
|
922
|
+
*/
|
|
923
|
+
setFingerprint(fingerprint) {
|
|
924
|
+
this.config.fingerprint = fingerprint && fingerprint.length > 0 ? fingerprint : void 0;
|
|
925
|
+
}
|
|
926
|
+
setIdentity(identity) {
|
|
927
|
+
if (identity.sdkName) this.config.sdkName = identity.sdkName;
|
|
928
|
+
if (identity.sdkVersion) this.config.sdkVersion = identity.sdkVersion;
|
|
929
|
+
if (identity.platform) this.config.platform = identity.platform;
|
|
930
|
+
if (identity.dist) this.config.dist = identity.dist;
|
|
931
|
+
}
|
|
932
|
+
getSessionId() {
|
|
933
|
+
return this.sessionId;
|
|
934
|
+
}
|
|
935
|
+
getConfig() {
|
|
936
|
+
return this.config;
|
|
937
|
+
}
|
|
938
|
+
destroy() {
|
|
939
|
+
if (typeof window !== "undefined") {
|
|
940
|
+
if (this.onErrorHandler) window.removeEventListener("error", this.onErrorHandler);
|
|
941
|
+
if (this.onRejectionHandler) window.removeEventListener("unhandledrejection", this.onRejectionHandler);
|
|
942
|
+
}
|
|
943
|
+
this.onErrorHandler = null;
|
|
944
|
+
this.onRejectionHandler = null;
|
|
945
|
+
this.tracing.destroy();
|
|
946
|
+
if (this.replay) {
|
|
947
|
+
this.replay.destroy();
|
|
948
|
+
this.replay = null;
|
|
949
|
+
}
|
|
950
|
+
this.breadcrumbs = [];
|
|
951
|
+
}
|
|
952
|
+
installBrowserHandlers() {
|
|
953
|
+
this.onErrorHandler = (ev) => {
|
|
954
|
+
const err = ev.error instanceof Error ? ev.error : new Error(ev.message || "Unknown error");
|
|
955
|
+
this.captureException(err, { source: "window.onerror" });
|
|
956
|
+
};
|
|
957
|
+
this.onRejectionHandler = (ev) => {
|
|
958
|
+
const err = ev.reason instanceof Error ? ev.reason : new Error(String(ev.reason));
|
|
959
|
+
this.captureException(err, { source: "window.unhandledrejection" });
|
|
960
|
+
};
|
|
961
|
+
window.addEventListener("error", this.onErrorHandler);
|
|
962
|
+
window.addEventListener("unhandledrejection", this.onRejectionHandler);
|
|
963
|
+
}
|
|
964
|
+
sendLog(level, message) {
|
|
965
|
+
this.transport.send(LOGS_PATH, {
|
|
966
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
967
|
+
level,
|
|
968
|
+
message,
|
|
969
|
+
sessionId: this.sessionId,
|
|
970
|
+
environment: this.config.environment,
|
|
971
|
+
release: this.config.release,
|
|
972
|
+
platform: this.config.platform,
|
|
973
|
+
sdkName: this.config.sdkName,
|
|
974
|
+
sdkVersion: this.config.sdkVersion,
|
|
975
|
+
metadata: { ...this.releaseTags(), ...this.config.tags }
|
|
976
|
+
});
|
|
977
|
+
}
|
|
978
|
+
passesSampleRate() {
|
|
979
|
+
const r = this.config.sampleRate;
|
|
980
|
+
if (typeof r !== "number" || r >= 1) return true;
|
|
981
|
+
if (r <= 0) return false;
|
|
982
|
+
return Math.random() < r;
|
|
983
|
+
}
|
|
984
|
+
/**
|
|
985
|
+
* Returns the effective config layer = base config + every active scope.
|
|
986
|
+
* Scope-only overrides (set inside `withScope`) flow into the wire
|
|
987
|
+
* payload without leaking out of the callback.
|
|
988
|
+
*/
|
|
989
|
+
effective() {
|
|
990
|
+
return mergeScopes(this.config, this.scopeStack);
|
|
991
|
+
}
|
|
992
|
+
buildMetadata(perCallContext) {
|
|
993
|
+
const eff = this.effective();
|
|
994
|
+
const out = {
|
|
995
|
+
...this.releaseTags(),
|
|
996
|
+
...eff.tags,
|
|
997
|
+
...eff.extras ?? {},
|
|
998
|
+
...perCallContext ?? {}
|
|
999
|
+
};
|
|
1000
|
+
if (eff.contexts) {
|
|
1001
|
+
for (const [name, ctx] of Object.entries(eff.contexts)) {
|
|
1002
|
+
out[`context.${name}`] = ctx;
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
return out;
|
|
1006
|
+
}
|
|
1007
|
+
/**
|
|
1008
|
+
* Run `callback` with a fresh, temporary {@link Scope} that isolates
|
|
1009
|
+
* any user/tag/extra/context/fingerprint/level it sets. The scope is
|
|
1010
|
+
* popped automatically when the callback returns or throws — including
|
|
1011
|
+
* for `Promise`-returning callbacks (the pop runs in `.finally`).
|
|
1012
|
+
*
|
|
1013
|
+
* Use this on the server (SSR / RSC / API route handlers) to attach
|
|
1014
|
+
* per-request user/tags without leaking that data into another request
|
|
1015
|
+
* being processed concurrently.
|
|
1016
|
+
*/
|
|
1017
|
+
withScope(callback) {
|
|
1018
|
+
const scope = new Scope();
|
|
1019
|
+
this.scopeStack.push(scope);
|
|
1020
|
+
let popped = false;
|
|
1021
|
+
const pop = () => {
|
|
1022
|
+
if (!popped) {
|
|
1023
|
+
popped = true;
|
|
1024
|
+
this.scopeStack.pop();
|
|
1025
|
+
}
|
|
1026
|
+
};
|
|
1027
|
+
try {
|
|
1028
|
+
const result = callback(scope);
|
|
1029
|
+
if (result && typeof result.then === "function") {
|
|
1030
|
+
return result.then(
|
|
1031
|
+
(v) => {
|
|
1032
|
+
pop();
|
|
1033
|
+
return v;
|
|
1034
|
+
},
|
|
1035
|
+
(e) => {
|
|
1036
|
+
pop();
|
|
1037
|
+
throw e;
|
|
1038
|
+
}
|
|
1039
|
+
);
|
|
1040
|
+
}
|
|
1041
|
+
pop();
|
|
1042
|
+
return result;
|
|
1043
|
+
} catch (err) {
|
|
1044
|
+
pop();
|
|
1045
|
+
throw err;
|
|
1046
|
+
}
|
|
1047
|
+
}
|
|
1048
|
+
/** Direct access to the topmost active scope, or null. @internal */
|
|
1049
|
+
getCurrentScope() {
|
|
1050
|
+
return this.scopeStack[this.scopeStack.length - 1] ?? null;
|
|
1051
|
+
}
|
|
1052
|
+
async sendThroughBeforeSend(payload) {
|
|
1053
|
+
let final = payload;
|
|
1054
|
+
if (this.config.beforeSend) {
|
|
1055
|
+
try {
|
|
1056
|
+
final = await this.config.beforeSend(payload);
|
|
1057
|
+
} catch {
|
|
1058
|
+
final = payload;
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
if (!final) return;
|
|
1062
|
+
this.transport.send(ERRORS_PATH, final);
|
|
1063
|
+
}
|
|
1064
|
+
releaseTags() {
|
|
1065
|
+
const out = {};
|
|
1066
|
+
if (this.config.sdkName) out["sdk.name"] = this.config.sdkName;
|
|
1067
|
+
if (this.config.sdkVersion) out["sdk.version"] = this.config.sdkVersion;
|
|
1068
|
+
if (this.config.platform) out["platform"] = this.config.platform;
|
|
1069
|
+
if (this.config.dist) out["dist"] = this.config.dist;
|
|
1070
|
+
if (this.config.commitSha) out["commit.sha"] = this.config.commitSha;
|
|
1071
|
+
if (this.config.branch) out["commit.branch"] = this.config.branch;
|
|
1072
|
+
return out;
|
|
1073
|
+
}
|
|
1074
|
+
};
|
|
1075
|
+
var instance = null;
|
|
1076
|
+
function ensureInit() {
|
|
1077
|
+
if (!instance) throw new Error("AllStak.init() must be called before using the SDK");
|
|
1078
|
+
return instance;
|
|
1079
|
+
}
|
|
1080
|
+
function safeAddBreadcrumb(type, message, level, data) {
|
|
1081
|
+
try {
|
|
1082
|
+
instance?.addBreadcrumb(type, message, level, data);
|
|
1083
|
+
} catch {
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
1086
|
+
__setDefaultBreadcrumbForwarder(safeAddBreadcrumb);
|
|
1087
|
+
var AllStak = {
|
|
1088
|
+
init(config) {
|
|
1089
|
+
if (instance) instance.destroy();
|
|
1090
|
+
instance = new AllStakClient(config);
|
|
1091
|
+
return instance;
|
|
1092
|
+
},
|
|
1093
|
+
captureException(error, context) {
|
|
1094
|
+
ensureInit().captureException(error, context);
|
|
1095
|
+
},
|
|
1096
|
+
captureMessage(message, level = "info", options) {
|
|
1097
|
+
ensureInit().captureMessage(message, level, options);
|
|
1098
|
+
},
|
|
1099
|
+
addBreadcrumb(type, message, level, data) {
|
|
1100
|
+
ensureInit().addBreadcrumb(type, message, level, data);
|
|
1101
|
+
},
|
|
1102
|
+
clearBreadcrumbs() {
|
|
1103
|
+
ensureInit().clearBreadcrumbs();
|
|
1104
|
+
},
|
|
1105
|
+
setUser(user) {
|
|
1106
|
+
ensureInit().setUser(user);
|
|
1107
|
+
},
|
|
1108
|
+
setTag(key, value) {
|
|
1109
|
+
ensureInit().setTag(key, value);
|
|
1110
|
+
},
|
|
1111
|
+
setTags(tags) {
|
|
1112
|
+
ensureInit().setTags(tags);
|
|
1113
|
+
},
|
|
1114
|
+
setExtra(key, value) {
|
|
1115
|
+
ensureInit().setExtra(key, value);
|
|
1116
|
+
},
|
|
1117
|
+
setExtras(extras) {
|
|
1118
|
+
ensureInit().setExtras(extras);
|
|
1119
|
+
},
|
|
1120
|
+
setContext(name, ctx) {
|
|
1121
|
+
ensureInit().setContext(name, ctx);
|
|
1122
|
+
},
|
|
1123
|
+
setLevel(level) {
|
|
1124
|
+
ensureInit().setLevel(level);
|
|
1125
|
+
},
|
|
1126
|
+
setFingerprint(fingerprint) {
|
|
1127
|
+
ensureInit().setFingerprint(fingerprint);
|
|
1128
|
+
},
|
|
1129
|
+
flush(timeoutMs) {
|
|
1130
|
+
return ensureInit().flush(timeoutMs);
|
|
1131
|
+
},
|
|
1132
|
+
setIdentity(identity) {
|
|
1133
|
+
ensureInit().setIdentity(identity);
|
|
1134
|
+
},
|
|
1135
|
+
/**
|
|
1136
|
+
* Run `callback` with a fresh, temporary {@link Scope}. Any user/tag/
|
|
1137
|
+
* extra/context/fingerprint/level set on the scope is visible only inside
|
|
1138
|
+
* the callback. Pop is automatic (sync, async, throwing).
|
|
1139
|
+
*/
|
|
1140
|
+
withScope(callback) {
|
|
1141
|
+
return ensureInit().withScope(callback);
|
|
1142
|
+
},
|
|
1143
|
+
startSpan(operation, options) {
|
|
1144
|
+
return ensureInit().startSpan(operation, options);
|
|
1145
|
+
},
|
|
1146
|
+
getTraceId() {
|
|
1147
|
+
return ensureInit().getTraceId();
|
|
1148
|
+
},
|
|
1149
|
+
setTraceId(traceId) {
|
|
1150
|
+
ensureInit().setTraceId(traceId);
|
|
1151
|
+
},
|
|
1152
|
+
getCurrentSpanId() {
|
|
1153
|
+
return ensureInit().getCurrentSpanId();
|
|
1154
|
+
},
|
|
1155
|
+
resetTrace() {
|
|
1156
|
+
ensureInit().resetTrace();
|
|
1157
|
+
},
|
|
1158
|
+
getSessionId() {
|
|
1159
|
+
return ensureInit().getSessionId();
|
|
1160
|
+
},
|
|
1161
|
+
getConfig() {
|
|
1162
|
+
return instance?.getConfig() ?? null;
|
|
1163
|
+
},
|
|
1164
|
+
destroy() {
|
|
1165
|
+
instance?.destroy();
|
|
1166
|
+
instance = null;
|
|
1167
|
+
},
|
|
1168
|
+
/** @internal */
|
|
1169
|
+
_getInstance() {
|
|
1170
|
+
return instance;
|
|
1171
|
+
}
|
|
1172
|
+
};
|
|
1173
|
+
|
|
1174
|
+
// src/index.ts
|
|
1175
|
+
var AllStakErrorBoundary = class extends React.Component {
|
|
1176
|
+
constructor() {
|
|
1177
|
+
super(...arguments);
|
|
1178
|
+
this.state = { error: null };
|
|
1179
|
+
this.reset = () => this.setState({ error: null });
|
|
1180
|
+
}
|
|
1181
|
+
static getDerivedStateFromError(error) {
|
|
1182
|
+
return { error };
|
|
1183
|
+
}
|
|
1184
|
+
componentDidCatch(error, info) {
|
|
1185
|
+
try {
|
|
1186
|
+
AllStak.addBreadcrumb("ui", "React error boundary caught error", "error", {
|
|
1187
|
+
componentStack: info.componentStack ?? ""
|
|
1188
|
+
});
|
|
1189
|
+
const context = {
|
|
1190
|
+
componentStack: info.componentStack ?? "",
|
|
1191
|
+
source: "react-error-boundary"
|
|
1192
|
+
};
|
|
1193
|
+
if (this.props.tags) {
|
|
1194
|
+
for (const [k, v] of Object.entries(this.props.tags)) {
|
|
1195
|
+
context[`tag.${k}`] = v;
|
|
1196
|
+
}
|
|
1197
|
+
}
|
|
1198
|
+
AllStak.captureException(error, context);
|
|
1199
|
+
} catch {
|
|
1200
|
+
}
|
|
1201
|
+
try {
|
|
1202
|
+
this.props.onError?.(error, info);
|
|
1203
|
+
} catch {
|
|
1204
|
+
}
|
|
1205
|
+
}
|
|
1206
|
+
render() {
|
|
1207
|
+
if (this.state.error) {
|
|
1208
|
+
const { fallback } = this.props;
|
|
1209
|
+
if (typeof fallback === "function") {
|
|
1210
|
+
return fallback({ error: this.state.error, reset: this.reset });
|
|
1211
|
+
}
|
|
1212
|
+
if (fallback !== void 0) return fallback;
|
|
1213
|
+
return null;
|
|
1214
|
+
}
|
|
1215
|
+
return this.props.children;
|
|
1216
|
+
}
|
|
1217
|
+
};
|
|
1218
|
+
function useAllStak() {
|
|
1219
|
+
return React.useMemo(
|
|
1220
|
+
() => ({
|
|
1221
|
+
captureException: (error, ctx) => AllStak.captureException(error, ctx),
|
|
1222
|
+
captureMessage: (msg, level = "info") => AllStak.captureMessage(msg, level),
|
|
1223
|
+
setUser: (user) => AllStak.setUser(user),
|
|
1224
|
+
setTag: (key, value) => AllStak.setTag(key, value),
|
|
1225
|
+
addBreadcrumb: (type, message, level, data) => AllStak.addBreadcrumb(type, message, level, data)
|
|
1226
|
+
}),
|
|
1227
|
+
[]
|
|
1228
|
+
);
|
|
1229
|
+
}
|
|
1230
|
+
function withAllStakProfiler(Component2, name) {
|
|
1231
|
+
const displayName = name ?? Component2.displayName ?? Component2.name ?? "AnonymousComponent";
|
|
1232
|
+
const Wrapped = (props) => {
|
|
1233
|
+
React.useEffect(() => {
|
|
1234
|
+
AllStak.addBreadcrumb("navigation", `Mounted <${displayName}>`, "info");
|
|
1235
|
+
}, []);
|
|
1236
|
+
return React.createElement(Component2, props);
|
|
1237
|
+
};
|
|
1238
|
+
Wrapped.displayName = `withAllStakProfiler(${displayName})`;
|
|
1239
|
+
return Wrapped;
|
|
1240
|
+
}
|
|
8
1241
|
export {
|
|
9
1242
|
AllStak,
|
|
1243
|
+
AllStakClient,
|
|
10
1244
|
AllStakErrorBoundary,
|
|
1245
|
+
INGEST_HOST,
|
|
1246
|
+
ReplayRecorder,
|
|
1247
|
+
SDK_NAME,
|
|
1248
|
+
SDK_VERSION,
|
|
1249
|
+
Scope,
|
|
1250
|
+
instrumentBrowserNavigation,
|
|
1251
|
+
instrumentConsole,
|
|
1252
|
+
instrumentFetch,
|
|
1253
|
+
instrumentNextRouter,
|
|
1254
|
+
instrumentReactRouter,
|
|
11
1255
|
useAllStak,
|
|
12
1256
|
withAllStakProfiler
|
|
13
1257
|
};
|