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.
Files changed (84) hide show
  1. package/README.md +823 -619
  2. package/dist/index.d.ts +4 -3
  3. package/dist/index.d.ts.map +1 -1
  4. package/dist/index.js +1 -1
  5. package/dist/index.js.map +3 -3
  6. package/dist/machine/machine.svelte.d.ts +6 -3
  7. package/dist/machine/machine.svelte.d.ts.map +1 -1
  8. package/dist/machine/machine.svelte.js +1 -1
  9. package/dist/machine/machine.svelte.js.map +3 -3
  10. package/dist/machine/types.d.ts +2 -2
  11. package/dist/machine/types.d.ts.map +1 -1
  12. package/dist/metrics/index.d.ts +1 -1
  13. package/dist/metrics/index.d.ts.map +1 -1
  14. package/dist/metrics/index.js +1 -1
  15. package/dist/metrics/index.js.map +3 -3
  16. package/dist/metrics/metric.d.ts +1 -15
  17. package/dist/metrics/metric.d.ts.map +1 -1
  18. package/dist/metrics/metric.js +1 -1
  19. package/dist/metrics/metric.js.map +3 -3
  20. package/dist/metrics/typed.d.ts +1 -1
  21. package/dist/metrics/typed.d.ts.map +1 -1
  22. package/dist/metrics/typed.js.map +3 -3
  23. package/dist/observe/errors.d.ts.map +1 -1
  24. package/dist/observe/errors.js +1 -1
  25. package/dist/observe/errors.js.map +3 -3
  26. package/dist/observe/index.d.ts +4 -1
  27. package/dist/observe/index.d.ts.map +1 -1
  28. package/dist/observe/index.js +1 -1
  29. package/dist/observe/index.js.map +3 -3
  30. package/dist/observe/observe.svelte.d.ts +13 -13
  31. package/dist/observe/observe.svelte.d.ts.map +1 -1
  32. package/dist/observe/observe.svelte.js +1 -1
  33. package/dist/observe/observe.svelte.js.map +3 -3
  34. package/dist/observe/presets.d.ts +19 -0
  35. package/dist/observe/presets.d.ts.map +1 -0
  36. package/dist/observe/presets.js +2 -0
  37. package/dist/observe/presets.js.map +7 -0
  38. package/dist/observe/sampling.d.ts.map +1 -1
  39. package/dist/observe/sampling.js +1 -1
  40. package/dist/observe/sampling.js.map +2 -2
  41. package/dist/observe/session.d.ts +1 -1
  42. package/dist/observe/session.d.ts.map +1 -1
  43. package/dist/observe/session.js +1 -1
  44. package/dist/observe/session.js.map +3 -3
  45. package/dist/observe/vitals.d.ts.map +1 -1
  46. package/dist/observe/vitals.js +1 -1
  47. package/dist/observe/vitals.js.map +2 -2
  48. package/dist/svelte/index.svelte.d.ts +2 -2
  49. package/dist/svelte/index.svelte.d.ts.map +1 -1
  50. package/dist/svelte/index.svelte.js +1 -1
  51. package/dist/svelte/index.svelte.js.map +3 -3
  52. package/dist/transport/fetch.d.ts +3 -3
  53. package/dist/transport/fetch.d.ts.map +1 -1
  54. package/dist/transport/fetch.js +1 -1
  55. package/dist/transport/fetch.js.map +3 -3
  56. package/dist/transport/hybrid.d.ts +3 -0
  57. package/dist/transport/hybrid.d.ts.map +1 -1
  58. package/dist/transport/hybrid.js +1 -1
  59. package/dist/transport/hybrid.js.map +3 -3
  60. package/dist/transport/index.d.ts +2 -1
  61. package/dist/transport/index.d.ts.map +1 -1
  62. package/dist/transport/index.js +1 -1
  63. package/dist/transport/index.js.map +3 -3
  64. package/dist/transport/retry.d.ts +25 -0
  65. package/dist/transport/retry.d.ts.map +1 -0
  66. package/dist/transport/retry.js +2 -0
  67. package/dist/transport/retry.js.map +7 -0
  68. package/dist/transport/transport.d.ts +1 -1
  69. package/dist/transport/transport.d.ts.map +1 -1
  70. package/dist/types/index.d.ts +76 -8
  71. package/dist/types/index.d.ts.map +1 -1
  72. package/package.json +72 -72
  73. package/dist/upgrade/after.d.ts +0 -28
  74. package/dist/upgrade/after.d.ts.map +0 -1
  75. package/dist/upgrade/history.d.ts +0 -36
  76. package/dist/upgrade/history.d.ts.map +0 -1
  77. package/dist/upgrade/index.d.ts +0 -25
  78. package/dist/upgrade/index.d.ts.map +0 -1
  79. package/dist/upgrade/invoke.d.ts +0 -39
  80. package/dist/upgrade/invoke.d.ts.map +0 -1
  81. package/dist/upgrade/parallel.d.ts +0 -36
  82. package/dist/upgrade/parallel.d.ts.map +0 -1
  83. package/dist/upgrade/spawn.d.ts +0 -35
  84. 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. **~5.3KB gzipped** (core ~3.6KB).
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()` (v0.1.6+)
12
- - **Beacon Transport** — reliable delivery on page close with auto-chunking (v0.1.8+)
13
- - **Session Tracking** — automatic sessionId with timeout (v0.1.5+)
14
- - **Sampling** — per-event-type rate limiting (v0.1.3+)
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
- ## Quick Start
26
-
27
- ```typescript
28
- import { observe, createMachine } from 'svoose';
29
-
30
- // Start collecting metrics
31
- observe({ endpoint: '/api/metrics' });
32
-
33
- // Create a state machine
34
- const auth = createMachine({
35
- id: 'auth',
36
- initial: 'idle',
37
- context: { user: null },
38
- states: {
39
- idle: { on: { LOGIN: 'loading' } },
40
- loading: {
41
- on: {
42
- SUCCESS: {
43
- target: 'authenticated',
44
- action: (ctx, e) => ({ user: e.user }),
45
- },
46
- ERROR: 'idle',
47
- },
48
- },
49
- authenticated: { on: { LOGOUT: 'idle' } },
50
- },
51
- observe: true, // Track transitions
52
- });
53
-
54
- // Use it
55
- auth.send('LOGIN');
56
- auth.state; // 'loading'
57
- auth.context; // { user: null }
58
- ```
59
-
60
- ## API
61
-
62
- ### `observe(options?)`
63
-
64
- Start collecting Web Vitals and errors.
65
-
66
- ```typescript
67
- const cleanup = observe({
68
- // Where to send data (Option 1: endpoint)
69
- endpoint: '/api/metrics',
70
-
71
- // Or use custom transport (Option 2: transport)
72
- // NOTE: endpoint and transport are mutually exclusive
73
- // If transport is provided, endpoint is ignored
74
- transport: myTransport,
75
-
76
- // What to collect
77
- vitals: true, // or ['CLS', 'LCP', 'INP']
78
- errors: true,
79
-
80
- // Batching
81
- batchSize: 10,
82
- flushInterval: 5000,
83
-
84
- // Sampling (v0.1.3+) number or per-event-type config
85
- sampling: {
86
- vitals: 0.1, // 10% — sufficient for statistics
87
- errors: 1.0, // 100% — all errors matter
88
- custom: 0.5, // 50% of custom metrics
89
- transitions: 0.0, // disabled
90
- },
91
-
92
- // Sessions (v0.1.5+)
93
- session: true, // or { timeout: 30 * 60 * 1000, storage: 'sessionStorage' }
94
-
95
- // Debug
96
- debug: false,
97
- });
98
-
99
- // Stop observing
100
- cleanup();
101
- ```
102
-
103
- > **Note**: If neither `endpoint` nor `transport` is provided, defaults to `endpoint: '/api/metrics'`.
104
-
105
- #### Sampling (v0.1.3+)
106
-
107
- Control what percentage of events are sent to your backend:
108
-
109
- ```typescript
110
- // Simple: same rate for all events
111
- observe({
112
- endpoint: '/api/metrics',
113
- sampling: 0.1, // 10% of all events
114
- });
115
-
116
- // Per-event-type: recommended for production
117
- observe({
118
- endpoint: '/api/metrics',
119
- sampling: {
120
- vitals: 0.1, // 10% sufficient for accurate statistics
121
- errors: 1.0, // 100% capture all errors
122
- custom: 0.5, // 50% of custom metrics
123
- transitions: 0.0, // disabled — no state machine events
124
- },
125
- });
126
- ```
127
-
128
- > **Note**: `sampleRate` is deprecated. Use `sampling` instead.
129
-
130
- #### Sessions (v0.1.5+)
131
-
132
- Automatic session tracking with configurable timeout:
133
-
134
- ```typescript
135
- observe({
136
- endpoint: '/api/metrics',
137
-
138
- // Enable with defaults (30 min timeout, sessionStorage)
139
- session: true,
140
-
141
- // Or custom config
142
- session: {
143
- timeout: 60 * 60 * 1000, // 1 hour in milliseconds = new session after 1h inactivity
144
- storage: 'localStorage', // 'sessionStorage' | 'localStorage' | 'memory'
145
- },
146
- });
147
-
148
- // All events now include sessionId:
149
- // { type: 'vital', name: 'LCP', value: 1234, sessionId: '1706123456789-abc123def' }
150
- ```
151
-
152
- > **Note**: `timeout` is in **milliseconds**. Common values: `30 * 60 * 1000` (30 min), `60 * 60 * 1000` (1 hour).
153
-
154
- **Storage options:**
155
- - `sessionStorage` (default) session per browser tab
156
- - `localStorage` session persists across tabs
157
- - `memory` no persistence, new session on page reload
158
-
159
- **Features:**
160
- - Automatic session ID generation (timestamp + random)
161
- - Session expires after inactivity timeout (default: 30 min)
162
- - Graceful degradation in private mode
163
- - SSR safe
164
-
165
- #### Web Vitals (v0.1.5+)
166
-
167
- svoose collects all Core Web Vitals using the standard [web-vitals](https://github.com/GoogleChrome/web-vitals) algorithm:
168
-
169
- | Metric | What it measures | When reported |
170
- |--------|------------------|---------------|
171
- | **CLS** | Visual stability (layout shifts) | On page hide/visibility change |
172
- | **LCP** | Loading performance | On user input or visibility change |
173
- | **INP** | Responsiveness (max interaction) | On page hide/visibility change |
174
- | **FCP** | First content painted | Once |
175
- | **TTFB** | Server response time | Once |
176
- | **FID** | First input delay (deprecated) | Once |
177
-
178
- **Web Vitals Reporting (v0.1.5+)**:
179
-
180
- All vitals follow the [web-vitals](https://github.com/GoogleChrome/web-vitals) standard:
181
-
182
- **CLS (Cumulative Layout Shift)**:
183
- - Groups shifts into sessions (max 5s, max 1s gap)
184
- - Reports maximum session value on page hide
185
-
186
- **LCP (Largest Contentful Paint)**:
187
- - Tracks largest content element painted
188
- - Finalized on first user interaction (click/keydown) or visibility change
189
-
190
- **INP (Interaction to Next Paint)**:
191
- - Tracks maximum interaction duration
192
- - Only counts discrete events with `interactionId` (ignores scroll, etc.)
193
- - Reports on page hide
194
-
195
- ```typescript
196
- // All vitals report automatically on page lifecycle events
197
- observe({ vitals: true });
198
-
199
- // Select specific vitals
200
- observe({ vitals: ['CLS', 'LCP', 'INP'] });
201
- ```
202
-
203
- > **Note (v0.1.5 breaking change)**: CLS, LCP, and INP now report once per page lifecycle instead of on every update. This matches Chrome DevTools and Google Search Console behavior.
204
-
205
- #### Custom Metrics (v0.1.6+)
206
-
207
- Track custom events for analytics:
208
-
209
- ```typescript
210
- import { metric } from 'svoose';
211
-
212
- // Basic usage
213
- metric('checkout_started', { step: 1, cartTotal: 99.99 });
214
- metric('button_clicked', { id: 'submit-btn' });
215
- metric('feature_used', { name: 'dark_mode', enabled: true });
216
- ```
217
-
218
- ##### Metric Helpers (v0.1.7+)
219
-
220
- Typed helpers for common metric patterns:
221
-
222
- ```typescript
223
- import { counter, gauge, histogram } from 'svoose';
224
-
225
- // Counter — increments (default value: 1)
226
- counter('page_views');
227
- counter('items_purchased', 3, { category: 'electronics' });
228
-
229
- // Gauge — point-in-time values
230
- gauge('active_users', 42);
231
- gauge('memory_usage_mb', 256, { heap: 'old' });
232
-
233
- // Histogram — distribution values
234
- histogram('response_time_ms', 123);
235
- histogram('payload_size', 4096, { route: '/api/data' });
236
- ```
237
-
238
- All helpers emit events with top-level `metricKind` and `value` fields for easy backend processing.
239
-
240
- ##### Typed Metrics (v0.1.7+)
241
-
242
- Full TypeScript autocomplete for metric names and data shapes:
243
-
244
- ```typescript
245
- import { createTypedMetric } from 'svoose';
246
-
247
- const track = createTypedMetric<{
248
- checkout_started: { step: number; cartTotal: number };
249
- button_clicked: { id: string };
250
- }>();
251
-
252
- track('checkout_started', { step: 1, cartTotal: 99.99 }); // ✅ autocomplete
253
- track('button_clicked', { id: 'submit' }); // ✅
254
- track('unknown_event', {}); // ❌ TypeScript error
255
- ```
256
-
257
- Events are automatically batched with other metrics. You can control the sampling rate:
258
-
259
- ```typescript
260
- observe({
261
- endpoint: '/api/metrics',
262
- sampling: {
263
- custom: 0.5, // 50% of custom metrics
264
- vitals: 0.1,
265
- errors: 1.0,
266
- },
267
- });
268
- ```
269
-
270
- **Buffer behavior**: If `metric()` / `counter()` / `gauge()` / `histogram()` is called before `observe()`, events are buffered (max 100). They're automatically flushed when `observe()` initializes.
271
-
272
- ### `createMachine(config)`
273
-
274
- Create a state machine.
275
-
276
- ```typescript
277
- const machine = createMachine({
278
- id: 'toggle',
279
- initial: 'off',
280
- context: { count: 0 },
281
- states: {
282
- off: {
283
- on: { TOGGLE: 'on' },
284
- },
285
- on: {
286
- entry: (ctx) => ({ count: ctx.count + 1 }),
287
- on: { TOGGLE: 'off' },
288
- },
289
- },
290
- });
291
-
292
- // State & context (use useMachine() from svoose/svelte for reactivity)
293
- machine.state; // 'off'
294
- machine.context; // { count: 0 }
295
-
296
- // Check state
297
- machine.matches('off'); // true
298
- machine.matchesAny('on', 'off'); // true
299
-
300
- // Check if event is valid
301
- machine.can('TOGGLE'); // true
302
-
303
- // Send events
304
- machine.send('TOGGLE');
305
- machine.send({ type: 'SET', value: 42 });
306
-
307
- // Cleanup
308
- machine.destroy();
309
- ```
310
-
311
- ### Guards & Actions
312
-
313
- ```typescript
314
- const counter = createMachine({
315
- id: 'counter',
316
- initial: 'active',
317
- context: { count: 0 },
318
- states: {
319
- active: {
320
- on: {
321
- INCREMENT: {
322
- target: 'active',
323
- guard: (ctx) => ctx.count < 10, // Only if count < 10
324
- action: (ctx) => ({ count: ctx.count + 1 }),
325
- },
326
- DECREMENT: {
327
- target: 'active',
328
- guard: (ctx) => ctx.count > 0, // Only if count > 0
329
- action: (ctx) => ({ count: ctx.count - 1 }),
330
- },
331
- },
332
- },
333
- },
334
- });
335
- ```
336
-
337
- ### Entry & Exit Actions
338
-
339
- ```typescript
340
- const wizard = createMachine({
341
- id: 'wizard',
342
- initial: 'step1',
343
- context: { data: {} },
344
- states: {
345
- step1: {
346
- entry: (ctx) => console.log('Entered step 1'),
347
- exit: (ctx) => console.log('Leaving step 1'),
348
- on: { NEXT: 'step2' },
349
- },
350
- step2: {
351
- on: { BACK: 'step1', SUBMIT: 'complete' },
352
- },
353
- complete: {
354
- entry: (ctx) => console.log('Done!'),
355
- },
356
- },
357
- });
358
- ```
359
-
360
- ### Observability Integration
361
-
362
- Machines automatically integrate with `observe()`:
363
-
364
- ```typescript
365
- // Errors include machine context
366
- observe({ errors: true });
367
-
368
- const auth = createMachine({
369
- id: 'auth',
370
- observe: true, // Track transitions
371
- // or
372
- observe: {
373
- transitions: true,
374
- context: true, // Include context in events
375
- },
376
- });
377
-
378
- // When an error occurs, it includes:
379
- // { machineId: 'auth', machineState: 'loading', ... }
380
- ```
381
-
382
- ### Custom Transport
383
-
384
- ```typescript
385
- import { observe, createFetchTransport, createConsoleTransport } from 'svoose';
386
-
387
- // Fetch with custom headers
388
- const transport = createFetchTransport('/api/metrics', {
389
- headers: { 'Authorization': 'Bearer xxx' },
390
- onError: (err) => console.error(err),
391
- });
392
- observe({ transport });
393
-
394
- // Console only (for development) — no network requests
395
- observe({ transport: createConsoleTransport({ pretty: true }) });
396
-
397
- // Noop (silent, for production without backend)
398
- observe({ transport: { send: () => {} } });
399
-
400
- // Custom transport (Sentry, Datadog, etc.)
401
- const myTransport = {
402
- async send(events) {
403
- await myApi.track(events);
404
- },
405
- };
406
- observe({ transport: myTransport });
407
-
408
- // Dev vs Prod pattern
409
- const isDev = import.meta.env.DEV;
410
- observe({
411
- transport: isDev
412
- ? createConsoleTransport({ pretty: true })
413
- : createFetchTransport('/api/metrics'),
414
- });
415
- ```
416
-
417
- #### Beacon & Hybrid Transport (v0.1.8+)
418
-
419
- Prevent data loss on page close with `sendBeacon`:
420
-
421
- ```typescript
422
- import { createBeaconTransport, createHybridTransport } from 'svoose';
423
-
424
- // Beacon only — guaranteed delivery on page close
425
- observe({
426
- transport: createBeaconTransport('/api/metrics', {
427
- maxPayloadSize: 60000, // auto-chunks if exceeded (default: 60KB)
428
- }),
429
- });
430
-
431
- // Hybrid (recommended for production)
432
- // Uses fetch normally, switches to beacon on page close
433
- const transport = createHybridTransport('/api/metrics', {
434
- default: 'fetch', // normal operation
435
- onUnload: 'beacon', // page close / tab switch
436
- headers: { 'Authorization': 'Bearer xxx' },
437
- });
438
-
439
- observe({ transport });
440
-
441
- // Cleanup when done (removes lifecycle listeners)
442
- transport.destroy();
443
- ```
444
-
445
- ## Bundle Size
446
-
447
- Tree-shakeable — pay only for what you use:
448
-
449
- | Import | Size (gzip) |
450
- |--------|-------------|
451
- | `observe()` + vitals + errors + metrics | ~3.6 KB |
452
- | `createMachine()` only | ~0.8 KB |
453
- | Full bundle (v0.1.x) | ~5.3 KB |
454
- | Full production (v0.2.0+) | ~6 KB |
455
-
456
- > Most apps only need `observe()` core (~3.6 KB). Compare: Sentry ~20KB, PostHog ~40KB.
457
-
458
- ## TypeScript
459
-
460
- Full TypeScript support with inference:
461
-
462
- ```typescript
463
- type AuthEvent =
464
- | { type: 'LOGIN'; email: string }
465
- | { type: 'SUCCESS'; user: User }
466
- | { type: 'ERROR'; message: string }
467
- | { type: 'LOGOUT' };
468
-
469
- const auth = createMachine<
470
- { user: User | null; error: string | null },
471
- 'idle' | 'loading' | 'authenticated',
472
- AuthEvent
473
- >({
474
- id: 'auth',
475
- initial: 'idle',
476
- context: { user: null, error: null },
477
- states: {
478
- // States are type-checked
479
- idle: {
480
- on: {
481
- // Events are type-checked
482
- LOGIN: 'loading',
483
- },
484
- },
485
- loading: {
486
- on: {
487
- SUCCESS: {
488
- target: 'authenticated',
489
- // event.user is typed as User
490
- action: (ctx, event) => ({ user: event.user }),
491
- },
492
- },
493
- },
494
- authenticated: {
495
- on: { LOGOUT: 'idle' },
496
- },
497
- },
498
- });
499
-
500
- auth.matches('idle'); // ✓ type-checked
501
- auth.matches('invalid'); // ✗ TypeScript error
502
- auth.send('LOGOUT'); // ✓ type-checked
503
- auth.send('INVALID'); // ✗ TypeScript error
504
- ```
505
-
506
- ## Svelte 5 Usage
507
-
508
- ### Reactive State Machines (Recommended)
509
-
510
- Use `useMachine()` from `svoose/svelte` for automatic reactivity:
511
-
512
- ```svelte
513
- <script lang="ts">
514
- import { useMachine } from 'svoose/svelte';
515
-
516
- // State machine with automatic Svelte 5 reactivity
517
- const toggle = useMachine({
518
- id: 'toggle',
519
- initial: 'off',
520
- states: {
521
- off: { on: { TOGGLE: 'on' } },
522
- on: { on: { TOGGLE: 'off' } },
523
- },
524
- });
525
-
526
- // toggle.state and toggle.context are reactive!
527
- // Changes automatically trigger re-renders
528
- </script>
529
-
530
- <button onclick={() => toggle.send('TOGGLE')}>
531
- {toggle.state}
532
- </button>
533
-
534
- {#if toggle.matches('on')}
535
- <p>Light is on!</p>
536
- {/if}
537
- ```
538
-
539
- ### With Observability
540
-
541
- ```svelte
542
- <script lang="ts">
543
- import { observe } from 'svoose';
544
- import { useMachine } from 'svoose/svelte';
545
- import { onMount, onDestroy } from 'svelte';
546
-
547
- // Start observing
548
- let cleanup: (() => void) | null = null;
549
-
550
- onMount(() => {
551
- cleanup = observe({ endpoint: '/api/metrics' });
552
- });
553
-
554
- onDestroy(() => cleanup?.());
555
-
556
- // Reactive machine with observation
557
- const auth = useMachine({
558
- id: 'auth',
559
- initial: 'idle',
560
- context: { user: null },
561
- observe: true, // Track transitions
562
- states: {
563
- idle: { on: { LOGIN: 'loading' } },
564
- loading: {
565
- on: {
566
- SUCCESS: {
567
- target: 'authenticated',
568
- action: (ctx, e) => ({ user: e.user }),
569
- },
570
- ERROR: 'idle',
571
- },
572
- },
573
- authenticated: { on: { LOGOUT: 'idle' } },
574
- },
575
- });
576
- </script>
577
-
578
- <p>Status: {auth.state}</p>
579
- <p>User: {auth.context.user?.name ?? 'Not logged in'}</p>
580
- ```
581
-
582
- ### Non-Reactive Usage
583
-
584
- For non-reactive scenarios (outside components, vanilla JS), use `createMachine()`:
585
-
586
- ```typescript
587
- import { createMachine } from 'svoose';
588
-
589
- const machine = createMachine({
590
- id: 'toggle',
591
- initial: 'off',
592
- states: {
593
- off: { on: { TOGGLE: 'on' } },
594
- on: { on: { TOGGLE: 'off' } },
595
- },
596
- });
597
- ```
598
-
599
- ## Roadmap
600
-
601
- - **v0.1.3** Sampling (per-event-type rate limiting)
602
- - **v0.1.4** — Hotfix (missing sampling.js)
603
- - **v0.1.5** ✅ — Session Tracking + CLS Session Windows fix
604
- - **v0.1.6** ✅ — Custom metrics (`metric()` API)
605
- - **v0.1.7** ✅ — Extended Metrics (counter/gauge/histogram + typed API)
606
- - **v0.1.8** ✅ — Beacon + Hybrid Transport
607
- - **v0.1.9** Retry Logic
608
- - **v0.1.10** — Privacy Utilities
609
- - **v0.2.0** — Production-Ready Observability + Bundle Restructure (modular entry points)
610
- - **v0.3.0** SvelteKit Integration (Vite plugin, hooks, route tracking)
611
- - **v1.0.0** — Stable Release (Q1 2027)
612
-
613
- > **Note**: FSM is a lightweight bonus feature, not an XState competitor. For complex state machines, use XState.
614
-
615
- See [ROADMAP.md](./ROADMAP.md) for detailed plans.
616
-
617
- ## License
618
-
619
- MIT
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