@statly/observe 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.
@@ -0,0 +1,870 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/integrations/fastify.ts
21
+ var fastify_exports = {};
22
+ __export(fastify_exports, {
23
+ createRequestCapture: () => createRequestCapture,
24
+ statlyFastifyPlugin: () => statlyFastifyPlugin,
25
+ statlyPlugin: () => statlyPlugin
26
+ });
27
+ module.exports = __toCommonJS(fastify_exports);
28
+
29
+ // src/transport.ts
30
+ var Transport = class {
31
+ constructor(options) {
32
+ this.queue = [];
33
+ this.isSending = false;
34
+ this.maxQueueSize = 100;
35
+ this.flushInterval = 5e3;
36
+ this.dsn = options.dsn;
37
+ this.debug = options.debug ?? false;
38
+ this.endpoint = this.parseEndpoint(options.dsn);
39
+ this.startFlushTimer();
40
+ }
41
+ parseEndpoint(dsn) {
42
+ try {
43
+ const url = new URL(dsn);
44
+ return `${url.protocol}//${url.host}/api/v1/observe/ingest`;
45
+ } catch {
46
+ return `https://statly.live/api/v1/observe/ingest`;
47
+ }
48
+ }
49
+ startFlushTimer() {
50
+ if (typeof window !== "undefined") {
51
+ this.flushTimer = setInterval(() => {
52
+ this.flush();
53
+ }, this.flushInterval);
54
+ }
55
+ }
56
+ /**
57
+ * Add an event to the queue
58
+ */
59
+ enqueue(event) {
60
+ if (this.queue.length >= this.maxQueueSize) {
61
+ this.queue.shift();
62
+ if (this.debug) {
63
+ console.warn("[Statly] Event queue full, dropping oldest event");
64
+ }
65
+ }
66
+ this.queue.push(event);
67
+ if (this.queue.length >= 10) {
68
+ this.flush();
69
+ }
70
+ }
71
+ /**
72
+ * Send a single event immediately
73
+ */
74
+ async send(event) {
75
+ return this.sendBatch([event]);
76
+ }
77
+ /**
78
+ * Flush all queued events
79
+ */
80
+ async flush() {
81
+ if (this.isSending || this.queue.length === 0) {
82
+ return;
83
+ }
84
+ this.isSending = true;
85
+ const events = [...this.queue];
86
+ this.queue = [];
87
+ try {
88
+ await this.sendBatch(events);
89
+ } catch (error) {
90
+ this.queue = [...events, ...this.queue].slice(0, this.maxQueueSize);
91
+ if (this.debug) {
92
+ console.error("[Statly] Failed to send events:", error);
93
+ }
94
+ } finally {
95
+ this.isSending = false;
96
+ }
97
+ }
98
+ /**
99
+ * Send a batch of events
100
+ */
101
+ async sendBatch(events) {
102
+ if (events.length === 0) {
103
+ return { success: true };
104
+ }
105
+ const payload = events.length === 1 ? events[0] : { events };
106
+ try {
107
+ const response = await fetch(this.endpoint, {
108
+ method: "POST",
109
+ headers: {
110
+ "Content-Type": "application/json",
111
+ "X-Statly-DSN": this.dsn
112
+ },
113
+ body: JSON.stringify(payload),
114
+ // Use keepalive for better reliability during page unload
115
+ keepalive: true
116
+ });
117
+ if (!response.ok) {
118
+ const errorText = await response.text().catch(() => "Unknown error");
119
+ if (this.debug) {
120
+ console.error("[Statly] API error:", response.status, errorText);
121
+ }
122
+ return {
123
+ success: false,
124
+ status: response.status,
125
+ error: errorText
126
+ };
127
+ }
128
+ if (this.debug) {
129
+ console.log(`[Statly] Sent ${events.length} event(s)`);
130
+ }
131
+ return { success: true, status: response.status };
132
+ } catch (error) {
133
+ if (this.debug) {
134
+ console.error("[Statly] Network error:", error);
135
+ }
136
+ return {
137
+ success: false,
138
+ error: error instanceof Error ? error.message : "Network error"
139
+ };
140
+ }
141
+ }
142
+ /**
143
+ * Clean up resources
144
+ */
145
+ destroy() {
146
+ if (this.flushTimer) {
147
+ clearInterval(this.flushTimer);
148
+ }
149
+ this.flush();
150
+ }
151
+ };
152
+
153
+ // src/breadcrumbs.ts
154
+ var BreadcrumbManager = class {
155
+ constructor(maxBreadcrumbs = 100) {
156
+ this.breadcrumbs = [];
157
+ this.maxBreadcrumbs = maxBreadcrumbs;
158
+ }
159
+ /**
160
+ * Add a breadcrumb
161
+ */
162
+ add(breadcrumb) {
163
+ const crumb = {
164
+ timestamp: Date.now(),
165
+ ...breadcrumb
166
+ };
167
+ this.breadcrumbs.push(crumb);
168
+ if (this.breadcrumbs.length > this.maxBreadcrumbs) {
169
+ this.breadcrumbs = this.breadcrumbs.slice(-this.maxBreadcrumbs);
170
+ }
171
+ }
172
+ /**
173
+ * Get all breadcrumbs
174
+ */
175
+ getAll() {
176
+ return [...this.breadcrumbs];
177
+ }
178
+ /**
179
+ * Clear all breadcrumbs
180
+ */
181
+ clear() {
182
+ this.breadcrumbs = [];
183
+ }
184
+ /**
185
+ * Set maximum breadcrumbs to keep
186
+ */
187
+ setMaxBreadcrumbs(max) {
188
+ this.maxBreadcrumbs = max;
189
+ if (this.breadcrumbs.length > max) {
190
+ this.breadcrumbs = this.breadcrumbs.slice(-max);
191
+ }
192
+ }
193
+ };
194
+
195
+ // src/integrations/global-handlers.ts
196
+ var GlobalHandlers = class {
197
+ constructor(options = {}) {
198
+ this.originalOnError = null;
199
+ this.originalOnUnhandledRejection = null;
200
+ this.errorCallback = null;
201
+ this.handleUnhandledRejection = (event) => {
202
+ if (!this.errorCallback) {
203
+ return;
204
+ }
205
+ let error;
206
+ if (event.reason instanceof Error) {
207
+ error = event.reason;
208
+ } else if (typeof event.reason === "string") {
209
+ error = new Error(event.reason);
210
+ } else {
211
+ error = new Error("Unhandled Promise Rejection");
212
+ error.reason = event.reason;
213
+ }
214
+ this.errorCallback(error, {
215
+ mechanism: { type: "onunhandledrejection", handled: false }
216
+ });
217
+ };
218
+ this.options = {
219
+ onerror: options.onerror !== false,
220
+ onunhandledrejection: options.onunhandledrejection !== false
221
+ };
222
+ }
223
+ /**
224
+ * Install global error handlers
225
+ */
226
+ install(callback) {
227
+ this.errorCallback = callback;
228
+ if (typeof window === "undefined") {
229
+ return;
230
+ }
231
+ if (this.options.onerror) {
232
+ this.installOnError();
233
+ }
234
+ if (this.options.onunhandledrejection) {
235
+ this.installOnUnhandledRejection();
236
+ }
237
+ }
238
+ /**
239
+ * Uninstall global error handlers
240
+ */
241
+ uninstall() {
242
+ if (typeof window === "undefined") {
243
+ return;
244
+ }
245
+ if (this.originalOnError !== null) {
246
+ window.onerror = this.originalOnError;
247
+ this.originalOnError = null;
248
+ }
249
+ if (this.originalOnUnhandledRejection !== null) {
250
+ window.removeEventListener("unhandledrejection", this.handleUnhandledRejection);
251
+ this.originalOnUnhandledRejection = null;
252
+ }
253
+ this.errorCallback = null;
254
+ }
255
+ installOnError() {
256
+ this.originalOnError = window.onerror;
257
+ window.onerror = (message, source, lineno, colno, error) => {
258
+ if (this.originalOnError) {
259
+ this.originalOnError.call(window, message, source, lineno, colno, error);
260
+ }
261
+ if (this.errorCallback) {
262
+ const errorObj = error || new Error(String(message));
263
+ if (!error && source) {
264
+ errorObj.filename = source;
265
+ errorObj.lineno = lineno;
266
+ errorObj.colno = colno;
267
+ }
268
+ this.errorCallback(errorObj, {
269
+ mechanism: { type: "onerror", handled: false },
270
+ source,
271
+ lineno,
272
+ colno
273
+ });
274
+ }
275
+ return false;
276
+ };
277
+ }
278
+ installOnUnhandledRejection() {
279
+ this.originalOnUnhandledRejection = this.handleUnhandledRejection.bind(this);
280
+ window.addEventListener("unhandledrejection", this.handleUnhandledRejection);
281
+ }
282
+ };
283
+
284
+ // src/integrations/console.ts
285
+ var ConsoleIntegration = class {
286
+ constructor() {
287
+ this.originalMethods = {};
288
+ this.callback = null;
289
+ this.levels = ["debug", "info", "warn", "error", "log"];
290
+ }
291
+ /**
292
+ * Install console breadcrumb tracking
293
+ */
294
+ install(callback, levels) {
295
+ this.callback = callback;
296
+ if (levels) {
297
+ this.levels = levels;
298
+ }
299
+ if (typeof console === "undefined") {
300
+ return;
301
+ }
302
+ for (const level of this.levels) {
303
+ this.wrapConsoleMethod(level);
304
+ }
305
+ }
306
+ /**
307
+ * Uninstall console breadcrumb tracking
308
+ */
309
+ uninstall() {
310
+ if (typeof console === "undefined") {
311
+ return;
312
+ }
313
+ for (const level of this.levels) {
314
+ if (this.originalMethods[level]) {
315
+ console[level] = this.originalMethods[level];
316
+ delete this.originalMethods[level];
317
+ }
318
+ }
319
+ this.callback = null;
320
+ }
321
+ wrapConsoleMethod(level) {
322
+ const originalMethod = console[level];
323
+ if (!originalMethod) {
324
+ return;
325
+ }
326
+ this.originalMethods[level] = originalMethod;
327
+ const self = this;
328
+ console[level] = function(...args) {
329
+ if (self.callback) {
330
+ self.callback({
331
+ category: "console",
332
+ message: self.formatArgs(args),
333
+ level: self.mapLevel(level),
334
+ data: args.length > 1 ? { arguments: args } : void 0
335
+ });
336
+ }
337
+ originalMethod.apply(console, args);
338
+ };
339
+ }
340
+ formatArgs(args) {
341
+ return args.map((arg) => {
342
+ if (typeof arg === "string") {
343
+ return arg;
344
+ }
345
+ if (arg instanceof Error) {
346
+ return arg.message;
347
+ }
348
+ try {
349
+ return JSON.stringify(arg);
350
+ } catch {
351
+ return String(arg);
352
+ }
353
+ }).join(" ");
354
+ }
355
+ mapLevel(consoleLevel) {
356
+ switch (consoleLevel) {
357
+ case "debug":
358
+ return "debug";
359
+ case "info":
360
+ case "log":
361
+ return "info";
362
+ case "warn":
363
+ return "warning";
364
+ case "error":
365
+ return "error";
366
+ default:
367
+ return "info";
368
+ }
369
+ }
370
+ };
371
+
372
+ // src/client.ts
373
+ var SDK_NAME = "@statly/observe-sdk";
374
+ var SDK_VERSION = "0.1.0";
375
+ var StatlyClient = class {
376
+ constructor(options) {
377
+ this.user = null;
378
+ this.initialized = false;
379
+ this.options = this.mergeOptions(options);
380
+ this.transport = new Transport({
381
+ dsn: this.options.dsn,
382
+ debug: this.options.debug
383
+ });
384
+ this.breadcrumbs = new BreadcrumbManager(this.options.maxBreadcrumbs);
385
+ this.globalHandlers = new GlobalHandlers();
386
+ this.consoleIntegration = new ConsoleIntegration();
387
+ }
388
+ mergeOptions(options) {
389
+ return {
390
+ dsn: options.dsn,
391
+ release: options.release ?? "",
392
+ environment: options.environment ?? this.detectEnvironment(),
393
+ debug: options.debug ?? false,
394
+ sampleRate: options.sampleRate ?? 1,
395
+ maxBreadcrumbs: options.maxBreadcrumbs ?? 100,
396
+ autoCapture: options.autoCapture !== false,
397
+ captureConsole: options.captureConsole !== false,
398
+ captureNetwork: options.captureNetwork ?? false,
399
+ tags: options.tags ?? {},
400
+ beforeSend: options.beforeSend ?? ((e) => e)
401
+ };
402
+ }
403
+ detectEnvironment() {
404
+ if (typeof window !== "undefined") {
405
+ if (window.location.hostname === "localhost" || window.location.hostname === "127.0.0.1") {
406
+ return "development";
407
+ }
408
+ if (window.location.hostname.includes("staging") || window.location.hostname.includes("stage")) {
409
+ return "staging";
410
+ }
411
+ }
412
+ return "production";
413
+ }
414
+ /**
415
+ * Initialize the SDK
416
+ */
417
+ init() {
418
+ if (this.initialized) {
419
+ if (this.options.debug) {
420
+ console.warn("[Statly] SDK already initialized");
421
+ }
422
+ return;
423
+ }
424
+ this.initialized = true;
425
+ if (this.options.autoCapture) {
426
+ this.globalHandlers.install((error, context) => {
427
+ this.captureError(error, context);
428
+ });
429
+ }
430
+ if (this.options.captureConsole) {
431
+ this.consoleIntegration.install((breadcrumb) => {
432
+ this.breadcrumbs.add(breadcrumb);
433
+ });
434
+ }
435
+ this.addBreadcrumb({
436
+ category: "navigation",
437
+ message: "SDK initialized",
438
+ level: "info"
439
+ });
440
+ if (this.options.debug) {
441
+ console.log("[Statly] SDK initialized", {
442
+ environment: this.options.environment,
443
+ release: this.options.release
444
+ });
445
+ }
446
+ }
447
+ /**
448
+ * Capture an exception/error
449
+ */
450
+ captureException(error, context) {
451
+ let errorObj;
452
+ if (error instanceof Error) {
453
+ errorObj = error;
454
+ } else if (typeof error === "string") {
455
+ errorObj = new Error(error);
456
+ } else {
457
+ errorObj = new Error("Unknown error");
458
+ errorObj.originalError = error;
459
+ }
460
+ return this.captureError(errorObj, context);
461
+ }
462
+ /**
463
+ * Capture a message
464
+ */
465
+ captureMessage(message, level = "info") {
466
+ const event = this.buildEvent({
467
+ message,
468
+ level
469
+ });
470
+ return this.sendEvent(event);
471
+ }
472
+ /**
473
+ * Internal method to capture an error
474
+ */
475
+ captureError(error, context) {
476
+ if (Math.random() > this.options.sampleRate) {
477
+ return "";
478
+ }
479
+ const event = this.buildEvent({
480
+ message: error.message,
481
+ level: "error",
482
+ stack: error.stack,
483
+ exception: {
484
+ type: error.name,
485
+ value: error.message,
486
+ stacktrace: this.parseStackTrace(error.stack)
487
+ },
488
+ extra: context
489
+ });
490
+ return this.sendEvent(event);
491
+ }
492
+ /**
493
+ * Build a complete event from partial data
494
+ */
495
+ buildEvent(partial) {
496
+ const event = {
497
+ message: partial.message || "Unknown error",
498
+ timestamp: Date.now(),
499
+ level: partial.level || "error",
500
+ environment: this.options.environment,
501
+ release: this.options.release || void 0,
502
+ url: typeof window !== "undefined" ? window.location.href : void 0,
503
+ user: this.user || void 0,
504
+ tags: { ...this.options.tags, ...partial.tags },
505
+ extra: partial.extra,
506
+ breadcrumbs: this.breadcrumbs.getAll(),
507
+ browser: this.getBrowserInfo(),
508
+ os: this.getOSInfo(),
509
+ sdk: {
510
+ name: SDK_NAME,
511
+ version: SDK_VERSION
512
+ },
513
+ ...partial
514
+ };
515
+ return event;
516
+ }
517
+ /**
518
+ * Parse a stack trace string into structured frames
519
+ */
520
+ parseStackTrace(stack) {
521
+ if (!stack) {
522
+ return void 0;
523
+ }
524
+ const frames = [];
525
+ const lines = stack.split("\n");
526
+ for (const line of lines) {
527
+ const chromeMatch = line.match(/^\s*at\s+(?:(.+?)\s+\()?(.+?):(\d+):(\d+)\)?$/);
528
+ if (chromeMatch) {
529
+ frames.push({
530
+ function: chromeMatch[1] || "<anonymous>",
531
+ filename: chromeMatch[2],
532
+ lineno: parseInt(chromeMatch[3], 10),
533
+ colno: parseInt(chromeMatch[4], 10)
534
+ });
535
+ continue;
536
+ }
537
+ const firefoxMatch = line.match(/^(.+?)@(.+?):(\d+):(\d+)$/);
538
+ if (firefoxMatch) {
539
+ frames.push({
540
+ function: firefoxMatch[1] || "<anonymous>",
541
+ filename: firefoxMatch[2],
542
+ lineno: parseInt(firefoxMatch[3], 10),
543
+ colno: parseInt(firefoxMatch[4], 10)
544
+ });
545
+ }
546
+ }
547
+ return frames.length > 0 ? { frames } : void 0;
548
+ }
549
+ /**
550
+ * Send an event to the server
551
+ */
552
+ sendEvent(event) {
553
+ const processed = this.options.beforeSend(event);
554
+ if (!processed) {
555
+ if (this.options.debug) {
556
+ console.log("[Statly] Event dropped by beforeSend");
557
+ }
558
+ return "";
559
+ }
560
+ const eventId = this.generateEventId();
561
+ this.breadcrumbs.add({
562
+ category: "statly",
563
+ message: `Captured ${event.level}: ${event.message.slice(0, 50)}`,
564
+ level: "info"
565
+ });
566
+ this.transport.enqueue(processed);
567
+ if (this.options.debug) {
568
+ console.log("[Statly] Event captured:", eventId, event.message);
569
+ }
570
+ return eventId;
571
+ }
572
+ generateEventId() {
573
+ return crypto.randomUUID?.() || "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
574
+ const r = Math.random() * 16 | 0;
575
+ const v = c === "x" ? r : r & 3 | 8;
576
+ return v.toString(16);
577
+ });
578
+ }
579
+ /**
580
+ * Set user context
581
+ */
582
+ setUser(user) {
583
+ this.user = user;
584
+ if (this.options.debug && user) {
585
+ console.log("[Statly] User set:", user.id || user.email);
586
+ }
587
+ }
588
+ /**
589
+ * Set a single tag
590
+ */
591
+ setTag(key, value) {
592
+ this.options.tags[key] = value;
593
+ }
594
+ /**
595
+ * Set multiple tags
596
+ */
597
+ setTags(tags) {
598
+ Object.assign(this.options.tags, tags);
599
+ }
600
+ /**
601
+ * Add a breadcrumb
602
+ */
603
+ addBreadcrumb(breadcrumb) {
604
+ this.breadcrumbs.add(breadcrumb);
605
+ }
606
+ /**
607
+ * Get browser info
608
+ */
609
+ getBrowserInfo() {
610
+ if (typeof navigator === "undefined") {
611
+ return void 0;
612
+ }
613
+ const ua = navigator.userAgent;
614
+ let name = "Unknown";
615
+ let version = "";
616
+ if (ua.includes("Firefox/")) {
617
+ name = "Firefox";
618
+ version = ua.split("Firefox/")[1]?.split(" ")[0] || "";
619
+ } else if (ua.includes("Chrome/")) {
620
+ name = "Chrome";
621
+ version = ua.split("Chrome/")[1]?.split(" ")[0] || "";
622
+ } else if (ua.includes("Safari/") && !ua.includes("Chrome")) {
623
+ name = "Safari";
624
+ version = ua.split("Version/")[1]?.split(" ")[0] || "";
625
+ } else if (ua.includes("Edge/") || ua.includes("Edg/")) {
626
+ name = "Edge";
627
+ version = ua.split(/Edg?e?\//)[1]?.split(" ")[0] || "";
628
+ }
629
+ return { name, version };
630
+ }
631
+ /**
632
+ * Get OS info
633
+ */
634
+ getOSInfo() {
635
+ if (typeof navigator === "undefined") {
636
+ return void 0;
637
+ }
638
+ const ua = navigator.userAgent;
639
+ let name = "Unknown";
640
+ let version = "";
641
+ if (ua.includes("Windows")) {
642
+ name = "Windows";
643
+ const match = ua.match(/Windows NT (\d+\.\d+)/);
644
+ if (match) version = match[1];
645
+ } else if (ua.includes("Mac OS X")) {
646
+ name = "macOS";
647
+ const match = ua.match(/Mac OS X (\d+[._]\d+)/);
648
+ if (match) version = match[1].replace("_", ".");
649
+ } else if (ua.includes("Linux")) {
650
+ name = "Linux";
651
+ } else if (ua.includes("Android")) {
652
+ name = "Android";
653
+ const match = ua.match(/Android (\d+\.\d+)/);
654
+ if (match) version = match[1];
655
+ } else if (ua.includes("iOS") || ua.includes("iPhone") || ua.includes("iPad")) {
656
+ name = "iOS";
657
+ const match = ua.match(/OS (\d+_\d+)/);
658
+ if (match) version = match[1].replace("_", ".");
659
+ }
660
+ return { name, version };
661
+ }
662
+ /**
663
+ * Flush pending events and clean up
664
+ */
665
+ async close() {
666
+ this.globalHandlers.uninstall();
667
+ this.consoleIntegration.uninstall();
668
+ await this.transport.flush();
669
+ this.transport.destroy();
670
+ this.initialized = false;
671
+ }
672
+ /**
673
+ * Force flush pending events
674
+ */
675
+ async flush() {
676
+ await this.transport.flush();
677
+ }
678
+ };
679
+
680
+ // src/index.ts
681
+ var client = null;
682
+ function init(options) {
683
+ if (client) {
684
+ console.warn("[Statly] SDK already initialized. Call close() first to reinitialize.");
685
+ return;
686
+ }
687
+ client = new StatlyClient(options);
688
+ client.init();
689
+ }
690
+ function captureException(error, context) {
691
+ if (!client) {
692
+ console.warn("[Statly] SDK not initialized. Call Statly.init() first.");
693
+ return "";
694
+ }
695
+ return client.captureException(error, context);
696
+ }
697
+ function captureMessage(message, level = "info") {
698
+ if (!client) {
699
+ console.warn("[Statly] SDK not initialized. Call Statly.init() first.");
700
+ return "";
701
+ }
702
+ return client.captureMessage(message, level);
703
+ }
704
+ function setUser(user) {
705
+ if (!client) {
706
+ console.warn("[Statly] SDK not initialized. Call Statly.init() first.");
707
+ return;
708
+ }
709
+ client.setUser(user);
710
+ }
711
+ function setTag(key, value) {
712
+ if (!client) {
713
+ console.warn("[Statly] SDK not initialized. Call Statly.init() first.");
714
+ return;
715
+ }
716
+ client.setTag(key, value);
717
+ }
718
+ function setTags(tags) {
719
+ if (!client) {
720
+ console.warn("[Statly] SDK not initialized. Call Statly.init() first.");
721
+ return;
722
+ }
723
+ client.setTags(tags);
724
+ }
725
+ function addBreadcrumb(breadcrumb) {
726
+ if (!client) {
727
+ console.warn("[Statly] SDK not initialized. Call Statly.init() first.");
728
+ return;
729
+ }
730
+ client.addBreadcrumb(breadcrumb);
731
+ }
732
+ async function flush() {
733
+ if (!client) {
734
+ return;
735
+ }
736
+ await client.flush();
737
+ }
738
+ async function close() {
739
+ if (!client) {
740
+ return;
741
+ }
742
+ await client.close();
743
+ client = null;
744
+ }
745
+ function getClient() {
746
+ return client;
747
+ }
748
+ var Statly = {
749
+ init,
750
+ captureException,
751
+ captureMessage,
752
+ setUser,
753
+ setTag,
754
+ setTags,
755
+ addBreadcrumb,
756
+ flush,
757
+ close,
758
+ getClient
759
+ };
760
+
761
+ // src/integrations/fastify.ts
762
+ function statlyFastifyPlugin(fastify, options, done) {
763
+ const {
764
+ captureValidationErrors = true,
765
+ shouldCapture,
766
+ skipStatusCodes = [400, 401, 403, 404]
767
+ } = options;
768
+ fastify.addHook("onRequest", (request, _reply, hookDone) => {
769
+ request.statlyStartTime = Date.now();
770
+ Statly.addBreadcrumb({
771
+ category: "http",
772
+ message: `${request.method} ${request.routerPath || request.url}`,
773
+ level: "info",
774
+ data: {
775
+ method: request.method,
776
+ url: request.url,
777
+ routerPath: request.routerPath,
778
+ requestId: request.id
779
+ }
780
+ });
781
+ hookDone();
782
+ });
783
+ fastify.addHook("onResponse", (request, reply, hookDone) => {
784
+ const startTime = request.statlyStartTime;
785
+ const duration = startTime ? Date.now() - startTime : void 0;
786
+ Statly.addBreadcrumb({
787
+ category: "http",
788
+ message: `Response ${reply.statusCode}`,
789
+ level: reply.statusCode >= 400 ? "error" : "info",
790
+ data: {
791
+ statusCode: reply.statusCode,
792
+ duration,
793
+ requestId: request.id
794
+ }
795
+ });
796
+ hookDone();
797
+ });
798
+ fastify.setErrorHandler((error, request, reply) => {
799
+ const statusCode = error.statusCode || 500;
800
+ if (skipStatusCodes.includes(statusCode)) {
801
+ throw error;
802
+ }
803
+ if (!captureValidationErrors && error.validation) {
804
+ throw error;
805
+ }
806
+ if (shouldCapture && !shouldCapture(error)) {
807
+ throw error;
808
+ }
809
+ const context = {
810
+ request: {
811
+ id: request.id,
812
+ method: request.method,
813
+ url: request.url,
814
+ routerPath: request.routerPath,
815
+ headers: sanitizeHeaders(request.headers),
816
+ query: request.query,
817
+ params: request.params
818
+ },
819
+ error: {
820
+ statusCode: error.statusCode,
821
+ code: error.code
822
+ }
823
+ };
824
+ if (request.ip) {
825
+ context.ip = request.ip;
826
+ }
827
+ if (error.validation) {
828
+ context.validation = error.validation;
829
+ }
830
+ Statly.setTag("http.method", request.method);
831
+ Statly.setTag("http.url", request.routerPath || request.url);
832
+ Statly.setTag("http.status_code", String(statusCode));
833
+ Statly.captureException(error, context);
834
+ throw error;
835
+ });
836
+ done();
837
+ }
838
+ var statlyPlugin = statlyFastifyPlugin;
839
+ function createRequestCapture(request) {
840
+ return (error, additionalContext) => {
841
+ const context = {
842
+ request: {
843
+ id: request.id,
844
+ method: request.method,
845
+ url: request.url,
846
+ routerPath: request.routerPath
847
+ },
848
+ ...additionalContext
849
+ };
850
+ return Statly.captureException(error, context);
851
+ };
852
+ }
853
+ function sanitizeHeaders(headers) {
854
+ const sensitiveHeaders = ["authorization", "cookie", "x-api-key", "x-auth-token"];
855
+ const sanitized = {};
856
+ for (const [key, value] of Object.entries(headers)) {
857
+ if (sensitiveHeaders.includes(key.toLowerCase())) {
858
+ sanitized[key] = "[Filtered]";
859
+ } else {
860
+ sanitized[key] = value;
861
+ }
862
+ }
863
+ return sanitized;
864
+ }
865
+ // Annotate the CommonJS export names for ESM import in node:
866
+ 0 && (module.exports = {
867
+ createRequestCapture,
868
+ statlyFastifyPlugin,
869
+ statlyPlugin
870
+ });