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