@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/LICENSE +21 -0
- package/dist/client-DIOWX7_9.js +170 -0
- package/dist/client.d.ts +2 -0
- package/dist/client.js +11 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2622 -0
- package/dist/module-BfXpy-Wp.js +4198 -0
- package/dist/server.d.ts +2 -0
- package/dist/server.js +78 -0
- package/dist/src/adapters/client/browser-analytics.d.ts +24 -0
- package/dist/src/adapters/server/server-analytics.d.ts +20 -0
- package/dist/src/client/index.d.ts +7 -0
- package/dist/src/client.d.ts +57 -0
- package/dist/src/core/events/index.d.ts +18 -0
- package/dist/src/core/events/types.d.ts +48 -0
- package/dist/src/index.d.ts +6 -0
- package/dist/src/providers/base.provider.d.ts +17 -0
- package/dist/src/providers/index.d.ts +4 -0
- package/dist/src/providers/posthog/client.d.ts +24 -0
- package/dist/src/providers/posthog/server.d.ts +19 -0
- package/dist/src/providers/posthog/types.d.ts +27 -0
- package/dist/src/server/index.d.ts +6 -0
- package/dist/src/server.d.ts +29 -0
- package/dist/test/base-provider.test.d.ts +1 -0
- package/dist/test/client-analytics.test.d.ts +1 -0
- package/dist/test/events.test.d.ts +1 -0
- package/dist/test/index.test.d.ts +1 -0
- package/dist/test/mock-provider.d.ts +29 -0
- package/dist/test/server-analytics.test.d.ts +1 -0
- package/package.json +74 -0
- package/readme.md +814 -0
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
|