@stacksee/analytics 0.4.6 → 0.5.0
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/dist/adapters/client/browser-analytics.d.ts +42 -5
- package/dist/adapters/server/server-analytics.d.ts +50 -16
- package/dist/client-DTHZYkxx.js +90 -0
- package/dist/client.d.ts +2 -3
- package/dist/client.js +101 -59
- package/dist/core/events/types.d.ts +7 -1
- package/dist/providers/client.js +1 -1
- package/dist/providers/server.js +1 -1
- package/dist/server-DjEk1fUD.js +88 -0
- package/dist/server.d.ts +1 -1
- package/dist/server.js +97 -64
- package/package.json +1 -1
- package/readme.md +200 -12
- package/dist/client-RZPcOfAk.js +0 -86
- package/dist/server-CMRw9K0d.js +0 -84
package/dist/providers/client.js
CHANGED
package/dist/providers/server.js
CHANGED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
var p = Object.defineProperty;
|
|
2
|
+
var u = (r, e, s) => e in r ? p(r, e, { enumerable: !0, configurable: !0, writable: !0, value: s }) : r[e] = s;
|
|
3
|
+
var a = (r, e, s) => u(r, typeof e != "symbol" ? e + "" : e, s);
|
|
4
|
+
import { B as g } from "./base.provider-AfFL5W_P.js";
|
|
5
|
+
import { PostHog as n } from "posthog-node";
|
|
6
|
+
class y extends g {
|
|
7
|
+
constructor(s) {
|
|
8
|
+
super({ debug: s.debug, enabled: s.enabled });
|
|
9
|
+
a(this, "name", "PostHog-Server");
|
|
10
|
+
a(this, "client");
|
|
11
|
+
a(this, "initialized", !1);
|
|
12
|
+
a(this, "config");
|
|
13
|
+
this.config = s;
|
|
14
|
+
}
|
|
15
|
+
initialize() {
|
|
16
|
+
if (this.isEnabled() && !this.initialized) {
|
|
17
|
+
if (!this.config.apiKey || typeof this.config.apiKey != "string")
|
|
18
|
+
throw new Error("PostHog requires an apiKey");
|
|
19
|
+
try {
|
|
20
|
+
const { apiKey: s, ...i } = this.config;
|
|
21
|
+
this.client = new n(s, {
|
|
22
|
+
host: "https://app.posthog.com",
|
|
23
|
+
flushAt: 20,
|
|
24
|
+
flushInterval: 1e4,
|
|
25
|
+
...i
|
|
26
|
+
}), this.initialized = !0, this.log("Initialized successfully", this.config);
|
|
27
|
+
} catch (s) {
|
|
28
|
+
throw console.error("[PostHog-Server] Failed to initialize:", s), s;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
identify(s, i) {
|
|
33
|
+
!this.isEnabled() || !this.initialized || !this.client || (this.client.identify({
|
|
34
|
+
distinctId: s,
|
|
35
|
+
properties: i
|
|
36
|
+
}), this.log("Identified user", { userId: s, traits: i }));
|
|
37
|
+
}
|
|
38
|
+
track(s, i) {
|
|
39
|
+
var t, h, d;
|
|
40
|
+
if (!this.isEnabled() || !this.initialized || !this.client) return;
|
|
41
|
+
const l = {
|
|
42
|
+
...s.properties,
|
|
43
|
+
category: s.category,
|
|
44
|
+
timestamp: s.timestamp ? new Date(s.timestamp) : /* @__PURE__ */ new Date(),
|
|
45
|
+
...s.sessionId && { sessionId: s.sessionId },
|
|
46
|
+
...(i == null ? void 0 : i.page) && {
|
|
47
|
+
$current_url: i.page.path,
|
|
48
|
+
$page_title: i.page.title,
|
|
49
|
+
$referrer: i.page.referrer
|
|
50
|
+
},
|
|
51
|
+
...(i == null ? void 0 : i.device) && { device: i.device },
|
|
52
|
+
...(i == null ? void 0 : i.utm) && { utm: i.utm },
|
|
53
|
+
// Include user email and traits as regular event properties
|
|
54
|
+
...((t = i == null ? void 0 : i.user) == null ? void 0 : t.email) && { user_email: i.user.email },
|
|
55
|
+
...((h = i == null ? void 0 : i.user) == null ? void 0 : h.traits) && { user_traits: i.user.traits }
|
|
56
|
+
};
|
|
57
|
+
this.client.capture({
|
|
58
|
+
distinctId: s.userId || ((d = i == null ? void 0 : i.user) == null ? void 0 : d.userId) || "anonymous",
|
|
59
|
+
event: s.action,
|
|
60
|
+
properties: l
|
|
61
|
+
}), this.log("Tracked event", { event: s, context: i });
|
|
62
|
+
}
|
|
63
|
+
pageView(s, i) {
|
|
64
|
+
if (!this.isEnabled() || !this.initialized || !this.client) return;
|
|
65
|
+
const l = {
|
|
66
|
+
...s,
|
|
67
|
+
...(i == null ? void 0 : i.page) && {
|
|
68
|
+
path: i.page.path,
|
|
69
|
+
title: i.page.title,
|
|
70
|
+
referrer: i.page.referrer
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
this.client.capture({
|
|
74
|
+
distinctId: "anonymous",
|
|
75
|
+
event: "$pageview",
|
|
76
|
+
properties: l
|
|
77
|
+
}), this.log("Tracked page view", { properties: s, context: i });
|
|
78
|
+
}
|
|
79
|
+
async reset() {
|
|
80
|
+
!this.isEnabled() || !this.initialized || !this.client || (await this.client.flush(), this.log("Flushed pending events"));
|
|
81
|
+
}
|
|
82
|
+
async shutdown() {
|
|
83
|
+
this.client && (await this.client.shutdown(), this.log("Shutdown complete"));
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
export {
|
|
87
|
+
y as P
|
|
88
|
+
};
|
package/dist/server.d.ts
CHANGED
|
@@ -34,5 +34,5 @@ export interface ServerAnalyticsConfig {
|
|
|
34
34
|
* }, { userId: 'user-123' });
|
|
35
35
|
* ```
|
|
36
36
|
*/
|
|
37
|
-
export declare function createServerAnalytics<TEvents = never>(config: ServerAnalyticsConfig): ServerAnalytics<EventMapFromCollection<TEvents
|
|
37
|
+
export declare function createServerAnalytics<TEvents = never, TUserTraits extends Record<string, unknown> = Record<string, unknown>>(config: ServerAnalyticsConfig): ServerAnalytics<EventMapFromCollection<TEvents>, TUserTraits>;
|
|
38
38
|
export { ServerAnalytics };
|
package/dist/server.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
var
|
|
2
|
-
var
|
|
3
|
-
var s = (
|
|
4
|
-
import { P as
|
|
5
|
-
import { B as
|
|
6
|
-
class
|
|
1
|
+
var u = Object.defineProperty;
|
|
2
|
+
var o = (i, e, r) => e in i ? u(i, e, { enumerable: !0, configurable: !0, writable: !0, value: r }) : i[e] = r;
|
|
3
|
+
var s = (i, e, r) => o(i, typeof e != "symbol" ? e + "" : e, r);
|
|
4
|
+
import { P as w } from "./server-DjEk1fUD.js";
|
|
5
|
+
import { B as z } from "./base.provider-AfFL5W_P.js";
|
|
6
|
+
class v {
|
|
7
7
|
/**
|
|
8
8
|
* Creates a new ServerAnalytics instance for server-side event tracking.
|
|
9
9
|
*
|
|
@@ -129,28 +129,33 @@ class u {
|
|
|
129
129
|
* }
|
|
130
130
|
* ```
|
|
131
131
|
*/
|
|
132
|
-
identify(e,
|
|
133
|
-
for (const
|
|
134
|
-
|
|
132
|
+
identify(e, r) {
|
|
133
|
+
for (const t of this.providers)
|
|
134
|
+
t.identify(e, r);
|
|
135
135
|
}
|
|
136
136
|
/**
|
|
137
137
|
* Tracks a custom event with properties and optional context.
|
|
138
|
-
*
|
|
138
|
+
*
|
|
139
139
|
* This is the main method for tracking business events on the server side.
|
|
140
140
|
* The method sends the event to all configured providers and waits for completion.
|
|
141
141
|
* Failed providers don't prevent others from succeeding.
|
|
142
|
-
*
|
|
142
|
+
*
|
|
143
143
|
* Server-side tracking typically includes additional context like IP addresses,
|
|
144
144
|
* user agents, and server-specific metadata that isn't available on the client.
|
|
145
|
-
*
|
|
145
|
+
*
|
|
146
|
+
* **User Context (New):** You can now pass user data (email, traits) with each event
|
|
147
|
+
* via `options.user` or `options.context.user`. This is useful for providers like
|
|
148
|
+
* Loops, Customer.io, or Intercom that require user identifiers.
|
|
149
|
+
*
|
|
146
150
|
* @param eventName Name of the event to track (must match your event definitions)
|
|
147
151
|
* @param properties Event-specific properties and data
|
|
148
|
-
* @param options Optional configuration including user ID, session ID, and context
|
|
152
|
+
* @param options Optional configuration including user ID, session ID, user context, and additional context
|
|
149
153
|
* @param options.userId User ID to associate with this event
|
|
150
154
|
* @param options.sessionId Session ID to associate with this event
|
|
151
|
-
* @param options.
|
|
155
|
+
* @param options.user User context including email and traits (automatically included in event context)
|
|
156
|
+
* @param options.context Additional context for this event (page, device, etc.)
|
|
152
157
|
* @returns Promise that resolves when tracking is complete for all providers
|
|
153
|
-
*
|
|
158
|
+
*
|
|
154
159
|
* @example
|
|
155
160
|
* ```typescript
|
|
156
161
|
* // Basic event tracking
|
|
@@ -161,10 +166,10 @@ class u {
|
|
|
161
166
|
* statusCode: 200
|
|
162
167
|
* });
|
|
163
168
|
* ```
|
|
164
|
-
*
|
|
169
|
+
*
|
|
165
170
|
* @example
|
|
166
171
|
* ```typescript
|
|
167
|
-
* // Track with user context
|
|
172
|
+
* // Track with user context (recommended for email-based providers)
|
|
168
173
|
* await analytics.track('purchase_completed', {
|
|
169
174
|
* orderId: 'order-123',
|
|
170
175
|
* amount: 99.99,
|
|
@@ -172,40 +177,68 @@ class u {
|
|
|
172
177
|
* itemCount: 3
|
|
173
178
|
* }, {
|
|
174
179
|
* userId: 'user-456',
|
|
175
|
-
*
|
|
180
|
+
* user: {
|
|
181
|
+
* email: 'user@example.com',
|
|
182
|
+
* traits: {
|
|
183
|
+
* plan: 'pro',
|
|
184
|
+
* company: 'Acme Corp'
|
|
185
|
+
* }
|
|
186
|
+
* },
|
|
176
187
|
* context: {
|
|
177
188
|
* page: { path: '/checkout/complete' },
|
|
178
|
-
* device: { userAgent: req.headers['user-agent'] }
|
|
179
|
-
* ip: req.ip
|
|
189
|
+
* device: { userAgent: req.headers['user-agent'] }
|
|
180
190
|
* }
|
|
181
191
|
* });
|
|
192
|
+
* // Providers receive: context.user = { email: 'user@example.com', traits: {...} }
|
|
182
193
|
* ```
|
|
183
|
-
*
|
|
194
|
+
*
|
|
195
|
+
* @example
|
|
196
|
+
* ```typescript
|
|
197
|
+
* // Alternative: Pass user via context.user
|
|
198
|
+
* await analytics.track('feature_used', {
|
|
199
|
+
* featureName: 'export'
|
|
200
|
+
* }, {
|
|
201
|
+
* userId: 'user-123',
|
|
202
|
+
* context: {
|
|
203
|
+
* user: {
|
|
204
|
+
* email: 'user@example.com'
|
|
205
|
+
* },
|
|
206
|
+
* page: { path: '/dashboard' }
|
|
207
|
+
* }
|
|
208
|
+
* });
|
|
209
|
+
* ```
|
|
210
|
+
*
|
|
184
211
|
* @example
|
|
185
212
|
* ```typescript
|
|
186
|
-
* // In an Express.js route handler
|
|
213
|
+
* // In an Express.js route handler with user data
|
|
187
214
|
* app.post('/api/users', async (req, res) => {
|
|
188
215
|
* const user = await createUser(req.body);
|
|
189
|
-
*
|
|
190
|
-
* // Track user creation with
|
|
216
|
+
*
|
|
217
|
+
* // Track user creation with full user context
|
|
191
218
|
* await analytics.track('user_created', {
|
|
192
219
|
* userId: user.id,
|
|
193
220
|
* email: user.email,
|
|
194
221
|
* plan: user.plan
|
|
195
222
|
* }, {
|
|
196
223
|
* userId: user.id,
|
|
224
|
+
* user: {
|
|
225
|
+
* email: user.email,
|
|
226
|
+
* traits: {
|
|
227
|
+
* name: user.name,
|
|
228
|
+
* plan: user.plan,
|
|
229
|
+
* company: user.company
|
|
230
|
+
* }
|
|
231
|
+
* },
|
|
197
232
|
* context: {
|
|
198
233
|
* page: { path: req.path },
|
|
199
|
-
* device: { userAgent: req.headers['user-agent'] }
|
|
200
|
-
* ip: req.ip,
|
|
201
|
-
* server: { version: process.env.APP_VERSION }
|
|
234
|
+
* device: { userAgent: req.headers['user-agent'] }
|
|
202
235
|
* }
|
|
203
236
|
* });
|
|
204
|
-
*
|
|
237
|
+
*
|
|
205
238
|
* res.json(user);
|
|
206
239
|
* });
|
|
207
240
|
* ```
|
|
208
|
-
*
|
|
241
|
+
*
|
|
209
242
|
* @example
|
|
210
243
|
* ```typescript
|
|
211
244
|
* // Error handling in tracking
|
|
@@ -221,7 +254,8 @@ class u {
|
|
|
221
254
|
* }
|
|
222
255
|
* ```
|
|
223
256
|
*/
|
|
224
|
-
async track(e,
|
|
257
|
+
async track(e, r, t) {
|
|
258
|
+
var n;
|
|
225
259
|
if (!this.initialized) {
|
|
226
260
|
console.warn("[Analytics] Not initialized. Call initialize() first.");
|
|
227
261
|
return;
|
|
@@ -229,24 +263,25 @@ class u {
|
|
|
229
263
|
const a = {
|
|
230
264
|
action: e,
|
|
231
265
|
category: this.getCategoryFromEventName(e),
|
|
232
|
-
properties:
|
|
266
|
+
properties: r,
|
|
233
267
|
timestamp: Date.now(),
|
|
234
|
-
userId:
|
|
235
|
-
sessionId:
|
|
236
|
-
},
|
|
268
|
+
userId: t == null ? void 0 : t.userId,
|
|
269
|
+
sessionId: t == null ? void 0 : t.sessionId
|
|
270
|
+
}, d = {
|
|
237
271
|
...this.config.defaultContext,
|
|
238
|
-
...
|
|
239
|
-
|
|
272
|
+
...t == null ? void 0 : t.context,
|
|
273
|
+
user: (t == null ? void 0 : t.user) || ((n = t == null ? void 0 : t.context) == null ? void 0 : n.user)
|
|
274
|
+
}, l = this.providers.map(async (c) => {
|
|
240
275
|
try {
|
|
241
|
-
await
|
|
242
|
-
} catch (
|
|
276
|
+
await c.track(a, d);
|
|
277
|
+
} catch (f) {
|
|
243
278
|
console.error(
|
|
244
|
-
`[Analytics] Provider ${
|
|
245
|
-
|
|
279
|
+
`[Analytics] Provider ${c.name} failed to track event:`,
|
|
280
|
+
f
|
|
246
281
|
);
|
|
247
282
|
}
|
|
248
283
|
});
|
|
249
|
-
await Promise.all(
|
|
284
|
+
await Promise.all(l);
|
|
250
285
|
}
|
|
251
286
|
/**
|
|
252
287
|
* Tracks a page view event from the server side.
|
|
@@ -308,14 +343,14 @@ class u {
|
|
|
308
343
|
* }
|
|
309
344
|
* ```
|
|
310
345
|
*/
|
|
311
|
-
pageView(e,
|
|
346
|
+
pageView(e, r) {
|
|
312
347
|
if (!this.initialized) return;
|
|
313
|
-
const
|
|
348
|
+
const t = {
|
|
314
349
|
...this.config.defaultContext,
|
|
315
|
-
...
|
|
350
|
+
...r == null ? void 0 : r.context
|
|
316
351
|
};
|
|
317
352
|
for (const a of this.providers)
|
|
318
|
-
a.pageView(e,
|
|
353
|
+
a.pageView(e, t);
|
|
319
354
|
}
|
|
320
355
|
/**
|
|
321
356
|
* Tracks when a user leaves a page from the server side.
|
|
@@ -376,14 +411,14 @@ class u {
|
|
|
376
411
|
* }
|
|
377
412
|
* ```
|
|
378
413
|
*/
|
|
379
|
-
pageLeave(e,
|
|
414
|
+
pageLeave(e, r) {
|
|
380
415
|
if (!this.initialized) return;
|
|
381
|
-
const
|
|
416
|
+
const t = {
|
|
382
417
|
...this.config.defaultContext,
|
|
383
|
-
...
|
|
418
|
+
...r == null ? void 0 : r.context
|
|
384
419
|
};
|
|
385
420
|
for (const a of this.providers)
|
|
386
|
-
a.pageLeave && a.pageLeave(e,
|
|
421
|
+
a.pageLeave && a.pageLeave(e, t);
|
|
387
422
|
}
|
|
388
423
|
/**
|
|
389
424
|
* Shuts down all analytics providers and flushes pending events.
|
|
@@ -470,27 +505,25 @@ class u {
|
|
|
470
505
|
* ```
|
|
471
506
|
*/
|
|
472
507
|
async shutdown() {
|
|
473
|
-
const e = this.providers.map((
|
|
508
|
+
const e = this.providers.map((r) => "shutdown" in r && typeof r.shutdown == "function" ? r.shutdown() : Promise.resolve());
|
|
474
509
|
await Promise.all(e);
|
|
475
510
|
}
|
|
476
511
|
getCategoryFromEventName(e) {
|
|
477
|
-
const
|
|
478
|
-
return
|
|
512
|
+
const r = e.split("_");
|
|
513
|
+
return r.length > 1 && r[0] ? r[0] : "engagement";
|
|
479
514
|
}
|
|
480
515
|
}
|
|
481
|
-
function
|
|
516
|
+
function g(i) {
|
|
482
517
|
const e = {
|
|
483
|
-
providers:
|
|
484
|
-
debug:
|
|
485
|
-
enabled:
|
|
486
|
-
},
|
|
487
|
-
|
|
488
|
-
);
|
|
489
|
-
return t.initialize(), t;
|
|
518
|
+
providers: i.providers || [],
|
|
519
|
+
debug: i.debug,
|
|
520
|
+
enabled: i.enabled
|
|
521
|
+
}, r = new v(e);
|
|
522
|
+
return r.initialize(), r;
|
|
490
523
|
}
|
|
491
524
|
export {
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
525
|
+
z as BaseAnalyticsProvider,
|
|
526
|
+
w as PostHogServerProvider,
|
|
527
|
+
v as ServerAnalytics,
|
|
528
|
+
g as createServerAnalytics
|
|
496
529
|
};
|
package/package.json
CHANGED
package/readme.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @stacksee/analytics
|
|
2
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.
|
|
3
|
+
A highly typed, zero-dependency, provider-agnostic analytics library for TypeScript applications. Works seamlessly on both client and server sides with full type safety for your custom events.
|
|
4
4
|
|
|
5
5
|
## Table of Contents
|
|
6
6
|
|
|
@@ -39,9 +39,9 @@ A highly typed, provider-agnostic analytics library for TypeScript applications.
|
|
|
39
39
|
- 🎯 **Type-safe events**: Define your own strongly typed events with full IntelliSense support
|
|
40
40
|
- 🔌 **Plugin architecture**: Easily add analytics providers by passing them as plugins
|
|
41
41
|
- 🌐 **Universal**: Same API works on both client (browser) and server (Node.js)
|
|
42
|
-
-
|
|
43
|
-
- 🏗️ **Framework agnostic**: Use with any JavaScript framework
|
|
44
|
-
- 🌎 **Edge ready**: The server client is compatible with edge runtime (e.g. Cloudflare Workers)
|
|
42
|
+
- 👤 **User context**: Automatically attach user data (email, traits) to all events
|
|
43
|
+
- 🏗️ **Framework agnostic**: Use with any JavaScript framework. Can also be used only on the client.
|
|
44
|
+
- 🌎 **Edge ready**: The server client is compatible with edge runtime (e.g. Cloudflare Workers, Vercel Edge functions)
|
|
45
45
|
- 🔧 **Extensible**: Simple interface to add new providers
|
|
46
46
|
|
|
47
47
|
## Installation
|
|
@@ -126,12 +126,19 @@ analytics.track('user_signed_up', {
|
|
|
126
126
|
// analytics.track('wrong_event', {}); // ❌ Error: Argument of type '"wrong_event"' is not assignable
|
|
127
127
|
// analytics.track('user_signed_up', { wrongProp: 'value' }); // ❌ Error: Object literal may only specify known properties
|
|
128
128
|
|
|
129
|
-
// Identify users
|
|
129
|
+
// Identify users - user context is automatically included in all subsequent events
|
|
130
130
|
analytics.identify('user-123', {
|
|
131
131
|
email: 'user@example.com',
|
|
132
132
|
name: 'John Doe',
|
|
133
133
|
plan: 'pro'
|
|
134
134
|
});
|
|
135
|
+
|
|
136
|
+
// Now all tracked events automatically include user context
|
|
137
|
+
analytics.track('feature_used', {
|
|
138
|
+
featureName: 'export-data',
|
|
139
|
+
userId: 'user-123'
|
|
140
|
+
});
|
|
141
|
+
// Providers receive: context.user = { userId: 'user-123', email: 'user@example.com', traits: {...} }
|
|
135
142
|
```
|
|
136
143
|
|
|
137
144
|
### 3. Server-Side Usage
|
|
@@ -155,24 +162,201 @@ const analytics = createServerAnalytics<AppEvents>({
|
|
|
155
162
|
enabled: true
|
|
156
163
|
});
|
|
157
164
|
|
|
158
|
-
// Track events - now returns a Promise with full type safety
|
|
165
|
+
// Track events with user context - now returns a Promise with full type safety
|
|
159
166
|
await analytics.track('feature_used', {
|
|
160
167
|
featureName: 'export-data',
|
|
161
168
|
userId: 'user-123',
|
|
162
169
|
duration: 1500
|
|
163
170
|
}, {
|
|
164
171
|
userId: 'user-123',
|
|
172
|
+
user: {
|
|
173
|
+
email: 'user@example.com',
|
|
174
|
+
traits: {
|
|
175
|
+
plan: 'pro',
|
|
176
|
+
company: 'Acme Corp'
|
|
177
|
+
}
|
|
178
|
+
},
|
|
165
179
|
context: {
|
|
166
180
|
page: {
|
|
167
181
|
path: '/api/export',
|
|
168
182
|
}
|
|
169
183
|
}
|
|
170
184
|
});
|
|
185
|
+
// Providers receive: context.user = { userId: 'user-123', email: 'user@example.com', traits: {...} }
|
|
171
186
|
|
|
172
187
|
// Important: Always call shutdown when done, some providers such as Posthog require flushing events.
|
|
173
188
|
await analytics.shutdown();
|
|
174
189
|
```
|
|
175
190
|
|
|
191
|
+
## User Context
|
|
192
|
+
|
|
193
|
+
The library automatically manages user context, making it easy to include user data (email, traits) in all your analytics events. This is especially useful for providers like Loops or Intercom that require user identifiers.
|
|
194
|
+
|
|
195
|
+
### How It Works
|
|
196
|
+
|
|
197
|
+
**Client-Side (Stateful):**
|
|
198
|
+
```typescript
|
|
199
|
+
// 1. Identify the user once (typically after login)
|
|
200
|
+
analytics.identify('user-123', {
|
|
201
|
+
email: 'user@example.com',
|
|
202
|
+
name: 'John Doe',
|
|
203
|
+
plan: 'pro',
|
|
204
|
+
company: 'Acme Corp'
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
// 2. Track events - user context is automatically included
|
|
208
|
+
analytics.track('button_clicked', { buttonId: 'checkout' });
|
|
209
|
+
|
|
210
|
+
// Behind the scenes, providers receive:
|
|
211
|
+
// {
|
|
212
|
+
// event: { action: 'button_clicked', ... },
|
|
213
|
+
// context: {
|
|
214
|
+
// user: {
|
|
215
|
+
// userId: 'user-123',
|
|
216
|
+
// email: 'user@example.com',
|
|
217
|
+
// traits: { email: '...', name: '...', plan: '...', company: '...' }
|
|
218
|
+
// }
|
|
219
|
+
// }
|
|
220
|
+
// }
|
|
221
|
+
|
|
222
|
+
// 3. Reset on logout to clear user context
|
|
223
|
+
analytics.reset();
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
**Server-Side (Stateless):**
|
|
227
|
+
```typescript
|
|
228
|
+
// Pass user context with each track call
|
|
229
|
+
await analytics.track('api_request', {
|
|
230
|
+
endpoint: '/users',
|
|
231
|
+
method: 'POST'
|
|
232
|
+
}, {
|
|
233
|
+
userId: 'user-123',
|
|
234
|
+
user: {
|
|
235
|
+
email: 'user@example.com',
|
|
236
|
+
traits: {
|
|
237
|
+
plan: 'pro',
|
|
238
|
+
company: 'Acme Corp'
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
// Alternatively, pass via context.user
|
|
244
|
+
await analytics.track('api_request', { ... }, {
|
|
245
|
+
userId: 'user-123',
|
|
246
|
+
context: {
|
|
247
|
+
user: {
|
|
248
|
+
email: 'user@example.com'
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
});
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
### Using User Context in Custom Providers
|
|
255
|
+
|
|
256
|
+
When building custom providers, you can access user context from the `EventContext`:
|
|
257
|
+
|
|
258
|
+
```typescript
|
|
259
|
+
export class LoopsProvider extends BaseAnalyticsProvider {
|
|
260
|
+
name = 'Loops';
|
|
261
|
+
|
|
262
|
+
async track(event: BaseEvent, context?: EventContext): Promise<void> {
|
|
263
|
+
// Access user data from context
|
|
264
|
+
const email = context?.user?.email;
|
|
265
|
+
const userId = context?.user?.userId;
|
|
266
|
+
const traits = context?.user?.traits;
|
|
267
|
+
|
|
268
|
+
// Loops requires either email or userId
|
|
269
|
+
if (!email && !userId) {
|
|
270
|
+
this.log('Skipping event - Loops requires email or userId');
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
await this.loops.sendEvent({
|
|
275
|
+
...(email && { email }),
|
|
276
|
+
...(userId && { userId }),
|
|
277
|
+
eventName: event.action,
|
|
278
|
+
eventProperties: event.properties,
|
|
279
|
+
// Optionally include all user traits
|
|
280
|
+
contactProperties: traits,
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
### Security & Privacy
|
|
287
|
+
|
|
288
|
+
User context is handled securely:
|
|
289
|
+
|
|
290
|
+
- ✅ **Memory-only storage** - No localStorage, cookies, or persistence
|
|
291
|
+
- ✅ **Session-scoped** - Cleared on `reset()` (logout)
|
|
292
|
+
- ✅ **Provider-controlled** - Only sent to providers you configure
|
|
293
|
+
- ✅ **No cross-session leaks** - Fresh state on each page load
|
|
294
|
+
|
|
295
|
+
### Type-Safe User Traits
|
|
296
|
+
|
|
297
|
+
You can define a custom interface for your user traits to get full type safety:
|
|
298
|
+
|
|
299
|
+
```typescript
|
|
300
|
+
// Define your user traits interface
|
|
301
|
+
interface UserTraits {
|
|
302
|
+
email: string;
|
|
303
|
+
name: string;
|
|
304
|
+
plan: 'free' | 'pro' | 'enterprise';
|
|
305
|
+
company?: string;
|
|
306
|
+
role?: 'admin' | 'user' | 'viewer';
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// Client-side with typed traits
|
|
310
|
+
const analytics = createClientAnalytics<typeof AppEvents, UserTraits>({
|
|
311
|
+
providers: [/* ... */]
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
// Now identify() and traits are fully typed!
|
|
315
|
+
analytics.identify('user-123', {
|
|
316
|
+
email: 'user@example.com',
|
|
317
|
+
name: 'John Doe',
|
|
318
|
+
plan: 'pro', // ✅ Autocomplete works!
|
|
319
|
+
company: 'Acme Corp',
|
|
320
|
+
role: 'admin'
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
// TypeScript will error on invalid trait values
|
|
324
|
+
analytics.identify('user-123', {
|
|
325
|
+
plan: 'invalid' // ❌ Error: Type '"invalid"' is not assignable to type 'free' | 'pro' | 'enterprise'
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
// Server-side with typed traits
|
|
329
|
+
const serverAnalytics = createServerAnalytics<typeof AppEvents, UserTraits>({
|
|
330
|
+
providers: [/* ... */]
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
await serverAnalytics.track('event', {}, {
|
|
334
|
+
user: {
|
|
335
|
+
email: 'user@example.com',
|
|
336
|
+
plan: 'pro', // ✅ Fully typed!
|
|
337
|
+
traits: {
|
|
338
|
+
company: 'Acme Corp'
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
});
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
**Benefits:**
|
|
345
|
+
- ✅ Full IntelliSense/autocomplete for user traits
|
|
346
|
+
- ✅ Compile-time type checking prevents typos
|
|
347
|
+
- ✅ Self-documenting code
|
|
348
|
+
- ✅ Refactoring safety
|
|
349
|
+
|
|
350
|
+
### Client vs Server Differences
|
|
351
|
+
|
|
352
|
+
| Feature | Client (Browser) | Server (Node.js) |
|
|
353
|
+
|---------|------------------|------------------|
|
|
354
|
+
| **State Management** | Stateful - persists after `identify()` | Stateless - pass per request |
|
|
355
|
+
| **Usage Pattern** | Call `identify()` once, track many times | Pass `user` option with each `track()` |
|
|
356
|
+
| **Reset** | Call `reset()` on logout | No reset needed (stateless) |
|
|
357
|
+
| **Use Case** | Single user per session | Multiple users per instance |
|
|
358
|
+
| **Type Safety** | `createClientAnalytics<Events, Traits>` | `createServerAnalytics<Events, Traits>` |
|
|
359
|
+
|
|
176
360
|
### Async Tracking: When to await vs fire-and-forget
|
|
177
361
|
|
|
178
362
|
The `track()` method now returns a `Promise<void>`, giving you control over how to handle event tracking:
|
|
@@ -567,7 +751,7 @@ const analytics = await createClientAnalytics<typeof AppEvents>({
|
|
|
567
751
|
**Important**: To avoid bundling Node.js dependencies in your client code, always use the environment-specific provider imports:
|
|
568
752
|
|
|
569
753
|
- **Client-side**: `@stacksee/analytics/providers/client` - Only includes browser-compatible providers
|
|
570
|
-
- **Server-side**: `@stacksee/analytics/providers/server` - Only includes Node.js providers
|
|
754
|
+
- **Server-side**: `@stacksee/analytics/providers/server` - Only includes Node.js providers
|
|
571
755
|
- **Both**: `@stacksee/analytics/providers` - Includes all providers (may cause bundling issues in browsers)
|
|
572
756
|
|
|
573
757
|
Some analytics libraries are designed to work only in specific environments. For example:
|
|
@@ -799,11 +983,11 @@ const analytics = createClientAnalytics<typeof AppEvents>({
|
|
|
799
983
|
```
|
|
800
984
|
|
|
801
985
|
#### `BrowserAnalytics<TEventMap>`
|
|
802
|
-
- `track(eventName, properties): Promise<void>` - Track an event with type-safe event names and properties
|
|
803
|
-
- `identify(userId, traits)` - Identify a user
|
|
986
|
+
- `track(eventName, properties): Promise<void>` - Track an event with type-safe event names and properties. User context from `identify()` is automatically included.
|
|
987
|
+
- `identify(userId, traits)` - Identify a user and store their traits. All subsequent `track()` calls will include this user context.
|
|
804
988
|
- `pageView(properties)` - Track a page view
|
|
805
989
|
- `pageLeave(properties)` - Track a page leave event
|
|
806
|
-
- `reset()` - Reset user session
|
|
990
|
+
- `reset()` - Reset user session, clearing userId and user traits
|
|
807
991
|
- `updateContext(context)` - Update event context
|
|
808
992
|
|
|
809
993
|
### Server API
|
|
@@ -825,8 +1009,12 @@ const analytics = createServerAnalytics<AppEvents>({
|
|
|
825
1009
|
```
|
|
826
1010
|
|
|
827
1011
|
#### `ServerAnalytics<TEventMap>`
|
|
828
|
-
- `track(eventName, properties, options): Promise<void>` - Track an event with type-safe event names and properties
|
|
829
|
-
- `
|
|
1012
|
+
- `track(eventName, properties, options): Promise<void>` - Track an event with type-safe event names and properties. Pass user context via `options.user` or `options.context.user`.
|
|
1013
|
+
- `options.userId` - User ID for this event
|
|
1014
|
+
- `options.sessionId` - Session ID for this event
|
|
1015
|
+
- `options.user` - User context (email, traits) for this event
|
|
1016
|
+
- `options.context` - Additional event context (page, device, etc.)
|
|
1017
|
+
- `identify(userId, traits)` - Identify a user (sends to providers but doesn't persist on server)
|
|
830
1018
|
- `pageView(properties, options)` - Track a page view
|
|
831
1019
|
- `pageLeave(properties, options)` - Track a page leave event
|
|
832
1020
|
- `shutdown()` - Flush pending events and cleanup
|