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