@watchforge/browser 0.1.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/CONFIGURATION_GUIDE.md +755 -0
- package/LICENSE +201 -0
- package/README.md +142 -0
- package/package.json +58 -0
- package/src/client.js +628 -0
- package/src/express.js +77 -0
- package/src/index.js +12 -0
- package/src/react.js +46 -0
- package/src/tracing.js +253 -0
- package/src/transport.js +191 -0
package/src/client.js
ADDED
|
@@ -0,0 +1,628 @@
|
|
|
1
|
+
import { sendEvent, parseDsn } from "./transport.js";
|
|
2
|
+
|
|
3
|
+
let DSN = null;
|
|
4
|
+
let APP_ENV = "production";
|
|
5
|
+
let RELEASE = null;
|
|
6
|
+
let DEBUG = false;
|
|
7
|
+
|
|
8
|
+
// Detect environment
|
|
9
|
+
const isBrowser = typeof window !== "undefined";
|
|
10
|
+
const isNode =
|
|
11
|
+
typeof process !== "undefined" && process.versions && process.versions.node;
|
|
12
|
+
|
|
13
|
+
// In-memory breadcrumb buffer (shared for all events in this process / page)
|
|
14
|
+
const MAX_BREADCRUMBS = 100;
|
|
15
|
+
let breadcrumbs = [];
|
|
16
|
+
|
|
17
|
+
function addBreadcrumb(breadcrumb) {
|
|
18
|
+
const entry = {
|
|
19
|
+
level: "info",
|
|
20
|
+
category: "log",
|
|
21
|
+
data: {},
|
|
22
|
+
...breadcrumb,
|
|
23
|
+
timestamp: new Date().toISOString(),
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
breadcrumbs.push(entry);
|
|
27
|
+
if (breadcrumbs.length > MAX_BREADCRUMBS) {
|
|
28
|
+
breadcrumbs = breadcrumbs.slice(-MAX_BREADCRUMBS);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function getBreadcrumbsSnapshot() {
|
|
33
|
+
return breadcrumbs.slice();
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Simple browser environment detectors (best-effort, not 100% accurate)
|
|
37
|
+
function getBrowserContext() {
|
|
38
|
+
if (!isBrowser) return null;
|
|
39
|
+
|
|
40
|
+
const ua = navigator.userAgent || "";
|
|
41
|
+
let name = "Unknown";
|
|
42
|
+
if (/chrome|crios|crmo/i.test(ua) && !/edge|edg\//i.test(ua)) {
|
|
43
|
+
name = "Chrome";
|
|
44
|
+
} else if (/safari/i.test(ua) && !/chrome|crios|crmo/i.test(ua)) {
|
|
45
|
+
name = "Safari";
|
|
46
|
+
} else if (/firefox|fxios/i.test(ua)) {
|
|
47
|
+
name = "Firefox";
|
|
48
|
+
} else if (/edg\//i.test(ua)) {
|
|
49
|
+
name = "Edge";
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const versionMatch = ua.match(/(chrome|crios|firefox|fxios|safari|edg)\/([\d.]+)/i);
|
|
53
|
+
const version = versionMatch ? versionMatch[2] : null;
|
|
54
|
+
|
|
55
|
+
const language =
|
|
56
|
+
(navigator.languages && navigator.languages[0]) ||
|
|
57
|
+
navigator.language ||
|
|
58
|
+
null;
|
|
59
|
+
|
|
60
|
+
let timezone = null;
|
|
61
|
+
try {
|
|
62
|
+
timezone = Intl.DateTimeFormat().resolvedOptions().timeZone || null;
|
|
63
|
+
} catch {
|
|
64
|
+
timezone = null;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return {
|
|
68
|
+
name,
|
|
69
|
+
version,
|
|
70
|
+
user_agent: ua,
|
|
71
|
+
language,
|
|
72
|
+
timezone,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function getOsContext() {
|
|
77
|
+
if (!isBrowser) return null;
|
|
78
|
+
const ua = navigator.userAgent || "";
|
|
79
|
+
let name = "Unknown";
|
|
80
|
+
if (/windows/i.test(ua)) name = "Windows";
|
|
81
|
+
else if (/mac os x/i.test(ua)) name = "macOS";
|
|
82
|
+
else if (/android/i.test(ua)) name = "Android";
|
|
83
|
+
else if (/iphone|ipad|ipod/i.test(ua)) name = "iOS";
|
|
84
|
+
else if (/linux/i.test(ua)) name = "Linux";
|
|
85
|
+
|
|
86
|
+
return {
|
|
87
|
+
name,
|
|
88
|
+
version: null,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function getDeviceContext() {
|
|
93
|
+
if (!isBrowser) return null;
|
|
94
|
+
const ua = navigator.userAgent || "";
|
|
95
|
+
const isMobile = /iphone|ipad|ipod|android|mobile/i.test(ua);
|
|
96
|
+
const family = isMobile ? "mobile" : "desktop";
|
|
97
|
+
|
|
98
|
+
const viewport =
|
|
99
|
+
typeof window !== "undefined"
|
|
100
|
+
? { width: window.innerWidth, height: window.innerHeight }
|
|
101
|
+
: null;
|
|
102
|
+
const screenInfo =
|
|
103
|
+
typeof window !== "undefined" && window.screen
|
|
104
|
+
? { width: window.screen.width, height: window.screen.height }
|
|
105
|
+
: null;
|
|
106
|
+
|
|
107
|
+
const ctx = { family };
|
|
108
|
+
if (viewport) {
|
|
109
|
+
ctx.viewport_width = viewport.width;
|
|
110
|
+
ctx.viewport_height = viewport.height;
|
|
111
|
+
}
|
|
112
|
+
if (screenInfo) {
|
|
113
|
+
ctx.screen_width = screenInfo.width;
|
|
114
|
+
ctx.screen_height = screenInfo.height;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return ctx;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function getRuntimeContext() {
|
|
121
|
+
if (isNode) {
|
|
122
|
+
return {
|
|
123
|
+
name: "node",
|
|
124
|
+
version: process.version,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
if (isBrowser) {
|
|
128
|
+
return {
|
|
129
|
+
name: "javascript",
|
|
130
|
+
version: null,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function setupBrowserInstrumentation() {
|
|
137
|
+
if (!isBrowser) return;
|
|
138
|
+
|
|
139
|
+
// Global error handlers (already existed but keep here with breadcrumbs)
|
|
140
|
+
window.onerror = function (msg, url, line, col, error) {
|
|
141
|
+
addBreadcrumb({
|
|
142
|
+
type: "error",
|
|
143
|
+
level: "error",
|
|
144
|
+
category: "window.onerror",
|
|
145
|
+
message: String(msg),
|
|
146
|
+
data: { url, line, col },
|
|
147
|
+
});
|
|
148
|
+
captureException(error || msg);
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
window.onunhandledrejection = function (event) {
|
|
152
|
+
addBreadcrumb({
|
|
153
|
+
type: "error",
|
|
154
|
+
level: "error",
|
|
155
|
+
category: "unhandledrejection",
|
|
156
|
+
message: String(event.reason),
|
|
157
|
+
data: {},
|
|
158
|
+
});
|
|
159
|
+
captureException(event.reason);
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
// Console breadcrumbs
|
|
163
|
+
["log", "info", "warn", "error", "debug"].forEach((level) => {
|
|
164
|
+
if (!console[level]) return;
|
|
165
|
+
const original = console[level].bind(console);
|
|
166
|
+
console[level] = (...args) => {
|
|
167
|
+
try {
|
|
168
|
+
addBreadcrumb({
|
|
169
|
+
type: "log",
|
|
170
|
+
level,
|
|
171
|
+
category: "console",
|
|
172
|
+
message: args.map(String).join(" "),
|
|
173
|
+
data: {},
|
|
174
|
+
});
|
|
175
|
+
} catch (_) {
|
|
176
|
+
// best-effort, never break console
|
|
177
|
+
}
|
|
178
|
+
original(...args);
|
|
179
|
+
};
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
// Click breadcrumbs
|
|
183
|
+
window.addEventListener(
|
|
184
|
+
"click",
|
|
185
|
+
(event) => {
|
|
186
|
+
try {
|
|
187
|
+
const target = event.target;
|
|
188
|
+
if (!target) return;
|
|
189
|
+
const element = target.closest
|
|
190
|
+
? target.closest("*")
|
|
191
|
+
: target;
|
|
192
|
+
const tag = element && element.tagName;
|
|
193
|
+
const text =
|
|
194
|
+
element && element.innerText
|
|
195
|
+
? element.innerText.trim().slice(0, 100)
|
|
196
|
+
: null;
|
|
197
|
+
const id = element && element.id;
|
|
198
|
+
const className = element && element.className;
|
|
199
|
+
|
|
200
|
+
addBreadcrumb({
|
|
201
|
+
type: "user",
|
|
202
|
+
level: "info",
|
|
203
|
+
category: "ui.click",
|
|
204
|
+
message: tag ? `Click on <${tag.toLowerCase()}>` : "Click",
|
|
205
|
+
data: { id, className, text },
|
|
206
|
+
});
|
|
207
|
+
} catch (_) {
|
|
208
|
+
// ignore
|
|
209
|
+
}
|
|
210
|
+
},
|
|
211
|
+
true
|
|
212
|
+
);
|
|
213
|
+
|
|
214
|
+
// Navigation breadcrumbs (history API + hash changes)
|
|
215
|
+
const oldHref = window.location.href;
|
|
216
|
+
addBreadcrumb({
|
|
217
|
+
type: "navigation",
|
|
218
|
+
level: "info",
|
|
219
|
+
category: "navigation",
|
|
220
|
+
message: "Page loaded",
|
|
221
|
+
data: {
|
|
222
|
+
from: null,
|
|
223
|
+
to: oldHref,
|
|
224
|
+
},
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
function recordNavigation(from, to) {
|
|
228
|
+
if (from === to) return;
|
|
229
|
+
addBreadcrumb({
|
|
230
|
+
type: "navigation",
|
|
231
|
+
level: "info",
|
|
232
|
+
category: "navigation",
|
|
233
|
+
message: `${from} -> ${to}`,
|
|
234
|
+
data: { from, to },
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const originalPushState = history.pushState;
|
|
239
|
+
history.pushState = function (...args) {
|
|
240
|
+
const from = window.location.href;
|
|
241
|
+
originalPushState.apply(history, args);
|
|
242
|
+
const to = window.location.href;
|
|
243
|
+
recordNavigation(from, to);
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
const originalReplaceState = history.replaceState;
|
|
247
|
+
history.replaceState = function (...args) {
|
|
248
|
+
const from = window.location.href;
|
|
249
|
+
originalReplaceState.apply(history, args);
|
|
250
|
+
const to = window.location.href;
|
|
251
|
+
recordNavigation(from, to);
|
|
252
|
+
};
|
|
253
|
+
|
|
254
|
+
window.addEventListener("hashchange", (event) => {
|
|
255
|
+
recordNavigation(event.oldURL, event.newURL);
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
// HTTP breadcrumbs: fetch
|
|
259
|
+
if (typeof window.fetch === "function") {
|
|
260
|
+
const originalFetch = window.fetch.bind(window);
|
|
261
|
+
window.fetch = async (input, init = {}) => {
|
|
262
|
+
const method = (init.method || "GET").toUpperCase();
|
|
263
|
+
const url = typeof input === "string" ? input : input.url;
|
|
264
|
+
const start = Date.now();
|
|
265
|
+
try {
|
|
266
|
+
const response = await originalFetch(input, init);
|
|
267
|
+
const duration = Date.now() - start;
|
|
268
|
+
addBreadcrumb({
|
|
269
|
+
type: "http",
|
|
270
|
+
level: "info",
|
|
271
|
+
category: "http",
|
|
272
|
+
message: `${method} ${url}`,
|
|
273
|
+
data: {
|
|
274
|
+
url,
|
|
275
|
+
method,
|
|
276
|
+
status_code: response.status,
|
|
277
|
+
duration_ms: duration,
|
|
278
|
+
},
|
|
279
|
+
});
|
|
280
|
+
return response;
|
|
281
|
+
} catch (error) {
|
|
282
|
+
const duration = Date.now() - start;
|
|
283
|
+
addBreadcrumb({
|
|
284
|
+
type: "http",
|
|
285
|
+
level: "error",
|
|
286
|
+
category: "http",
|
|
287
|
+
message: `${method} ${url} - failed`,
|
|
288
|
+
data: {
|
|
289
|
+
url,
|
|
290
|
+
method,
|
|
291
|
+
error: String(error),
|
|
292
|
+
duration_ms: duration,
|
|
293
|
+
},
|
|
294
|
+
});
|
|
295
|
+
throw error;
|
|
296
|
+
}
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// HTTP breadcrumbs: XHR
|
|
301
|
+
if (typeof XMLHttpRequest !== "undefined") {
|
|
302
|
+
const OriginalXHR = XMLHttpRequest;
|
|
303
|
+
function WrappedXHR() {
|
|
304
|
+
const xhr = new OriginalXHR();
|
|
305
|
+
let url = null;
|
|
306
|
+
let method = null;
|
|
307
|
+
|
|
308
|
+
const origOpen = xhr.open;
|
|
309
|
+
xhr.open = function (m, u, ...rest) {
|
|
310
|
+
method = m;
|
|
311
|
+
url = u;
|
|
312
|
+
origOpen.call(xhr, m, u, ...rest);
|
|
313
|
+
};
|
|
314
|
+
|
|
315
|
+
const origSend = xhr.send;
|
|
316
|
+
xhr.send = function (...sendArgs) {
|
|
317
|
+
const start = Date.now();
|
|
318
|
+
xhr.addEventListener("loadend", () => {
|
|
319
|
+
const duration = Date.now() - start;
|
|
320
|
+
addBreadcrumb({
|
|
321
|
+
type: "http",
|
|
322
|
+
level: "info",
|
|
323
|
+
category: "http",
|
|
324
|
+
message: `${method} ${url}`,
|
|
325
|
+
data: {
|
|
326
|
+
url,
|
|
327
|
+
method,
|
|
328
|
+
status_code: xhr.status,
|
|
329
|
+
duration_ms: duration,
|
|
330
|
+
},
|
|
331
|
+
});
|
|
332
|
+
});
|
|
333
|
+
origSend.apply(xhr, sendArgs);
|
|
334
|
+
};
|
|
335
|
+
|
|
336
|
+
return xhr;
|
|
337
|
+
}
|
|
338
|
+
// eslint-disable-next-line no-global-assign
|
|
339
|
+
XMLHttpRequest = WrappedXHR;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// Parse a JavaScript Error.stack string into structured frames compatible with backend
|
|
344
|
+
function buildStacktraceFromError(error) {
|
|
345
|
+
if (!error || !error.stack || typeof error.stack !== "string") {
|
|
346
|
+
return null;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
const stackLines = error.stack.split("\n");
|
|
350
|
+
const frames = [];
|
|
351
|
+
|
|
352
|
+
for (let i = 1; i < stackLines.length; i++) {
|
|
353
|
+
const line = stackLines[i].trim();
|
|
354
|
+
// Examples:
|
|
355
|
+
// at funcName (http://localhost:5173/src/App.tsx:10:15)
|
|
356
|
+
// at http://localhost:5173/assets/index-abc123.js:1:1234
|
|
357
|
+
const match =
|
|
358
|
+
line.match(/^at\s+(.*?)\s+\((.*?):(\d+):(\d+)\)$/) ||
|
|
359
|
+
line.match(/^at\s+(.*?):(\d+):(\d+)$/);
|
|
360
|
+
|
|
361
|
+
if (!match) continue;
|
|
362
|
+
|
|
363
|
+
let functionName;
|
|
364
|
+
let file;
|
|
365
|
+
let lineNo;
|
|
366
|
+
let colNo;
|
|
367
|
+
|
|
368
|
+
if (match.length === 5) {
|
|
369
|
+
functionName = match[1] || "<anonymous>";
|
|
370
|
+
file = match[2];
|
|
371
|
+
lineNo = parseInt(match[3], 10);
|
|
372
|
+
colNo = parseInt(match[4], 10);
|
|
373
|
+
} else {
|
|
374
|
+
functionName = "<anonymous>";
|
|
375
|
+
file = match[1];
|
|
376
|
+
lineNo = parseInt(match[2], 10);
|
|
377
|
+
colNo = parseInt(match[3], 10);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
const filename = file.split("/").pop() || file;
|
|
381
|
+
|
|
382
|
+
frames.push({
|
|
383
|
+
filename,
|
|
384
|
+
abs_path: file,
|
|
385
|
+
lineno: lineNo,
|
|
386
|
+
colno: colNo,
|
|
387
|
+
function: functionName,
|
|
388
|
+
module: null,
|
|
389
|
+
context_line: null,
|
|
390
|
+
pre_context: [],
|
|
391
|
+
post_context: [],
|
|
392
|
+
vars: {},
|
|
393
|
+
in_app: true,
|
|
394
|
+
});
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
if (!frames.length) {
|
|
398
|
+
return null;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// Backend expects oldest frame first, newest last; JS stacks are newest first,
|
|
402
|
+
// so we reverse to be consistent with Python-style traces.
|
|
403
|
+
frames.reverse();
|
|
404
|
+
|
|
405
|
+
return { frames };
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
import { initTracing } from "./tracing.js";
|
|
409
|
+
|
|
410
|
+
export function register({
|
|
411
|
+
dsn,
|
|
412
|
+
app_env = "production",
|
|
413
|
+
release = null,
|
|
414
|
+
debug = false,
|
|
415
|
+
}) {
|
|
416
|
+
DSN = dsn;
|
|
417
|
+
APP_ENV = app_env;
|
|
418
|
+
RELEASE = release;
|
|
419
|
+
DEBUG = debug;
|
|
420
|
+
|
|
421
|
+
// Initialize tracing
|
|
422
|
+
initTracing(dsn, app_env, debug);
|
|
423
|
+
|
|
424
|
+
if (DEBUG) {
|
|
425
|
+
// Parse DSN to show API URL in debug
|
|
426
|
+
const parsed = parseDsn(dsn);
|
|
427
|
+
const apiUrl = parsed ? parsed.apiUrl : "default";
|
|
428
|
+
const envApiUrl =
|
|
429
|
+
(isNode && process.env.WATCHFORGE_API_URL) ||
|
|
430
|
+
(isBrowser &&
|
|
431
|
+
typeof window !== "undefined" &&
|
|
432
|
+
window.WATCHFORGE_API_URL) ||
|
|
433
|
+
null;
|
|
434
|
+
console.log("WatchForge SDK initialized", {
|
|
435
|
+
dsn: dsn.substring(0, 50) + "...",
|
|
436
|
+
app_env,
|
|
437
|
+
release: release || "not set",
|
|
438
|
+
apiUrl: envApiUrl || apiUrl,
|
|
439
|
+
source: envApiUrl ? "environment variable" : "DSN-derived",
|
|
440
|
+
});
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
// Browser: Set up full instrumentation (errors, breadcrumbs, HTTP, navigation)
|
|
444
|
+
if (isBrowser) {
|
|
445
|
+
setupBrowserInstrumentation();
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
// Node.js: Set up process error handlers
|
|
449
|
+
if (isNode) {
|
|
450
|
+
process.on("uncaughtException", (error) => {
|
|
451
|
+
captureException(error);
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
process.on("unhandledRejection", (reason) => {
|
|
455
|
+
captureException(reason);
|
|
456
|
+
});
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
// Deprecated: Use register() instead
|
|
461
|
+
export function init(options) {
|
|
462
|
+
console.warn("WatchForge SDK: init() is deprecated. Use register() instead.");
|
|
463
|
+
return register(options);
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
export function captureException(error, context = {}) {
|
|
467
|
+
if (!DSN) return;
|
|
468
|
+
|
|
469
|
+
const stacktrace = buildStacktraceFromError(error);
|
|
470
|
+
|
|
471
|
+
const event = {
|
|
472
|
+
event_id: generateEventId(),
|
|
473
|
+
level: "error",
|
|
474
|
+
environment: APP_ENV,
|
|
475
|
+
message: error?.message || String(error),
|
|
476
|
+
stacktrace: stacktrace || error?.stack,
|
|
477
|
+
timestamp: new Date().toISOString(),
|
|
478
|
+
platform: isBrowser ? "javascript" : "node",
|
|
479
|
+
};
|
|
480
|
+
|
|
481
|
+
// Add release if set
|
|
482
|
+
if (RELEASE) {
|
|
483
|
+
event.release = RELEASE;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
// Add context
|
|
487
|
+
if (context.user) {
|
|
488
|
+
event.user = context.user;
|
|
489
|
+
}
|
|
490
|
+
if (context.request) {
|
|
491
|
+
event.request = context.request;
|
|
492
|
+
}
|
|
493
|
+
if (context.tags) {
|
|
494
|
+
event.tags = context.tags;
|
|
495
|
+
}
|
|
496
|
+
if (context.extra) {
|
|
497
|
+
event.extra = context.extra;
|
|
498
|
+
}
|
|
499
|
+
// In browser, auto-populate basic request + URL tags if not provided
|
|
500
|
+
if (isBrowser) {
|
|
501
|
+
const url = typeof window !== "undefined" ? window.location.href : undefined;
|
|
502
|
+
const referrer =
|
|
503
|
+
typeof document !== "undefined" ? document.referrer || undefined : undefined;
|
|
504
|
+
if (!event.request && url) {
|
|
505
|
+
event.request = {
|
|
506
|
+
url,
|
|
507
|
+
method: "GET",
|
|
508
|
+
headers: {},
|
|
509
|
+
query: {},
|
|
510
|
+
body: undefined,
|
|
511
|
+
};
|
|
512
|
+
}
|
|
513
|
+
event.tags = {
|
|
514
|
+
...(event.tags || {}),
|
|
515
|
+
url: url || (event.tags && event.tags.url),
|
|
516
|
+
referrer: referrer || (event.tags && event.tags.referrer),
|
|
517
|
+
};
|
|
518
|
+
}
|
|
519
|
+
// Attach runtime + environment contexts (Sentry-like)
|
|
520
|
+
const runtime = getRuntimeContext();
|
|
521
|
+
const browser = getBrowserContext();
|
|
522
|
+
const os = getOsContext();
|
|
523
|
+
const device = getDeviceContext();
|
|
524
|
+
|
|
525
|
+
event.contexts = {
|
|
526
|
+
...(context.contexts || {}),
|
|
527
|
+
...(runtime ? { runtime } : {}),
|
|
528
|
+
...(browser ? { browser } : {}),
|
|
529
|
+
...(os ? { os } : {}),
|
|
530
|
+
...(device ? { device } : {}),
|
|
531
|
+
};
|
|
532
|
+
|
|
533
|
+
// Attach breadcrumbs (copied snapshot so later mutations don't affect this event)
|
|
534
|
+
const bcs = getBreadcrumbsSnapshot();
|
|
535
|
+
if (bcs.length > 0) {
|
|
536
|
+
event.breadcrumbs = bcs;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
if (DEBUG) {
|
|
540
|
+
console.log("WatchForge SDK - Capturing exception:", event);
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
sendEvent(DSN, event);
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
export function captureMessage(message, level = "info", context = {}) {
|
|
547
|
+
if (!DSN) return;
|
|
548
|
+
|
|
549
|
+
const event = {
|
|
550
|
+
event_id: generateEventId(),
|
|
551
|
+
level,
|
|
552
|
+
environment: APP_ENV,
|
|
553
|
+
message,
|
|
554
|
+
timestamp: new Date().toISOString(),
|
|
555
|
+
platform: isBrowser ? "javascript" : "node",
|
|
556
|
+
};
|
|
557
|
+
|
|
558
|
+
// Add release if set
|
|
559
|
+
if (RELEASE) {
|
|
560
|
+
event.release = RELEASE;
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
// Add context
|
|
564
|
+
if (context.user) {
|
|
565
|
+
event.user = context.user;
|
|
566
|
+
}
|
|
567
|
+
if (context.request) {
|
|
568
|
+
event.request = context.request;
|
|
569
|
+
}
|
|
570
|
+
if (context.tags) {
|
|
571
|
+
event.tags = context.tags;
|
|
572
|
+
}
|
|
573
|
+
if (context.extra) {
|
|
574
|
+
event.extra = context.extra;
|
|
575
|
+
}
|
|
576
|
+
if (isBrowser) {
|
|
577
|
+
const url = typeof window !== "undefined" ? window.location.href : undefined;
|
|
578
|
+
const referrer =
|
|
579
|
+
typeof document !== "undefined" ? document.referrer || undefined : undefined;
|
|
580
|
+
if (!event.request && url) {
|
|
581
|
+
event.request = {
|
|
582
|
+
url,
|
|
583
|
+
method: "GET",
|
|
584
|
+
headers: {},
|
|
585
|
+
query: {},
|
|
586
|
+
body: undefined,
|
|
587
|
+
};
|
|
588
|
+
}
|
|
589
|
+
event.tags = {
|
|
590
|
+
...(event.tags || {}),
|
|
591
|
+
url: url || (event.tags && event.tags.url),
|
|
592
|
+
referrer: referrer || (event.tags && event.tags.referrer),
|
|
593
|
+
};
|
|
594
|
+
}
|
|
595
|
+
// Attach runtime + environment contexts
|
|
596
|
+
const runtime = getRuntimeContext();
|
|
597
|
+
const browser = getBrowserContext();
|
|
598
|
+
const os = getOsContext();
|
|
599
|
+
const device = getDeviceContext();
|
|
600
|
+
|
|
601
|
+
event.contexts = {
|
|
602
|
+
...(context.contexts || {}),
|
|
603
|
+
...(runtime ? { runtime } : {}),
|
|
604
|
+
...(browser ? { browser } : {}),
|
|
605
|
+
...(os ? { os } : {}),
|
|
606
|
+
...(device ? { device } : {}),
|
|
607
|
+
};
|
|
608
|
+
|
|
609
|
+
const bcs = getBreadcrumbsSnapshot();
|
|
610
|
+
if (bcs.length > 0) {
|
|
611
|
+
event.breadcrumbs = bcs;
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
if (DEBUG) {
|
|
615
|
+
console.log("WatchForge SDK - Capturing message:", event);
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
sendEvent(DSN, event);
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
// Generate unique event ID
|
|
622
|
+
function generateEventId() {
|
|
623
|
+
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, function (c) {
|
|
624
|
+
const r = (Math.random() * 16) | 0;
|
|
625
|
+
const v = c === "x" ? r : (r & 0x3) | 0x8;
|
|
626
|
+
return v.toString(16);
|
|
627
|
+
});
|
|
628
|
+
}
|
package/src/express.js
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Express.js integration for WatchForge SDK
|
|
3
|
+
*
|
|
4
|
+
* Usage:
|
|
5
|
+
* import express from "express";
|
|
6
|
+
* import { register } from "@watchforge/browser";
|
|
7
|
+
* import { expressMiddleware } from "@watchforge/browser/express";
|
|
8
|
+
*
|
|
9
|
+
* register({ dsn: "...", app_env: "production" });
|
|
10
|
+
* app.use(express.json());
|
|
11
|
+
* app.use(expressMiddleware());
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { captureException } from "./client.js";
|
|
15
|
+
|
|
16
|
+
export function expressMiddleware() {
|
|
17
|
+
return function (err, req, res, next) {
|
|
18
|
+
// Extract IP address
|
|
19
|
+
const ip =
|
|
20
|
+
req.ip ||
|
|
21
|
+
req.connection?.remoteAddress ||
|
|
22
|
+
req.socket?.remoteAddress ||
|
|
23
|
+
(req.connection?.socket ? req.connection.socket.remoteAddress : null) ||
|
|
24
|
+
req.headers["x-forwarded-for"]?.split(",")[0]?.trim() ||
|
|
25
|
+
req.headers["x-real-ip"] ||
|
|
26
|
+
null;
|
|
27
|
+
|
|
28
|
+
// Build user context
|
|
29
|
+
const user = req.user
|
|
30
|
+
? {
|
|
31
|
+
id: String(req.user.id),
|
|
32
|
+
email: req.user.email || null,
|
|
33
|
+
username: req.user.username || null,
|
|
34
|
+
ip_address: ip,
|
|
35
|
+
}
|
|
36
|
+
: null;
|
|
37
|
+
|
|
38
|
+
// Build request context (matches backend's request data schema)
|
|
39
|
+
const request = {
|
|
40
|
+
url: `${req.protocol}://${req.get("host")}${req.originalUrl || req.url}`,
|
|
41
|
+
method: req.method,
|
|
42
|
+
headers: sanitizeHeaders(req.headers),
|
|
43
|
+
query: req.query || {},
|
|
44
|
+
body: req.body || {},
|
|
45
|
+
ip: ip,
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
// Capture exception with rich API-level context
|
|
49
|
+
captureException(err, {
|
|
50
|
+
user,
|
|
51
|
+
request,
|
|
52
|
+
tags: {
|
|
53
|
+
framework: "express",
|
|
54
|
+
route: req.route?.path || req.path,
|
|
55
|
+
status_code: res && res.statusCode,
|
|
56
|
+
},
|
|
57
|
+
extra: {
|
|
58
|
+
params: req.params || {},
|
|
59
|
+
},
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
// Call next to continue error handling
|
|
63
|
+
next(err);
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function sanitizeHeaders(headers) {
|
|
68
|
+
const sanitized = {};
|
|
69
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
70
|
+
const lowerKey = key.toLowerCase();
|
|
71
|
+
// Exclude sensitive headers
|
|
72
|
+
if (!["authorization", "cookie", "x-csrftoken", "x-api-key"].includes(lowerKey)) {
|
|
73
|
+
sanitized[key] = value;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return sanitized;
|
|
77
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// Export main functions
|
|
2
|
+
export { register, captureException, captureMessage } from "./client.js";
|
|
3
|
+
|
|
4
|
+
// Export deprecated init for backward compatibility
|
|
5
|
+
export { init } from "./client.js";
|
|
6
|
+
|
|
7
|
+
// Export tracing functions
|
|
8
|
+
export { startTransaction, getCurrentTransaction, finishTransaction, Transaction, Span } from "./tracing.js";
|
|
9
|
+
|
|
10
|
+
// Export framework integrations
|
|
11
|
+
export { expressMiddleware } from "./express.js";
|
|
12
|
+
export { ErrorBoundary } from "./react.js";
|