@stacksee/analytics 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/readme.md ADDED
@@ -0,0 +1,814 @@
1
+ # @stacksee/analytics
2
+
3
+ A highly typed, provider-agnostic analytics library for TypeScript applications. Works seamlessly on both client and server sides with full type safety for your custom events.
4
+
5
+ ## Features
6
+
7
+ - 🎯 **Type-safe events**: Define your own strongly typed events with full IntelliSense support
8
+ - 🔌 **Plugin architecture**: Easily add analytics providers by passing them as plugins
9
+ - 🌐 **Universal**: Same API works on both client (browser) and server (Node.js)
10
+ - 📦 **Lightweight**: Zero dependencies on the core library
11
+ - 🏗️ **Framework agnostic**: Use with any JavaScript framework
12
+ - 🌎 **Edge ready**: The server client is compatible with edge runtime (e.g. Cloudflare Workers)
13
+ - 🔧 **Extensible**: Simple interface to add new providers
14
+
15
+ ## Installation
16
+
17
+ ```bash
18
+ pnpm install @stacksee/analytics
19
+
20
+ # For PostHog support
21
+ pnpm install posthog-js posthog-node
22
+ ```
23
+
24
+ ## Quick Start
25
+
26
+ ### 1. Define Your Events
27
+
28
+ Create strongly typed events specific to your application:
29
+
30
+ ```typescript
31
+ import { CreateEventDefinition, EventCollection } from '@stacksee/analytics';
32
+
33
+ // Define your event types
34
+ export const AppEvents = {
35
+ userSignedUp: {
36
+ name: 'user_signed_up',
37
+ category: 'user',
38
+ properties: {} as {
39
+ userId: string;
40
+ email: string;
41
+ plan: 'free' | 'pro' | 'enterprise';
42
+ referralSource?: string;
43
+ }
44
+ },
45
+
46
+ featureUsed: {
47
+ name: 'feature_used',
48
+ category: 'engagement',
49
+ properties: {} as {
50
+ featureName: string;
51
+ userId: string;
52
+ duration?: number;
53
+ }
54
+ }
55
+ } as const satisfies EventCollection<Record<string, CreateEventDefinition<string>>>;
56
+
57
+ // Extract types for use in your app
58
+ export type AppEventName = keyof typeof AppEvents;
59
+ export type AppEventProperties<T extends AppEventName> = typeof AppEvents[T]['properties'];
60
+ ```
61
+
62
+ Tip: If you have a lot of events, you can also divide your events into multiple files, then export them as a single object.
63
+
64
+ ### 2. Client-Side Usage
65
+
66
+ ```typescript
67
+ import { createClientAnalytics } from '@stacksee/analytics/client';
68
+ import { PostHogClientProvider } from '@stacksee/analytics/providers/posthog';
69
+ import { AppEvents } from './events';
70
+
71
+ // Initialize analytics with providers as plugins
72
+ const analytics = createClientAnalytics({
73
+ providers: [
74
+ new PostHogClientProvider({
75
+ apiKey: 'your-posthog-api-key',
76
+ host: 'https://app.posthog.com' // optional
77
+ }),
78
+ // Add more providers here as needed
79
+ ],
80
+ debug: true,
81
+ enabled: true
82
+ });
83
+
84
+ // Track events with full type safety
85
+ analytics.track(AppEvents.pageViewed.name, {
86
+ path: '/dashboard',
87
+ title: 'Dashboard',
88
+ referrer: document.referrer
89
+ });
90
+
91
+ analytics.track(AppEvents.userSignedUp.name, {
92
+ userId: 'user-123',
93
+ email: 'user@example.com',
94
+ plan: 'pro',
95
+ referralSource: 'google'
96
+ });
97
+
98
+ // Identify users
99
+ analytics.identify('user-123', {
100
+ email: 'user@example.com',
101
+ name: 'John Doe',
102
+ plan: 'pro'
103
+ });
104
+ ```
105
+
106
+ ### 3. Server-Side Usage
107
+
108
+ ```typescript
109
+ import { createServerAnalytics } from '@stacksee/analytics/server';
110
+ import { PostHogServerProvider } from '@stacksee/analytics/providers/posthog';
111
+ import { AppEvents } from './events';
112
+
113
+ // Create analytics instance with providers as plugins
114
+ const analytics = createServerAnalytics({
115
+ providers: [
116
+ new PostHogServerProvider({
117
+ apiKey: process.env.POSTHOG_API_KEY,
118
+ host: process.env.POSTHOG_HOST
119
+ }),
120
+ // Add more providers here as needed
121
+ ],
122
+ debug: process.env.NODE_ENV === 'development',
123
+ enabled: true
124
+ });
125
+
126
+ // Track events - now returns a Promise
127
+ await analytics.track(AppEvents.featureUsed.name, {
128
+ featureName: 'export-data',
129
+ userId: 'user-123',
130
+ duration: 1500
131
+ }, {
132
+ userId: 'user-123',
133
+ context: {
134
+ page: {
135
+ path: '/api/export',
136
+ }
137
+ }
138
+ });
139
+
140
+ // Important: Always call shutdown when done, some providers such as Posthog require flushing events.
141
+ await analytics.shutdown();
142
+ ```
143
+
144
+ ### Async Tracking: When to await vs fire-and-forget
145
+
146
+ The `track()` method now returns a `Promise<void>`, giving you control over how to handle event tracking:
147
+
148
+ #### Fire-and-forget (Client-side typical usage)
149
+ ```typescript
150
+ // Don't await - let events send in the background
151
+ analytics.track('button_clicked', {
152
+ buttonId: 'checkout',
153
+ label: 'Proceed to Checkout'
154
+ });
155
+
156
+ // User interaction continues immediately
157
+ ```
158
+
159
+ #### Await for critical events (Server-side typical usage)
160
+ ```typescript
161
+ // In serverless/edge functions, you have two patterns:
162
+
163
+ // Pattern 1: Critical events that MUST complete before response
164
+ export async function handler(req, res) {
165
+ try {
166
+ // Process payment
167
+ const paymentResult = await processPayment(req.body);
168
+
169
+ // For critical events like payments, await to ensure they're tracked
170
+ // This blocks the response but guarantees the event is recorded
171
+ await analytics.track('payment_processed', {
172
+ amount: paymentResult.amount,
173
+ currency: 'USD',
174
+ userId: req.userId,
175
+ transactionId: paymentResult.id
176
+ });
177
+
178
+ return res.json({ success: true, transactionId: paymentResult.id });
179
+ } catch (error) {
180
+ // Even on error, you might want to track
181
+ await analytics.track('payment_failed', {
182
+ error: error.message,
183
+ userId: req.userId
184
+ });
185
+
186
+ return res.status(500).json({ error: 'Payment failed' });
187
+ }
188
+ }
189
+
190
+ // Pattern 2: Non-critical events using waitUntil (Vercel example)
191
+ import { waitUntil } from '@vercel/functions';
192
+
193
+ export default async function handler(req, res) {
194
+ const startTime = Date.now();
195
+
196
+ // Process request
197
+ const result = await processRequest(req);
198
+
199
+ // Track analytics in background without blocking response
200
+ waitUntil(
201
+ analytics.track('api_request', {
202
+ endpoint: req.url,
203
+ duration: Date.now() - startTime,
204
+ userId: req.headers['x-user-id']
205
+ }).then(() => analytics.shutdown())
206
+ );
207
+
208
+ // Response sent immediately
209
+ return res.json(result);
210
+ }
211
+ ```
212
+
213
+ #### Error handling
214
+ ```typescript
215
+ // The track method catches provider errors internally and logs them
216
+ // It won't throw even if a provider fails, ensuring one provider's failure
217
+ // doesn't affect others
218
+
219
+ // If you need to know about failures, check your logs
220
+ await analytics.track('important_event', { data: 'value' });
221
+ // Even if one provider fails, others will still receive the event
222
+ ```
223
+
224
+ #### Best practices:
225
+ - **Client-side**: Usually fire-and-forget for better UX
226
+ - **Server-side (serverless)**: Use `waitUntil` for non-critical events to avoid blocking responses
227
+ - **Server-side (long-running)**: Can await or fire-and-forget based on criticality
228
+ - **Critical events**: Always await (e.g., payments, sign-ups, conversions that must be recorded)
229
+ - **High-volume/non-critical events**: Use `waitUntil` in serverless or fire-and-forget in long-running servers
230
+ - **Error tracking**: Consider awaiting to ensure errors are captured before function terminates
231
+
232
+ ### A complete example
233
+
234
+ Here's a complete example using Svelte 5 that demonstrates both client and server-side analytics for a waitlist signup:
235
+
236
+ ```typescript
237
+ // src/lib/config/analytics.ts
238
+ import { createClientAnalytics } from '@stacksee/analytics/client';
239
+ import { PostHogClientProvider } from '@stacksee/analytics/providers/posthog';
240
+ import { PUBLIC_POSTHOG_API_KEY, PUBLIC_POSTHOG_HOST } from '$env/static/public';
241
+
242
+ // Define your events for the waitlist
243
+ export const AppEvents = {
244
+ waitlistJoined: {
245
+ name: 'waitlist_joined',
246
+ category: 'user',
247
+ properties: {} as {
248
+ email: string;
249
+ source: string; // e.g., 'homepage_banner', 'product_page_modal'
250
+ }
251
+ },
252
+ waitlistApproved: {
253
+ name: 'waitlist_approved',
254
+ category: 'user',
255
+ properties: {} as {
256
+ userId: string; // This could be the email or a generated ID
257
+ email: string;
258
+ }
259
+ }
260
+ } as const;
261
+
262
+ // Client-side analytics instance
263
+ export const clientAnalytics = createClientAnalytics({
264
+ providers: [
265
+ new PostHogClientProvider({
266
+ apiKey: import.meta.env.VITE_POSTHOG_KEY, // Ensure VITE_POSTHOG_KEY is in your .env file
267
+ host: 'https://app.posthog.com'
268
+ })
269
+ ],
270
+ debug: import.meta.env.DEV
271
+ });
272
+ ```
273
+
274
+ ```typescript
275
+ // src/lib/server/analytics.ts
276
+ import { createServerAnalytics } from '@stacksee/analytics/server';
277
+ import { PostHogServerProvider } from '@stacksee/analytics/providers/posthog';
278
+ import { AppEvents } from '$lib/config/analytics'; // Import AppEvents
279
+ import { PUBLIC_POSTHOG_API_KEY, PUBLIC_POSTHOG_HOST } from '$env/static/public';
280
+
281
+ export const serverAnalytics = createServerAnalytics({
282
+ providers: [
283
+ new PostHogServerProvider({
284
+ apiKey: PUBLIC_POSTHOG_API_KEY,
285
+ host: PUBLIC_POSTHOG_HOST
286
+ })
287
+ ],
288
+ debug: process.env.NODE_ENV === 'development'
289
+ });
290
+ ```
291
+
292
+ ```svelte
293
+ <!-- src/routes/join-waitlist/+page.svelte -->
294
+ <script lang="ts">
295
+ import { clientAnalytics, AppEvents } from '$lib/config/analytics';
296
+
297
+ let email = $state('');
298
+ let loading = $state(false);
299
+ let message = $state('');
300
+
301
+ async function handleWaitlistSubmit(event: Event) {
302
+ event.preventDefault();
303
+ loading = true;
304
+ message = '';
305
+
306
+ try {
307
+ // Track waitlist joined event on the client
308
+ clientAnalytics.track(AppEvents.waitlistJoined.name, {
309
+ email,
310
+ source: 'waitlist_page_form'
311
+ });
312
+
313
+ // Submit email to the server
314
+ const response = await fetch('/api/join-waitlist', {
315
+ method: 'POST',
316
+ headers: {
317
+ 'Content-Type': 'application/json'
318
+ },
319
+ body: JSON.stringify({ email })
320
+ });
321
+
322
+ const result = await response.json();
323
+
324
+ if (!response.ok) {
325
+ throw new Error(result.message || 'Failed to join waitlist');
326
+ }
327
+
328
+ message = 'Successfully joined the waitlist! We will notify you once you are approved.';
329
+ } catch (error) {
330
+ console.error('Waitlist submission failed:', error);
331
+ message = error instanceof Error ? error.message : 'An unexpected error occurred.';
332
+ } finally {
333
+ loading = false;
334
+ }
335
+ }
336
+ </script>
337
+
338
+ <h2>Join Our Waitlist</h2>
339
+ <form onsubmit={handleWaitlistSubmit}>
340
+ <label>
341
+ Email:
342
+ <input
343
+ type="email"
344
+ bind:value={email}
345
+ placeholder="you@example.com"
346
+ required
347
+ disabled={loading}
348
+ />
349
+ </label>
350
+ <button type="submit" disabled={loading}>
351
+ {loading ? 'Joining...' : 'Join Waitlist'}
352
+ </button>
353
+ </form>
354
+
355
+ {#if message}
356
+ <p>{message}</p>
357
+ {/if}
358
+ ```
359
+
360
+ ```typescript
361
+ // src/routes/api/join-waitlist/+server.ts
362
+ import { serverAnalytics } from '$lib/server/analytics';
363
+ import { AppEvents } from '$lib/config/analytics'; // Import AppEvents
364
+ import { json, type RequestHandler } from '@sveltejs/kit';
365
+
366
+ async function approveUserForWaitlist(email: string): Promise<{ userId: string }> {
367
+ console.log(`Processing waitlist application for: ${email}`);
368
+
369
+ const userId = `user_${Date.now()}_${email.split('@')[0]}`;
370
+
371
+ return { userId };
372
+ }
373
+
374
+ export const POST: RequestHandler = async ({ request }) => {
375
+ try {
376
+ const body = await request.json();
377
+ const email = body.email;
378
+
379
+ if (!email || typeof email !== 'string') {
380
+ return json({ success: false, message: 'Email is required' }, { status: 400 });
381
+ }
382
+
383
+ const { userId } = await approveUserForWaitlist(email);
384
+
385
+ serverAnalytics.track(AppEvents.waitlistApproved.name, {
386
+ userId,
387
+ email
388
+ }, {
389
+ userId,
390
+ context: {
391
+ page: {
392
+ path: '/api/join-waitlist'
393
+ },
394
+ ip: request.headers.get('x-forwarded-for') || undefined
395
+ }
396
+ });
397
+
398
+ // Important: Call shutdown if your application instance is short-lived. (e.g. serverless function)
399
+ // For long-running servers, you might call this on server shutdown.
400
+ await serverAnalytics.shutdown();
401
+
402
+ return json({ success: true, userId, message: 'Successfully joined and approved for waitlist.' });
403
+ } catch (error) {
404
+ console.error('Failed to process waitlist application:', error);
405
+ // In production, be careful about leaking error details
406
+ const errorMessage = error instanceof Error ? error.message : 'Internal server error';
407
+ return json({ success: false, message: errorMessage }, { status: 500 });
408
+ }
409
+ // Note: serverAnalytics.shutdown() should ideally be called when the server itself is shutting down,
410
+ // not after every request in a typical web server setup, unless the provider requires it for batching.
411
+ // For this example, PostHogServerProvider benefits from shutdown to flush events,
412
+ // so if this were, for example, a serverless function processing one event, calling shutdown would be appropriate.
413
+ // If it's a long-running server, manage shutdown centrally.
414
+ };
415
+ ```
416
+
417
+ ## Advanced Usage
418
+
419
+ ### Creating a Typed Analytics Service
420
+
421
+ For better type safety across your application, create a typed wrapper:
422
+
423
+ ```typescript
424
+ import {
425
+ BrowserAnalytics,
426
+ ServerAnalytics,
427
+ ExtractEventNames,
428
+ ExtractEventPropertiesFromCollection
429
+ } from '@stacksee/analytics';
430
+ import { AppEvents } from './events';
431
+
432
+ // Type aliases for your app
433
+ type AppEventName = ExtractEventNames<typeof AppEvents>;
434
+ type AppEventProps<T extends AppEventName> = ExtractEventPropertiesFromCollection<typeof AppEvents, T>;
435
+
436
+ // Client-side typed wrapper
437
+ export class AppAnalytics {
438
+ constructor(private analytics: BrowserAnalytics) {}
439
+
440
+ track<T extends AppEventName>(
441
+ eventName: T,
442
+ properties: AppEventProps<T>
443
+ ): Promise<void> {
444
+ return this.analytics.track(eventName, properties);
445
+ }
446
+
447
+ // ... other methods
448
+ }
449
+
450
+ // Server-side typed wrapper
451
+ export class ServerAppAnalytics {
452
+ constructor(private analytics: ServerAnalytics) {}
453
+
454
+ track<T extends AppEventName>(
455
+ eventName: T,
456
+ properties: AppEventProps<T>,
457
+ options?: { userId?: string; sessionId?: string }
458
+ ): Promise<void> {
459
+ return this.analytics.track(eventName, properties, options);
460
+ }
461
+
462
+ // ... other methods
463
+ }
464
+ ```
465
+
466
+ ### Event Categories
467
+
468
+ Event categories help organize your analytics data. The SDK provides predefined categories with TypeScript autocomplete:
469
+
470
+ - `product` - Product-related events (views, purchases, etc.)
471
+ - `user` - User lifecycle events (signup, login, profile updates)
472
+ - `navigation` - Page views and navigation events
473
+ - `conversion` - Conversion and goal completion events
474
+ - `engagement` - Feature usage and interaction events
475
+ - `error` - Error tracking events
476
+ - `performance` - Performance monitoring events
477
+
478
+ You can also use **custom categories** for your specific needs:
479
+
480
+ ```typescript
481
+ export const AppEvents = {
482
+ aiResponse: {
483
+ name: 'ai_response_generated',
484
+ category: 'ai', // Custom category
485
+ properties: {} as {
486
+ model: string;
487
+ responseTime: number;
488
+ tokensUsed: number;
489
+ }
490
+ },
491
+
492
+ customWorkflow: {
493
+ name: 'workflow_completed',
494
+ category: 'workflow', // Another custom category
495
+ properties: {} as {
496
+ workflowId: string;
497
+ duration: number;
498
+ steps: number;
499
+ }
500
+ }
501
+ } as const satisfies EventCollection<Record<string, CreateEventDefinition<string>>>;
502
+ ```
503
+
504
+ ### Adding Custom Providers
505
+
506
+ Implement the `AnalyticsProvider` interface to add support for other analytics services:
507
+
508
+ ```typescript
509
+ import { BaseAnalyticsProvider, BaseEvent, EventContext } from '@stacksee/analytics';
510
+
511
+ export class GoogleAnalyticsProvider extends BaseAnalyticsProvider {
512
+ name = 'GoogleAnalytics';
513
+ private measurementId: string;
514
+
515
+ constructor(config: { measurementId: string; debug?: boolean; enabled?: boolean }) {
516
+ super({ debug: config.debug, enabled: config.enabled });
517
+ this.measurementId = config.measurementId;
518
+ }
519
+
520
+ async initialize(): Promise<void> {
521
+ // Initialize GA
522
+ }
523
+
524
+ track(event: BaseEvent, context?: EventContext): void {
525
+ // Send event to GA
526
+ }
527
+
528
+ identify(userId: string, traits?: Record<string, unknown>): void {
529
+ // Set user properties in GA
530
+ }
531
+
532
+ // ... implement other required methods
533
+ }
534
+ ```
535
+
536
+ Then use it as a plugin in your configuration:
537
+
538
+ ```typescript
539
+ const analytics = await createClientAnalytics({
540
+ providers: [
541
+ new PostHogClientProvider({ apiKey: 'xxx' }),
542
+ new GoogleAnalyticsProvider({ measurementId: 'xxx' })
543
+ ]
544
+ });
545
+ ```
546
+
547
+ ### Client-Only and Server-Only Providers
548
+
549
+ Some analytics libraries are designed to work only in specific environments. For example:
550
+ - **Client-only**: Google Analytics (gtag.js), Hotjar, FullStory
551
+ - **Server-only**: Some enterprise analytics APIs that require secret keys
552
+ - **Universal**: PostHog, Segment (have separate client/server SDKs)
553
+
554
+ The library handles this by having separate provider implementations for client and server environments:
555
+
556
+ ```typescript
557
+ // Client-side provider for a client-only analytics service
558
+ import { BaseAnalyticsProvider, BaseEvent, EventContext } from '@stacksee/analytics';
559
+
560
+ export class MixpanelClientProvider extends BaseAnalyticsProvider {
561
+ name = 'Mixpanel-Client';
562
+
563
+ constructor(config: { projectToken: string }) {
564
+ super();
565
+ // Initialize Mixpanel browser SDK
566
+ }
567
+
568
+ // ... implement required methods
569
+ }
570
+
571
+ // Server-side provider for a server-only analytics service
572
+ export class MixpanelServerProvider extends BaseAnalyticsProvider {
573
+ name = 'Mixpanel-Server';
574
+
575
+ constructor(config: { projectToken: string; apiSecret: string }) {
576
+ super();
577
+ // Initialize Mixpanel server SDK with secret
578
+ }
579
+
580
+ // ... implement required methods
581
+ }
582
+ ```
583
+
584
+ Then use the appropriate provider based on your environment:
585
+
586
+ ```typescript
587
+ // Client-side usage
588
+ import { createClientAnalytics } from '@stacksee/analytics/client';
589
+ import { MixpanelClientProvider } from './providers/mixpanel-client';
590
+
591
+ const clientAnalytics = createClientAnalytics({
592
+ providers: [
593
+ new MixpanelClientProvider({ projectToken: 'xxx' })
594
+ ]
595
+ });
596
+
597
+ // Server-side usage
598
+ import { createServerAnalytics } from '@stacksee/analytics/server';
599
+ import { MixpanelServerProvider } from './providers/mixpanel-server';
600
+
601
+ const serverAnalytics = createServerAnalytics({
602
+ providers: [
603
+ new MixpanelServerProvider({
604
+ projectToken: 'xxx',
605
+ apiSecret: 'secret-xxx' // Server-only configuration
606
+ })
607
+ ]
608
+ });
609
+ ```
610
+
611
+ **Important notes:**
612
+ - Client providers should only use browser-compatible APIs
613
+ - Server providers can use Node.js-specific features and secret credentials
614
+ - The provider interface is the same, ensuring consistent usage patterns
615
+ - Import paths are separate (`/client` vs `/server`) to prevent accidental usage in wrong environments
616
+
617
+ ### Using Multiple Providers
618
+
619
+ The plugin architecture makes it easy to send events to multiple analytics services simultaneously:
620
+
621
+ ```typescript
622
+ import { createClientAnalytics } from '@stacksee/analytics/client';
623
+ import { PostHogClientProvider } from '@stacksee/analytics/providers/posthog';
624
+ // Import your custom providers
625
+ import { GoogleAnalyticsProvider } from './providers/google-analytics';
626
+ import { MixpanelProvider } from './providers/mixpanel';
627
+
628
+ const analytics = createClientAnalytics({
629
+ providers: [
630
+ // PostHog for product analytics
631
+ new PostHogClientProvider({
632
+ apiKey: process.env.NEXT_PUBLIC_POSTHOG_KEY,
633
+ host: 'https://app.posthog.com'
634
+ }),
635
+
636
+ // Google Analytics for marketing insights
637
+ new GoogleAnalyticsProvider({
638
+ measurementId: process.env.NEXT_PUBLIC_GA_ID
639
+ }),
640
+
641
+ // Mixpanel for detailed user journey analysis
642
+ new MixpanelProvider({
643
+ projectToken: process.env.NEXT_PUBLIC_MIXPANEL_TOKEN
644
+ })
645
+ ],
646
+ debug: process.env.NODE_ENV === 'development',
647
+ enabled: true
648
+ });
649
+
650
+ // All providers will receive this event
651
+ analytics.track('user_signed_up', {
652
+ userId: 'user-123',
653
+ plan: 'pro'
654
+ });
655
+ ```
656
+
657
+ ## Server Deployments and waitUntil
658
+
659
+ When deploying your application to serverless environments, it's important to handle analytics events properly to ensure they are sent before the function terminates. Different platforms provide their own mechanisms for this:
660
+
661
+ ### Vercel Functions
662
+
663
+ Vercel provides a `waitUntil` API that allows you to continue processing after the response has been sent:
664
+
665
+ ```typescript
666
+ import { waitUntil } from '@vercel/functions';
667
+
668
+ export default async function handler(req, res) {
669
+ const analytics = createServerAnalytics({
670
+ providers: [new PostHogServerProvider({ apiKey: process.env.POSTHOG_API_KEY })]
671
+ });
672
+
673
+ // Process your request and prepare response
674
+ const result = { success: true, data: 'processed' };
675
+
676
+ // Use waitUntil to track events and flush without blocking the response
677
+ waitUntil(
678
+ analytics.track('api_request', {
679
+ endpoint: '/api/users',
680
+ method: 'POST',
681
+ statusCode: 200,
682
+ responseTime: 150
683
+ }).then(() => analytics.shutdown())
684
+ );
685
+
686
+ // Response is sent immediately, tracking happens in background
687
+ res.status(200).json(result);
688
+ }
689
+ ```
690
+
691
+ ### Cloudflare Workers
692
+
693
+ Cloudflare Workers provides a `waitUntil` method on the execution context:
694
+
695
+ ```typescript
696
+ export default {
697
+ async fetch(request, env, ctx) {
698
+ const analytics = createServerAnalytics({
699
+ providers: [new PostHogServerProvider({ apiKey: env.POSTHOG_API_KEY })]
700
+ });
701
+
702
+ // Process request and prepare response
703
+ const response = new Response('OK', { status: 200 });
704
+
705
+ // Use ctx.waitUntil to track events and flush without blocking the response
706
+ ctx.waitUntil(
707
+ analytics.track('worker_execution', {
708
+ url: request.url,
709
+ method: request.method,
710
+ cacheStatus: 'MISS',
711
+ executionTime: 45
712
+ }).then(() => analytics.shutdown())
713
+ );
714
+
715
+ // Response is returned immediately, tracking happens in background
716
+ return response;
717
+ }
718
+ };
719
+ ```
720
+
721
+ ### Netlify Functions
722
+
723
+ Netlify Functions also support `waitUntil` through their context object:
724
+
725
+ ```typescript
726
+ export async function handler(event, context) {
727
+ const analytics = createServerAnalytics({
728
+ providers: [new PostHogServerProvider({ apiKey: process.env.POSTHOG_API_KEY })]
729
+ });
730
+
731
+ const responseBody = { success: true, data: 'processed' };
732
+
733
+ // Use context.waitUntil to track events and flush without blocking the response
734
+ context.waitUntil(
735
+ analytics.track('function_invocation', {
736
+ path: event.path,
737
+ httpMethod: event.httpMethod,
738
+ queryStringParameters: event.queryStringParameters,
739
+ executionTime: 120
740
+ }).then(() => analytics.shutdown())
741
+ );
742
+
743
+ // Response is returned immediately, tracking happens in background
744
+ return {
745
+ statusCode: 200,
746
+ body: JSON.stringify(responseBody)
747
+ };
748
+ }
749
+ ```
750
+
751
+ **Important Notes:**
752
+ 1. Always call `analytics.shutdown()` within `waitUntil` to ensure events are sent
753
+ 2. The `waitUntil` API is platform-specific, so make sure to use the correct import/usage for your deployment platform
754
+ 3. For long-running servers (not serverless), you should call `shutdown()` when the server itself is shutting down
755
+ 4. Some providers may batch events, so `shutdown()` ensures all pending events are sent
756
+
757
+ ## API Reference
758
+
759
+ ### Client API
760
+
761
+ #### `createClientAnalytics(config)`
762
+ Initialize analytics for browser environment.
763
+
764
+ - `config.providers` - Array of analytics provider instances
765
+ - `config.debug` - Enable debug logging
766
+ - `config.enabled` - Enable/disable analytics
767
+
768
+ #### `BrowserAnalytics`
769
+ - `track(eventName, properties): Promise<void>` - Track an event (returns a promise)
770
+ - `identify(userId, traits)` - Identify a user
771
+ - `page(properties)` - Track a page view
772
+ - `reset()` - Reset user session
773
+ - `updateContext(context)` - Update event context
774
+
775
+ ### Server API
776
+
777
+ #### `createServerAnalytics(config)`
778
+ Create analytics instance for server environment.
779
+
780
+ - `config.providers` - Array of analytics provider instances
781
+ - `config.debug` - Enable debug logging
782
+ - `config.enabled` - Enable/disable analytics
783
+
784
+ #### `ServerAnalytics`
785
+ - `track(eventName, properties, options): Promise<void>` - Track an event with optional context (returns a promise)
786
+ - `identify(userId, traits)` - Identify a user
787
+ - `page(properties, options)` - Track a page view
788
+ - `shutdown()` - Flush pending events and cleanup
789
+
790
+ ### Type Helpers
791
+
792
+ - `CreateEventDefinition<TName, TProperties>` - Define a single event
793
+ - `EventCollection<T>` - Define a collection of events
794
+ - `ExtractEventNames<T>` - Extract event names from a collection
795
+ - `ExtractEventPropertiesFromCollection<T, TEventName>` - Extract properties for a specific event
796
+
797
+ ## Best Practices
798
+
799
+ 1. **Define events in a central location** - Keep all event definitions in one file for consistency
800
+ 2. **Use const assertions** - Use `as const` for better type inference
801
+ 3. **Initialize early** - Initialize analytics as early as possible in your app lifecycle
802
+ 4. **Handle errors gracefully** - Analytics should never break your app
803
+ 5. **Respect privacy** - Implement user consent and opt-out mechanisms
804
+ 6. **Test your events** - Verify events are tracked correctly in development
805
+ 7. **Document events** - Add comments to explain when each event should be fired
806
+ 8. **Create provider instances once** - Reuse provider instances across your app
807
+
808
+ ## Contributing
809
+
810
+ Contributions are welcome! Please read our contributing guidelines before submitting PRs.
811
+
812
+ ## License
813
+
814
+ MIT