svoose 0.1.8 → 0.1.10
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 +823 -619
- package/dist/index.d.ts +4 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +3 -3
- package/dist/machine/machine.svelte.d.ts +6 -3
- package/dist/machine/machine.svelte.d.ts.map +1 -1
- package/dist/machine/machine.svelte.js +1 -1
- package/dist/machine/machine.svelte.js.map +3 -3
- package/dist/machine/types.d.ts +2 -2
- package/dist/machine/types.d.ts.map +1 -1
- package/dist/metrics/index.d.ts +1 -1
- package/dist/metrics/index.d.ts.map +1 -1
- package/dist/metrics/index.js +1 -1
- package/dist/metrics/index.js.map +3 -3
- package/dist/metrics/metric.d.ts +1 -15
- package/dist/metrics/metric.d.ts.map +1 -1
- package/dist/metrics/metric.js +1 -1
- package/dist/metrics/metric.js.map +3 -3
- package/dist/metrics/typed.d.ts +1 -1
- package/dist/metrics/typed.d.ts.map +1 -1
- package/dist/metrics/typed.js.map +3 -3
- package/dist/observe/errors.d.ts.map +1 -1
- package/dist/observe/errors.js +1 -1
- package/dist/observe/errors.js.map +3 -3
- package/dist/observe/index.d.ts +4 -1
- package/dist/observe/index.d.ts.map +1 -1
- package/dist/observe/index.js +1 -1
- package/dist/observe/index.js.map +3 -3
- package/dist/observe/observe.svelte.d.ts +13 -13
- package/dist/observe/observe.svelte.d.ts.map +1 -1
- package/dist/observe/observe.svelte.js +1 -1
- package/dist/observe/observe.svelte.js.map +3 -3
- package/dist/observe/presets.d.ts +19 -0
- package/dist/observe/presets.d.ts.map +1 -0
- package/dist/observe/presets.js +2 -0
- package/dist/observe/presets.js.map +7 -0
- package/dist/observe/sampling.d.ts.map +1 -1
- package/dist/observe/sampling.js +1 -1
- package/dist/observe/sampling.js.map +2 -2
- package/dist/observe/session.d.ts +1 -1
- package/dist/observe/session.d.ts.map +1 -1
- package/dist/observe/session.js +1 -1
- package/dist/observe/session.js.map +3 -3
- package/dist/observe/vitals.d.ts.map +1 -1
- package/dist/observe/vitals.js +1 -1
- package/dist/observe/vitals.js.map +2 -2
- package/dist/svelte/index.svelte.d.ts +2 -2
- package/dist/svelte/index.svelte.d.ts.map +1 -1
- package/dist/svelte/index.svelte.js +1 -1
- package/dist/svelte/index.svelte.js.map +3 -3
- package/dist/transport/fetch.d.ts +3 -3
- package/dist/transport/fetch.d.ts.map +1 -1
- package/dist/transport/fetch.js +1 -1
- package/dist/transport/fetch.js.map +3 -3
- package/dist/transport/hybrid.d.ts +3 -0
- package/dist/transport/hybrid.d.ts.map +1 -1
- package/dist/transport/hybrid.js +1 -1
- package/dist/transport/hybrid.js.map +3 -3
- package/dist/transport/index.d.ts +2 -1
- package/dist/transport/index.d.ts.map +1 -1
- package/dist/transport/index.js +1 -1
- package/dist/transport/index.js.map +3 -3
- package/dist/transport/retry.d.ts +25 -0
- package/dist/transport/retry.d.ts.map +1 -0
- package/dist/transport/retry.js +2 -0
- package/dist/transport/retry.js.map +7 -0
- package/dist/transport/transport.d.ts +1 -1
- package/dist/transport/transport.d.ts.map +1 -1
- package/dist/types/index.d.ts +76 -8
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +72 -72
- package/dist/upgrade/after.d.ts +0 -28
- package/dist/upgrade/after.d.ts.map +0 -1
- package/dist/upgrade/history.d.ts +0 -36
- package/dist/upgrade/history.d.ts.map +0 -1
- package/dist/upgrade/index.d.ts +0 -25
- package/dist/upgrade/index.d.ts.map +0 -1
- package/dist/upgrade/invoke.d.ts +0 -39
- package/dist/upgrade/invoke.d.ts.map +0 -1
- package/dist/upgrade/parallel.d.ts +0 -36
- package/dist/upgrade/parallel.d.ts.map +0 -1
- package/dist/upgrade/spawn.d.ts +0 -35
- package/dist/upgrade/spawn.d.ts.map +0 -1
package/README.md
CHANGED
|
@@ -1,619 +1,823 @@
|
|
|
1
|
-
# svoose
|
|
2
|
-
|
|
3
|
-
> Svelte + Goose = **svoose** — the goose that sees everything
|
|
4
|
-
|
|
5
|
-
Lightweight observability + state machines for Svelte 5. Zero dependencies. Tree-shakeable. **~
|
|
6
|
-
|
|
7
|
-
## Features
|
|
8
|
-
|
|
9
|
-
- **Web Vitals** — CLS, LCP, FID, INP, FCP, TTFB (no external deps)
|
|
10
|
-
- **Error Tracking** — global errors + unhandled rejections
|
|
11
|
-
- **Custom Metrics** — `metric()`, `counter()`, `gauge()`, `histogram()`
|
|
12
|
-
- **Beacon Transport** — reliable delivery on page close with auto-chunking
|
|
13
|
-
- **Session Tracking** — automatic sessionId with timeout
|
|
14
|
-
- **Sampling** — per-event-type rate limiting
|
|
15
|
-
- **State Machines** — minimal FSM with TypeScript inference
|
|
16
|
-
- **Svelte 5 Native** — reactive `useMachine()` hook with $state runes
|
|
17
|
-
- **Tree-shakeable** — pay only for what you use
|
|
18
|
-
|
|
19
|
-
## Installation
|
|
20
|
-
|
|
21
|
-
```bash
|
|
22
|
-
npm install svoose
|
|
23
|
-
```
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
});
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
```
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
```
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
errors: 1.0,
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
//
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
//
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
//
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
//
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
```
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
//
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
});
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
});
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
}
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
},
|
|
524
|
-
}
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
{
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
```
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
1
|
+
# svoose
|
|
2
|
+
|
|
3
|
+
> Svelte + Goose = **svoose** — the goose that sees everything
|
|
4
|
+
|
|
5
|
+
Lightweight observability + state machines for Svelte 5. Zero dependencies. Tree-shakeable. **~6.7KB gzipped** (observe-only ~5.1KB).
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
- **Web Vitals** — CLS, LCP, FID, INP, FCP, TTFB (no external deps)
|
|
10
|
+
- **Error Tracking** — global errors + unhandled rejections
|
|
11
|
+
- **Custom Metrics** — `metric()`, `counter()`, `gauge()`, `histogram()`
|
|
12
|
+
- **Beacon Transport** — reliable delivery on page close with auto-chunking
|
|
13
|
+
- **Session Tracking** — automatic sessionId with timeout
|
|
14
|
+
- **Sampling** — per-event-type rate limiting
|
|
15
|
+
- **State Machines** — minimal FSM with TypeScript inference
|
|
16
|
+
- **Svelte 5 Native** — reactive `useMachine()` hook with $state runes
|
|
17
|
+
- **Tree-shakeable** — pay only for what you use
|
|
18
|
+
|
|
19
|
+
## Installation
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
npm install svoose
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
> svoose works without Svelte. The `svelte` peer dependency is optional — only needed if you use `svoose/svelte` (useMachine hook).
|
|
26
|
+
|
|
27
|
+
## Quick Start
|
|
28
|
+
|
|
29
|
+
### Step 1: See what svoose collects
|
|
30
|
+
|
|
31
|
+
Start with the console transport — you'll see events in DevTools immediately, no backend needed:
|
|
32
|
+
|
|
33
|
+
```typescript
|
|
34
|
+
import { observe, createConsoleTransport } from 'svoose';
|
|
35
|
+
|
|
36
|
+
const cleanup = observe({
|
|
37
|
+
transport: createConsoleTransport({ pretty: true }),
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
// Open DevTools console — you'll see Web Vitals, errors, and metrics as they happen
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### Step 2: Send to your backend
|
|
44
|
+
|
|
45
|
+
When you're ready, switch to an endpoint:
|
|
46
|
+
|
|
47
|
+
```typescript
|
|
48
|
+
import { observe } from 'svoose';
|
|
49
|
+
|
|
50
|
+
const obs = observe({
|
|
51
|
+
endpoint: '/api/metrics',
|
|
52
|
+
errors: true,
|
|
53
|
+
vitals: true,
|
|
54
|
+
session: true,
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
// New API
|
|
58
|
+
obs.flush(); // send buffered events now
|
|
59
|
+
obs.getStats(); // { buffered: 3, sent: 47, dropped: 0 }
|
|
60
|
+
obs.onEvent(e => ...); // subscribe to events
|
|
61
|
+
|
|
62
|
+
// Stop observing when done
|
|
63
|
+
obs.destroy();
|
|
64
|
+
// or: obs() — backward compatible, same as destroy()
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### Step 3: Add custom metrics and state machines
|
|
68
|
+
|
|
69
|
+
```typescript
|
|
70
|
+
import { observe, metric, counter, createMachine } from 'svoose';
|
|
71
|
+
|
|
72
|
+
observe({ endpoint: '/api/metrics' });
|
|
73
|
+
|
|
74
|
+
// Track custom events
|
|
75
|
+
metric('checkout_started', { step: 1, cartTotal: 99.99 });
|
|
76
|
+
counter('page_views');
|
|
77
|
+
|
|
78
|
+
// State machine with automatic transition tracking
|
|
79
|
+
const auth = createMachine({
|
|
80
|
+
id: 'auth',
|
|
81
|
+
initial: 'idle',
|
|
82
|
+
context: { user: null },
|
|
83
|
+
states: {
|
|
84
|
+
idle: { on: { LOGIN: 'loading' } },
|
|
85
|
+
loading: {
|
|
86
|
+
on: {
|
|
87
|
+
SUCCESS: {
|
|
88
|
+
target: 'authenticated',
|
|
89
|
+
action: (ctx, e) => ({ user: e.user }),
|
|
90
|
+
},
|
|
91
|
+
ERROR: 'idle',
|
|
92
|
+
},
|
|
93
|
+
},
|
|
94
|
+
authenticated: { on: { LOGOUT: 'idle' } },
|
|
95
|
+
},
|
|
96
|
+
observe: true,
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
auth.send('LOGIN');
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
## What Data Looks Like
|
|
103
|
+
|
|
104
|
+
svoose sends JSON arrays via `POST` to your endpoint. Here's an example batch:
|
|
105
|
+
|
|
106
|
+
```json
|
|
107
|
+
[
|
|
108
|
+
{
|
|
109
|
+
"type": "vital",
|
|
110
|
+
"name": "LCP",
|
|
111
|
+
"value": 1234,
|
|
112
|
+
"rating": "good",
|
|
113
|
+
"delta": 1234,
|
|
114
|
+
"timestamp": 1710500000000,
|
|
115
|
+
"url": "https://myapp.com/dashboard",
|
|
116
|
+
"sessionId": "1710500000000-a1b2c3"
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
"type": "error",
|
|
120
|
+
"message": "Cannot read properties of null (reading 'id')",
|
|
121
|
+
"stack": "TypeError: Cannot read properties...\n at handleClick (app.js:42)",
|
|
122
|
+
"filename": "app.js",
|
|
123
|
+
"lineno": 42,
|
|
124
|
+
"timestamp": 1710500001000,
|
|
125
|
+
"url": "https://myapp.com/dashboard",
|
|
126
|
+
"sessionId": "1710500000000-a1b2c3",
|
|
127
|
+
"machineId": "auth",
|
|
128
|
+
"machineState": "loading"
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
"type": "transition",
|
|
132
|
+
"machineId": "auth",
|
|
133
|
+
"from": "idle",
|
|
134
|
+
"to": "loading",
|
|
135
|
+
"event": "LOGIN",
|
|
136
|
+
"timestamp": 1710500002000,
|
|
137
|
+
"sessionId": "1710500000000-a1b2c3"
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
"type": "custom",
|
|
141
|
+
"name": "page_views",
|
|
142
|
+
"metricKind": "counter",
|
|
143
|
+
"value": 1,
|
|
144
|
+
"timestamp": 1710500003000,
|
|
145
|
+
"sessionId": "1710500000000-a1b2c3"
|
|
146
|
+
}
|
|
147
|
+
]
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
**Event types:**
|
|
151
|
+
|
|
152
|
+
| Type | When | Key fields |
|
|
153
|
+
|------|------|------------|
|
|
154
|
+
| `vital` | Web Vital measured (LCP, CLS, INP, etc.) | `name`, `value`, `rating` |
|
|
155
|
+
| `error` | Uncaught error | `message`, `stack`, `machineState` |
|
|
156
|
+
| `unhandled-rejection` | Unhandled promise rejection | `reason`, `machineState` |
|
|
157
|
+
| `transition` | State machine transition | `machineId`, `from`, `to`, `event` |
|
|
158
|
+
| `custom` | `metric()`, `counter()`, `gauge()`, `histogram()` | `name`, `metricKind`, `value`, `metadata` |
|
|
159
|
+
|
|
160
|
+
### What data leaves your browser
|
|
161
|
+
|
|
162
|
+
Every event svoose sends is JSON you can inspect with `createConsoleTransport()`. Here's what each field contains:
|
|
163
|
+
|
|
164
|
+
| Field | Source | May contain PII? |
|
|
165
|
+
|-------|--------|-----------------|
|
|
166
|
+
| `url` | `location.href` at event time | Yes — query params may have tokens (`?token=xxx`) |
|
|
167
|
+
| `message`, `stack` | Error object | Yes — error text may include user data |
|
|
168
|
+
| `machineId`, `machineState` | Your machine config | No (developer-defined strings) |
|
|
169
|
+
| `sessionId` | Random generated ID | No (not tied to user identity) |
|
|
170
|
+
| `name`, `value`, `metadata` | Your `metric()` / `counter()` calls | Depends on what you pass |
|
|
171
|
+
|
|
172
|
+
> **Tip**: Use a `filter` to strip sensitive data before it's sent:
|
|
173
|
+
> ```typescript
|
|
174
|
+
> observe({
|
|
175
|
+
> endpoint: '/api/metrics',
|
|
176
|
+
> filter: (event) => {
|
|
177
|
+
> if ('url' in event) {
|
|
178
|
+
> (event as any).url = event.url.split('?')[0]; // strip query params
|
|
179
|
+
> }
|
|
180
|
+
> return true;
|
|
181
|
+
> },
|
|
182
|
+
> });
|
|
183
|
+
> ```
|
|
184
|
+
|
|
185
|
+
## Receiving Events (Backend)
|
|
186
|
+
|
|
187
|
+
svoose is a **client-side collector** — it doesn't include a backend. Your server just needs one POST endpoint that accepts a JSON array.
|
|
188
|
+
|
|
189
|
+
### SvelteKit
|
|
190
|
+
|
|
191
|
+
> Planned for v0.3.0 — not yet implemented. The API below is a preview of the planned integration.
|
|
192
|
+
|
|
193
|
+
```typescript
|
|
194
|
+
// src/routes/api/metrics/+server.ts
|
|
195
|
+
import { json } from '@sveltejs/kit';
|
|
196
|
+
import type { RequestHandler } from './$types';
|
|
197
|
+
|
|
198
|
+
export const POST: RequestHandler = async ({ request }) => {
|
|
199
|
+
const events = await request.json();
|
|
200
|
+
|
|
201
|
+
// Option 1: Log to stdout (pipe to your log aggregator)
|
|
202
|
+
console.log(JSON.stringify(events));
|
|
203
|
+
|
|
204
|
+
// Option 2: Insert into database
|
|
205
|
+
// await db.insert('events', events);
|
|
206
|
+
|
|
207
|
+
return json({ ok: true }, { status: 200 });
|
|
208
|
+
};
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
### Express
|
|
212
|
+
|
|
213
|
+
```typescript
|
|
214
|
+
import express from 'express';
|
|
215
|
+
const app = express();
|
|
216
|
+
app.use(express.json());
|
|
217
|
+
|
|
218
|
+
app.post('/api/metrics', (req, res) => {
|
|
219
|
+
const events = req.body; // ObserveEvent[]
|
|
220
|
+
|
|
221
|
+
// Store, forward, or log — up to you
|
|
222
|
+
for (const event of events) {
|
|
223
|
+
if (event.type === 'error') {
|
|
224
|
+
console.error(`[${event.machineState ?? 'unknown'}] ${event.message}`);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
res.sendStatus(204);
|
|
229
|
+
});
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
### No backend? No problem
|
|
233
|
+
|
|
234
|
+
```typescript
|
|
235
|
+
// Development — just log to console
|
|
236
|
+
observe({ transport: createConsoleTransport({ pretty: true }) });
|
|
237
|
+
|
|
238
|
+
// Production without backend — silent noop
|
|
239
|
+
observe({ transport: { send: () => {} } });
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
### Production recommendations
|
|
243
|
+
|
|
244
|
+
Use the built-in production preset for sensible defaults:
|
|
245
|
+
|
|
246
|
+
```typescript
|
|
247
|
+
import { observe, productionDefaults } from 'svoose';
|
|
248
|
+
|
|
249
|
+
observe({ ...productionDefaults, endpoint: '/api/metrics' });
|
|
250
|
+
// Includes: batchSize 50, flushInterval 10s, sampling, sessions
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
Or configure manually for production traffic (1000+ users):
|
|
254
|
+
|
|
255
|
+
```typescript
|
|
256
|
+
observe({
|
|
257
|
+
endpoint: '/api/metrics',
|
|
258
|
+
|
|
259
|
+
// Larger batches = fewer HTTP requests
|
|
260
|
+
batchSize: 50,
|
|
261
|
+
flushInterval: 10000,
|
|
262
|
+
|
|
263
|
+
// Sample to reduce volume
|
|
264
|
+
sampling: {
|
|
265
|
+
errors: 1.0, // never skip errors
|
|
266
|
+
vitals: 0.5, // 50% is enough for p75/p95 stats
|
|
267
|
+
custom: 0.5,
|
|
268
|
+
transitions: 0.1, // transitions are high-volume
|
|
269
|
+
},
|
|
270
|
+
|
|
271
|
+
// Handle transport failures
|
|
272
|
+
onError: (err) => console.error('svoose transport failed:', err),
|
|
273
|
+
|
|
274
|
+
// Track sessions
|
|
275
|
+
session: true,
|
|
276
|
+
});
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
**Volume math**: 1000 users with default settings (`batchSize: 10`, `flushInterval: 5s`) = ~200 req/s to your endpoint. With `batchSize: 50` + `flushInterval: 10s` + `sampling: 0.5` = ~10 req/s.
|
|
280
|
+
|
|
281
|
+
## API
|
|
282
|
+
|
|
283
|
+
### `observe(options?)`
|
|
284
|
+
|
|
285
|
+
Start collecting Web Vitals and errors.
|
|
286
|
+
|
|
287
|
+
```typescript
|
|
288
|
+
const cleanup = observe({
|
|
289
|
+
// Where to send data
|
|
290
|
+
endpoint: '/api/metrics',
|
|
291
|
+
|
|
292
|
+
// Or use custom transport (overrides endpoint)
|
|
293
|
+
transport: myTransport,
|
|
294
|
+
|
|
295
|
+
// What to collect
|
|
296
|
+
vitals: true, // or ['CLS', 'LCP', 'INP']
|
|
297
|
+
errors: true,
|
|
298
|
+
|
|
299
|
+
// Batching
|
|
300
|
+
batchSize: 10,
|
|
301
|
+
flushInterval: 5000,
|
|
302
|
+
|
|
303
|
+
// Sampling — number or per-event-type config
|
|
304
|
+
sampling: {
|
|
305
|
+
vitals: 0.1, // 10%
|
|
306
|
+
errors: 1.0, // 100%
|
|
307
|
+
custom: 0.5, // 50%
|
|
308
|
+
transitions: 0.0, // disabled
|
|
309
|
+
},
|
|
310
|
+
|
|
311
|
+
// Sessions
|
|
312
|
+
session: true, // or { timeout: 30 * 60 * 1000, storage: 'sessionStorage' }
|
|
313
|
+
|
|
314
|
+
// Error callback — handle transport failures
|
|
315
|
+
onError: (err) => console.error('Transport failed:', err),
|
|
316
|
+
|
|
317
|
+
// Filter events before sending
|
|
318
|
+
filter: (event) => !(event.type === 'vital' && event.name === 'TTFB'),
|
|
319
|
+
|
|
320
|
+
// Debug
|
|
321
|
+
debug: false,
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
// Stop observing
|
|
325
|
+
cleanup();
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
> **Note**: If neither `endpoint` nor `transport` is provided, defaults to `endpoint: '/api/metrics'`.
|
|
329
|
+
> The default transport is hybrid (fetch + beacon on page close) for reliable delivery.
|
|
330
|
+
|
|
331
|
+
#### Sampling
|
|
332
|
+
|
|
333
|
+
Control what percentage of events are sent:
|
|
334
|
+
|
|
335
|
+
```typescript
|
|
336
|
+
// Simple: same rate for all events
|
|
337
|
+
observe({ sampling: 0.1 }); // 10% of everything
|
|
338
|
+
|
|
339
|
+
// Per-event-type (recommended)
|
|
340
|
+
observe({
|
|
341
|
+
sampling: {
|
|
342
|
+
vitals: 0.1, // 10% — sufficient for accurate statistics
|
|
343
|
+
errors: 1.0, // 100% — capture all errors
|
|
344
|
+
custom: 0.5, // 50% of custom metrics
|
|
345
|
+
transitions: 0.0, // disabled
|
|
346
|
+
},
|
|
347
|
+
});
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
#### Sessions
|
|
351
|
+
|
|
352
|
+
Automatic session tracking with configurable timeout:
|
|
353
|
+
|
|
354
|
+
```typescript
|
|
355
|
+
// Enable with defaults (30 min timeout, sessionStorage)
|
|
356
|
+
observe({ session: true });
|
|
357
|
+
|
|
358
|
+
// Or custom config
|
|
359
|
+
observe({
|
|
360
|
+
session: {
|
|
361
|
+
timeout: 60 * 60 * 1000, // 1 hour
|
|
362
|
+
storage: 'localStorage', // 'sessionStorage' | 'localStorage' | 'memory'
|
|
363
|
+
},
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
// All events now include sessionId:
|
|
367
|
+
// { type: 'vital', name: 'LCP', value: 1234, sessionId: '1706123456789-abc123def' }
|
|
368
|
+
```
|
|
369
|
+
|
|
370
|
+
**Storage options:**
|
|
371
|
+
- `sessionStorage` (default) — session per browser tab
|
|
372
|
+
- `localStorage` — session persists across tabs
|
|
373
|
+
- `memory` — no persistence, new session on page reload
|
|
374
|
+
|
|
375
|
+
#### Web Vitals
|
|
376
|
+
|
|
377
|
+
svoose collects all Core Web Vitals using the standard [web-vitals](https://github.com/GoogleChrome/web-vitals) algorithm (own implementation, no external dependency):
|
|
378
|
+
|
|
379
|
+
| Metric | What it measures | When reported |
|
|
380
|
+
|--------|------------------|---------------|
|
|
381
|
+
| **CLS** | Visual stability (layout shifts) | On page hide |
|
|
382
|
+
| **LCP** | Loading performance | On user input or page hide |
|
|
383
|
+
| **INP** | Responsiveness (max interaction) | On page hide |
|
|
384
|
+
| **FCP** | First content painted | Once |
|
|
385
|
+
| **TTFB** | Server response time | Once |
|
|
386
|
+
| **FID** | First input delay (deprecated) | Once |
|
|
387
|
+
|
|
388
|
+
```typescript
|
|
389
|
+
// All vitals
|
|
390
|
+
observe({ vitals: true });
|
|
391
|
+
|
|
392
|
+
// Select specific vitals
|
|
393
|
+
observe({ vitals: ['CLS', 'LCP', 'INP'] });
|
|
394
|
+
```
|
|
395
|
+
|
|
396
|
+
> CLS, LCP, and INP report once per page lifecycle (matches Chrome DevTools and Google Search Console behavior).
|
|
397
|
+
|
|
398
|
+
#### Custom Metrics
|
|
399
|
+
|
|
400
|
+
Track custom events for analytics:
|
|
401
|
+
|
|
402
|
+
```typescript
|
|
403
|
+
import { metric, counter, gauge, histogram } from 'svoose';
|
|
404
|
+
|
|
405
|
+
// Basic event
|
|
406
|
+
metric('checkout_started', { step: 1, cartTotal: 99.99 });
|
|
407
|
+
|
|
408
|
+
// Counter — increments (default value: 1)
|
|
409
|
+
counter('page_views');
|
|
410
|
+
counter('items_purchased', 3, { category: 'electronics' });
|
|
411
|
+
|
|
412
|
+
// Gauge — point-in-time values
|
|
413
|
+
gauge('active_users', 42);
|
|
414
|
+
gauge('memory_usage_mb', 256, { heap: 'old' });
|
|
415
|
+
|
|
416
|
+
// Histogram — distribution values
|
|
417
|
+
histogram('response_time_ms', 123);
|
|
418
|
+
histogram('payload_size', 4096, { route: '/api/data' });
|
|
419
|
+
```
|
|
420
|
+
|
|
421
|
+
**Buffer behavior**: If `metric()` / `counter()` / `gauge()` / `histogram()` is called before `observe()`, events are buffered (max 100). They're automatically flushed when `observe()` initializes.
|
|
422
|
+
|
|
423
|
+
##### Typed Metrics
|
|
424
|
+
|
|
425
|
+
Full TypeScript autocomplete for metric names and metadata shapes:
|
|
426
|
+
|
|
427
|
+
```typescript
|
|
428
|
+
import { createTypedMetric } from 'svoose';
|
|
429
|
+
|
|
430
|
+
const track = createTypedMetric<{
|
|
431
|
+
checkout_started: { step: number; cartTotal: number };
|
|
432
|
+
button_clicked: { id: string };
|
|
433
|
+
}>();
|
|
434
|
+
|
|
435
|
+
track('checkout_started', { step: 1, cartTotal: 99.99 }); // autocomplete
|
|
436
|
+
track('button_clicked', { id: 'submit' }); // autocomplete
|
|
437
|
+
track('unknown_event', {}); // TypeScript error
|
|
438
|
+
```
|
|
439
|
+
|
|
440
|
+
### `createMachine(config)`
|
|
441
|
+
|
|
442
|
+
Create a state machine.
|
|
443
|
+
|
|
444
|
+
```typescript
|
|
445
|
+
const machine = createMachine({
|
|
446
|
+
id: 'toggle',
|
|
447
|
+
initial: 'off',
|
|
448
|
+
context: { count: 0 },
|
|
449
|
+
states: {
|
|
450
|
+
off: {
|
|
451
|
+
on: { TOGGLE: 'on' },
|
|
452
|
+
},
|
|
453
|
+
on: {
|
|
454
|
+
entry: (ctx) => ({ count: ctx.count + 1 }),
|
|
455
|
+
on: { TOGGLE: 'off' },
|
|
456
|
+
},
|
|
457
|
+
},
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
machine.state; // 'off'
|
|
461
|
+
machine.context; // { count: 0 }
|
|
462
|
+
|
|
463
|
+
// Note: context is shallow-cloned from your initial object.
|
|
464
|
+
// Nested objects/arrays are shared references — same as XState.
|
|
465
|
+
// If you need a deep clone, pass structuredClone(ctx) yourself.
|
|
466
|
+
|
|
467
|
+
machine.matches('off'); // true
|
|
468
|
+
machine.matchesAny('on', 'off'); // true
|
|
469
|
+
|
|
470
|
+
machine.can('TOGGLE'); // true
|
|
471
|
+
machine.can({ type: 'SET', value: 42 }); // full event for payload-dependent guards
|
|
472
|
+
|
|
473
|
+
machine.send('TOGGLE');
|
|
474
|
+
machine.send({ type: 'SET', value: 42 });
|
|
475
|
+
|
|
476
|
+
machine.destroy();
|
|
477
|
+
```
|
|
478
|
+
|
|
479
|
+
#### Guards & Actions
|
|
480
|
+
|
|
481
|
+
```typescript
|
|
482
|
+
const counter = createMachine({
|
|
483
|
+
id: 'counter',
|
|
484
|
+
initial: 'active',
|
|
485
|
+
context: { count: 0 },
|
|
486
|
+
states: {
|
|
487
|
+
active: {
|
|
488
|
+
on: {
|
|
489
|
+
INCREMENT: {
|
|
490
|
+
target: 'active',
|
|
491
|
+
guard: (ctx) => ctx.count < 10,
|
|
492
|
+
action: (ctx) => ({ count: ctx.count + 1 }),
|
|
493
|
+
},
|
|
494
|
+
DECREMENT: {
|
|
495
|
+
target: 'active',
|
|
496
|
+
guard: (ctx) => ctx.count > 0,
|
|
497
|
+
action: (ctx) => ({ count: ctx.count - 1 }),
|
|
498
|
+
},
|
|
499
|
+
},
|
|
500
|
+
},
|
|
501
|
+
},
|
|
502
|
+
});
|
|
503
|
+
```
|
|
504
|
+
|
|
505
|
+
#### Entry & Exit Actions
|
|
506
|
+
|
|
507
|
+
```typescript
|
|
508
|
+
const wizard = createMachine({
|
|
509
|
+
id: 'wizard',
|
|
510
|
+
initial: 'step1',
|
|
511
|
+
context: { data: {} },
|
|
512
|
+
states: {
|
|
513
|
+
step1: {
|
|
514
|
+
entry: (ctx) => console.log('Entered step 1'),
|
|
515
|
+
exit: (ctx) => console.log('Leaving step 1'),
|
|
516
|
+
on: { NEXT: 'step2' },
|
|
517
|
+
},
|
|
518
|
+
step2: {
|
|
519
|
+
on: { BACK: 'step1', SUBMIT: 'complete' },
|
|
520
|
+
},
|
|
521
|
+
complete: {
|
|
522
|
+
entry: (ctx) => console.log('Done!'),
|
|
523
|
+
},
|
|
524
|
+
},
|
|
525
|
+
});
|
|
526
|
+
```
|
|
527
|
+
|
|
528
|
+
#### Observability Integration
|
|
529
|
+
|
|
530
|
+
Machines automatically integrate with `observe()`:
|
|
531
|
+
|
|
532
|
+
```typescript
|
|
533
|
+
observe({ errors: true });
|
|
534
|
+
|
|
535
|
+
// Simple
|
|
536
|
+
const auth = createMachine({ id: 'auth', observe: true, /* ... */ });
|
|
537
|
+
|
|
538
|
+
// Or detailed config
|
|
539
|
+
const auth = createMachine({
|
|
540
|
+
id: 'auth',
|
|
541
|
+
observe: { transitions: true, context: true },
|
|
542
|
+
// ...
|
|
543
|
+
});
|
|
544
|
+
|
|
545
|
+
// When an error occurs, it includes all active machines:
|
|
546
|
+
// { machineId: 'auth', machineState: 'loading', machines: [{ id: 'auth', state: 'loading' }] }
|
|
547
|
+
```
|
|
548
|
+
|
|
549
|
+
### Transports
|
|
550
|
+
|
|
551
|
+
#### Retry & Timeout
|
|
552
|
+
|
|
553
|
+
Add retry logic with configurable backoff to any fetch-based transport:
|
|
554
|
+
|
|
555
|
+
```typescript
|
|
556
|
+
import { createFetchTransport } from 'svoose';
|
|
557
|
+
|
|
558
|
+
const transport = createFetchTransport('/api/metrics', {
|
|
559
|
+
retry: {
|
|
560
|
+
attempts: 3,
|
|
561
|
+
backoff: 'exponential', // 'fixed' | 'linear' | 'exponential'
|
|
562
|
+
initialDelay: 1000, // 1s → 2s → 4s
|
|
563
|
+
maxDelay: 30000,
|
|
564
|
+
jitter: true, // ±10% randomization
|
|
565
|
+
},
|
|
566
|
+
timeout: 10000, // 10s per request
|
|
567
|
+
});
|
|
568
|
+
```
|
|
569
|
+
|
|
570
|
+
Works with hybrid transport too — retry applies to fetch only, beacon never retries:
|
|
571
|
+
|
|
572
|
+
```typescript
|
|
573
|
+
import { createHybridTransport } from 'svoose';
|
|
574
|
+
|
|
575
|
+
observe({
|
|
576
|
+
transport: createHybridTransport('/api/metrics', {
|
|
577
|
+
retry: { attempts: 3, backoff: 'exponential' },
|
|
578
|
+
timeout: 10000,
|
|
579
|
+
}),
|
|
580
|
+
});
|
|
581
|
+
```
|
|
582
|
+
|
|
583
|
+
`withRetry()` is also available as a standalone utility for custom transports:
|
|
584
|
+
|
|
585
|
+
```typescript
|
|
586
|
+
import { withRetry } from 'svoose';
|
|
587
|
+
|
|
588
|
+
await withRetry(
|
|
589
|
+
(signal) => fetch('/api/metrics', { method: 'POST', body, signal }),
|
|
590
|
+
{ attempts: 3, backoff: 'exponential' },
|
|
591
|
+
{ timeout: 5000 }
|
|
592
|
+
);
|
|
593
|
+
```
|
|
594
|
+
|
|
595
|
+
#### Fetch Transport (default)
|
|
596
|
+
|
|
597
|
+
```typescript
|
|
598
|
+
import { observe, createFetchTransport } from 'svoose';
|
|
599
|
+
|
|
600
|
+
const transport = createFetchTransport('/api/metrics', {
|
|
601
|
+
headers: { 'Authorization': 'Bearer xxx' },
|
|
602
|
+
onError: (err) => console.error(err),
|
|
603
|
+
});
|
|
604
|
+
observe({ transport });
|
|
605
|
+
```
|
|
606
|
+
|
|
607
|
+
#### Console Transport (development)
|
|
608
|
+
|
|
609
|
+
```typescript
|
|
610
|
+
import { observe, createConsoleTransport } from 'svoose';
|
|
611
|
+
|
|
612
|
+
observe({ transport: createConsoleTransport({ pretty: true }) });
|
|
613
|
+
```
|
|
614
|
+
|
|
615
|
+
#### Beacon Transport
|
|
616
|
+
|
|
617
|
+
Guaranteed delivery on page close via `navigator.sendBeacon`:
|
|
618
|
+
|
|
619
|
+
```typescript
|
|
620
|
+
import { observe, createBeaconTransport } from 'svoose';
|
|
621
|
+
|
|
622
|
+
observe({
|
|
623
|
+
transport: createBeaconTransport('/api/metrics', {
|
|
624
|
+
maxPayloadSize: 60000, // auto-chunks if exceeded (default: 60KB)
|
|
625
|
+
}),
|
|
626
|
+
});
|
|
627
|
+
```
|
|
628
|
+
|
|
629
|
+
#### Hybrid Transport (recommended for production)
|
|
630
|
+
|
|
631
|
+
Uses fetch normally, switches to beacon on page close:
|
|
632
|
+
|
|
633
|
+
```typescript
|
|
634
|
+
import { observe, createHybridTransport } from 'svoose';
|
|
635
|
+
|
|
636
|
+
const transport = createHybridTransport('/api/metrics', {
|
|
637
|
+
default: 'fetch',
|
|
638
|
+
onUnload: 'beacon',
|
|
639
|
+
headers: { 'Authorization': 'Bearer xxx' },
|
|
640
|
+
});
|
|
641
|
+
|
|
642
|
+
observe({ transport });
|
|
643
|
+
|
|
644
|
+
// Cleanup when done (removes lifecycle listeners)
|
|
645
|
+
transport.destroy();
|
|
646
|
+
```
|
|
647
|
+
|
|
648
|
+
#### Custom Transport
|
|
649
|
+
|
|
650
|
+
```typescript
|
|
651
|
+
// Forward to any service
|
|
652
|
+
const myTransport = {
|
|
653
|
+
async send(events) {
|
|
654
|
+
await myApi.track(events);
|
|
655
|
+
},
|
|
656
|
+
};
|
|
657
|
+
observe({ transport: myTransport });
|
|
658
|
+
```
|
|
659
|
+
|
|
660
|
+
#### Dev vs Prod Pattern
|
|
661
|
+
|
|
662
|
+
```typescript
|
|
663
|
+
const isDev = import.meta.env.DEV;
|
|
664
|
+
observe({
|
|
665
|
+
transport: isDev
|
|
666
|
+
? createConsoleTransport({ pretty: true })
|
|
667
|
+
: createHybridTransport('/api/metrics'),
|
|
668
|
+
});
|
|
669
|
+
```
|
|
670
|
+
|
|
671
|
+
## Svelte 5 Usage
|
|
672
|
+
|
|
673
|
+
### Reactive State Machines
|
|
674
|
+
|
|
675
|
+
Use `useMachine()` from `svoose/svelte` for automatic reactivity:
|
|
676
|
+
|
|
677
|
+
```svelte
|
|
678
|
+
<script lang="ts">
|
|
679
|
+
import { useMachine } from 'svoose/svelte';
|
|
680
|
+
|
|
681
|
+
const toggle = useMachine({
|
|
682
|
+
id: 'toggle',
|
|
683
|
+
initial: 'off',
|
|
684
|
+
states: {
|
|
685
|
+
off: { on: { TOGGLE: 'on' } },
|
|
686
|
+
on: { on: { TOGGLE: 'off' } },
|
|
687
|
+
},
|
|
688
|
+
});
|
|
689
|
+
</script>
|
|
690
|
+
|
|
691
|
+
<button onclick={() => toggle.send('TOGGLE')}>
|
|
692
|
+
{toggle.state}
|
|
693
|
+
</button>
|
|
694
|
+
|
|
695
|
+
{#if toggle.matches('on')}
|
|
696
|
+
<p>Light is on!</p>
|
|
697
|
+
{/if}
|
|
698
|
+
```
|
|
699
|
+
|
|
700
|
+
### With Observability
|
|
701
|
+
|
|
702
|
+
```svelte
|
|
703
|
+
<script lang="ts">
|
|
704
|
+
import { observe } from 'svoose';
|
|
705
|
+
import { useMachine } from 'svoose/svelte';
|
|
706
|
+
import { onMount, onDestroy } from 'svelte';
|
|
707
|
+
|
|
708
|
+
let cleanup: (() => void) | null = null;
|
|
709
|
+
|
|
710
|
+
onMount(() => {
|
|
711
|
+
cleanup = observe({ endpoint: '/api/metrics' });
|
|
712
|
+
});
|
|
713
|
+
|
|
714
|
+
onDestroy(() => cleanup?.());
|
|
715
|
+
|
|
716
|
+
const auth = useMachine({
|
|
717
|
+
id: 'auth',
|
|
718
|
+
initial: 'idle',
|
|
719
|
+
context: { user: null },
|
|
720
|
+
observe: true,
|
|
721
|
+
states: {
|
|
722
|
+
idle: { on: { LOGIN: 'loading' } },
|
|
723
|
+
loading: {
|
|
724
|
+
on: {
|
|
725
|
+
SUCCESS: {
|
|
726
|
+
target: 'authenticated',
|
|
727
|
+
action: (ctx, e) => ({ user: e.user }),
|
|
728
|
+
},
|
|
729
|
+
ERROR: 'idle',
|
|
730
|
+
},
|
|
731
|
+
},
|
|
732
|
+
authenticated: { on: { LOGOUT: 'idle' } },
|
|
733
|
+
},
|
|
734
|
+
});
|
|
735
|
+
</script>
|
|
736
|
+
|
|
737
|
+
<p>Status: {auth.state}</p>
|
|
738
|
+
<p>User: {auth.context.user?.name ?? 'Not logged in'}</p>
|
|
739
|
+
```
|
|
740
|
+
|
|
741
|
+
### Non-Reactive Usage
|
|
742
|
+
|
|
743
|
+
For non-reactive scenarios (outside components, vanilla JS), use `createMachine()` directly.
|
|
744
|
+
|
|
745
|
+
## TypeScript
|
|
746
|
+
|
|
747
|
+
Full TypeScript support with inference:
|
|
748
|
+
|
|
749
|
+
```typescript
|
|
750
|
+
type AuthEvent =
|
|
751
|
+
| { type: 'LOGIN'; email: string }
|
|
752
|
+
| { type: 'SUCCESS'; user: User }
|
|
753
|
+
| { type: 'ERROR'; message: string }
|
|
754
|
+
| { type: 'LOGOUT' };
|
|
755
|
+
|
|
756
|
+
const auth = createMachine<
|
|
757
|
+
{ user: User | null; error: string | null },
|
|
758
|
+
'idle' | 'loading' | 'authenticated',
|
|
759
|
+
AuthEvent
|
|
760
|
+
>({
|
|
761
|
+
id: 'auth',
|
|
762
|
+
initial: 'idle',
|
|
763
|
+
context: { user: null, error: null },
|
|
764
|
+
states: {
|
|
765
|
+
idle: {
|
|
766
|
+
on: { LOGIN: 'loading' },
|
|
767
|
+
},
|
|
768
|
+
loading: {
|
|
769
|
+
on: {
|
|
770
|
+
SUCCESS: {
|
|
771
|
+
target: 'authenticated',
|
|
772
|
+
action: (ctx, event) => ({ user: event.user }),
|
|
773
|
+
},
|
|
774
|
+
},
|
|
775
|
+
},
|
|
776
|
+
authenticated: {
|
|
777
|
+
on: { LOGOUT: 'idle' },
|
|
778
|
+
},
|
|
779
|
+
},
|
|
780
|
+
});
|
|
781
|
+
|
|
782
|
+
auth.matches('idle'); // type-checked
|
|
783
|
+
auth.matches('invalid'); // TypeScript error
|
|
784
|
+
auth.send('LOGOUT'); // type-checked
|
|
785
|
+
auth.send('INVALID'); // TypeScript error
|
|
786
|
+
```
|
|
787
|
+
|
|
788
|
+
## Bundle Size
|
|
789
|
+
|
|
790
|
+
Tree-shakeable — pay only for what you use:
|
|
791
|
+
|
|
792
|
+
| Import | Size (gzip) |
|
|
793
|
+
|--------|-------------|
|
|
794
|
+
| `observe()` + vitals + errors + metrics | ~5.1 KB |
|
|
795
|
+
| `createMachine()` only | ~0.95 KB |
|
|
796
|
+
| Full bundle | ~6.7 KB |
|
|
797
|
+
|
|
798
|
+
> Compare: Sentry ~20KB, PostHog ~40KB.
|
|
799
|
+
|
|
800
|
+
## When to use something else
|
|
801
|
+
|
|
802
|
+
- **Session replay, alerting, team workflows** — use [Sentry](https://sentry.io) or [PostHog](https://posthog.com)
|
|
803
|
+
- **Complex state machines** (parallel states, invoke, spawn) — use [XState](https://xstate.js.org)
|
|
804
|
+
- **Full analytics platform** (funnels, cohorts, A/B tests) — use PostHog or Mixpanel
|
|
805
|
+
|
|
806
|
+
svoose is best for: lightweight self-hosted observability where you control the data and want minimal bundle overhead.
|
|
807
|
+
|
|
808
|
+
## Roadmap
|
|
809
|
+
|
|
810
|
+
- **v0.1.3–v0.1.10** — Done (sampling, sessions, custom metrics, beacon/hybrid transport, API cleanup, retry logic)
|
|
811
|
+
|
|
812
|
+
- **v0.1.11** — Privacy Utilities (planned)
|
|
813
|
+
- **v0.2.0** — Production-Ready: User ID, Offline, flush API, Rate Limiter (planned)
|
|
814
|
+
- **v0.3.0** — SvelteKit Integration (planned)
|
|
815
|
+
- **v1.0.0** — Stable Release
|
|
816
|
+
|
|
817
|
+
> FSM is a lightweight bonus feature, not an XState competitor. For complex state machines, use XState.
|
|
818
|
+
|
|
819
|
+
See [ROADMAP.md](./ROADMAP.md) for detailed plans.
|
|
820
|
+
|
|
821
|
+
## License
|
|
822
|
+
|
|
823
|
+
MIT
|