@stacksee/analytics 0.4.2 → 0.4.3

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.
@@ -4,20 +4,423 @@ export declare class ServerAnalytics<TEventMap extends DefaultEventMap = Default
4
4
  private providers;
5
5
  private config;
6
6
  private initialized;
7
+ /**
8
+ * Creates a new ServerAnalytics instance for server-side event tracking.
9
+ *
10
+ * The server analytics instance is designed for Node.js environments including
11
+ * long-running servers, serverless functions, and edge computing environments.
12
+ *
13
+ * @param config Analytics configuration including providers and default context
14
+ * @param config.providers Array of analytics provider instances (e.g., PostHogServerProvider)
15
+ * @param config.defaultContext Optional default context to include with all events
16
+ *
17
+ * @example
18
+ * ```typescript
19
+ * import { ServerAnalytics } from '@stacksee/analytics/server';
20
+ * import { PostHogServerProvider } from '@stacksee/analytics/providers/posthog';
21
+ *
22
+ * const analytics = new ServerAnalytics({
23
+ * providers: [
24
+ * new PostHogServerProvider({
25
+ * apiKey: process.env.POSTHOG_API_KEY,
26
+ * host: process.env.POSTHOG_HOST
27
+ * })
28
+ * ],
29
+ * defaultContext: {
30
+ * app: { version: '1.0.0', environment: 'production' }
31
+ * }
32
+ * });
33
+ *
34
+ * analytics.initialize();
35
+ * ```
36
+ */
7
37
  constructor(config: AnalyticsConfig);
38
+ /**
39
+ * Initializes all analytics providers.
40
+ *
41
+ * This method must be called before tracking events. It initializes all configured
42
+ * providers synchronously. Unlike the browser version, server initialization is
43
+ * typically synchronous as providers don't need to load external scripts.
44
+ *
45
+ * The method is safe to call multiple times and will not re-initialize if already done.
46
+ *
47
+ * @example
48
+ * ```typescript
49
+ * const analytics = new ServerAnalytics({ providers: [] });
50
+ *
51
+ * // Initialize before tracking events
52
+ * analytics.initialize();
53
+ *
54
+ * // Now ready to track events
55
+ * await analytics.track('api_request', { endpoint: '/users' });
56
+ * ```
57
+ *
58
+ * @example
59
+ * ```typescript
60
+ * // In a serverless function
61
+ * export async function handler(req, res) {
62
+ * const analytics = new ServerAnalytics({ providers: [] });
63
+ * analytics.initialize(); // Quick synchronous initialization
64
+ *
65
+ * await analytics.track('function_invoked', {
66
+ * path: req.path,
67
+ * method: req.method
68
+ * });
69
+ *
70
+ * await analytics.shutdown(); // Important for serverless
71
+ * }
72
+ * ```
73
+ */
8
74
  initialize(): void;
75
+ /**
76
+ * Identifies a user with optional traits.
77
+ *
78
+ * Associates subsequent events with the specified user ID and optionally
79
+ * sets user properties. This method is typically called when processing
80
+ * authentication or when you have user context available on the server.
81
+ *
82
+ * @param userId Unique identifier for the user (e.g., database ID, email)
83
+ * @param traits Optional user properties and characteristics
84
+ *
85
+ * @example
86
+ * ```typescript
87
+ * // Basic user identification
88
+ * analytics.identify('user-123');
89
+ * ```
90
+ *
91
+ * @example
92
+ * ```typescript
93
+ * // Identify with user traits from database
94
+ * analytics.identify('user-123', {
95
+ * email: 'john@example.com',
96
+ * name: 'John Doe',
97
+ * plan: 'enterprise',
98
+ * company: 'Acme Corp',
99
+ * createdAt: '2024-01-15T10:00:00Z',
100
+ * lastSeenAt: new Date().toISOString()
101
+ * });
102
+ * ```
103
+ *
104
+ * @example
105
+ * ```typescript
106
+ * // In an API authentication middleware
107
+ * async function authMiddleware(req, res, next) {
108
+ * const user = await getUserFromToken(req.headers.authorization);
109
+ *
110
+ * analytics.identify(user.id, {
111
+ * email: user.email,
112
+ * role: user.role,
113
+ * organization: user.organization
114
+ * });
115
+ *
116
+ * req.user = user;
117
+ * next();
118
+ * }
119
+ * ```
120
+ */
9
121
  identify(userId: string, traits?: Record<string, unknown>): void;
122
+ /**
123
+ * Tracks a custom event with properties and optional context.
124
+ *
125
+ * This is the main method for tracking business events on the server side.
126
+ * The method sends the event to all configured providers and waits for completion.
127
+ * Failed providers don't prevent others from succeeding.
128
+ *
129
+ * Server-side tracking typically includes additional context like IP addresses,
130
+ * user agents, and server-specific metadata that isn't available on the client.
131
+ *
132
+ * @param eventName Name of the event to track (must match your event definitions)
133
+ * @param properties Event-specific properties and data
134
+ * @param options Optional configuration including user ID, session ID, and context
135
+ * @param options.userId User ID to associate with this event
136
+ * @param options.sessionId Session ID to associate with this event
137
+ * @param options.context Additional context for this event
138
+ * @returns Promise that resolves when tracking is complete for all providers
139
+ *
140
+ * @example
141
+ * ```typescript
142
+ * // Basic event tracking
143
+ * await analytics.track('api_request', {
144
+ * endpoint: '/api/users',
145
+ * method: 'GET',
146
+ * responseTime: 150,
147
+ * statusCode: 200
148
+ * });
149
+ * ```
150
+ *
151
+ * @example
152
+ * ```typescript
153
+ * // Track with user context
154
+ * await analytics.track('purchase_completed', {
155
+ * orderId: 'order-123',
156
+ * amount: 99.99,
157
+ * currency: 'USD',
158
+ * itemCount: 3
159
+ * }, {
160
+ * userId: 'user-456',
161
+ * sessionId: 'session-789',
162
+ * context: {
163
+ * page: { path: '/checkout/complete' },
164
+ * device: { userAgent: req.headers['user-agent'] },
165
+ * ip: req.ip
166
+ * }
167
+ * });
168
+ * ```
169
+ *
170
+ * @example
171
+ * ```typescript
172
+ * // In an Express.js route handler
173
+ * app.post('/api/users', async (req, res) => {
174
+ * const user = await createUser(req.body);
175
+ *
176
+ * // Track user creation with server context
177
+ * await analytics.track('user_created', {
178
+ * userId: user.id,
179
+ * email: user.email,
180
+ * plan: user.plan
181
+ * }, {
182
+ * userId: user.id,
183
+ * context: {
184
+ * page: { path: req.path },
185
+ * device: { userAgent: req.headers['user-agent'] },
186
+ * ip: req.ip,
187
+ * server: { version: process.env.APP_VERSION }
188
+ * }
189
+ * });
190
+ *
191
+ * res.json(user);
192
+ * });
193
+ * ```
194
+ *
195
+ * @example
196
+ * ```typescript
197
+ * // Error handling in tracking
198
+ * try {
199
+ * await analytics.track('payment_processed', {
200
+ * amount: 100,
201
+ * currency: 'USD'
202
+ * });
203
+ * } catch (error) {
204
+ * // This only catches initialization errors
205
+ * // Individual provider failures are logged but don't throw
206
+ * console.error('Failed to track event:', error);
207
+ * }
208
+ * ```
209
+ */
10
210
  track<TEventName extends keyof TEventMap & string>(eventName: TEventName, properties: TEventMap[TEventName], options?: {
11
211
  userId?: string;
12
212
  sessionId?: string;
13
213
  context?: EventContext;
14
214
  }): Promise<void>;
215
+ /**
216
+ * Tracks a page view event from the server side.
217
+ *
218
+ * Server-side page view tracking is useful for server-rendered applications,
219
+ * SSR frameworks, or when you want to ensure page views are tracked even
220
+ * if client-side JavaScript fails.
221
+ *
222
+ * @param properties Optional properties to include with the page view
223
+ * @param options Optional configuration including context
224
+ * @param options.context Additional context for this page view
225
+ *
226
+ * @example
227
+ * ```typescript
228
+ * // Basic server-side page view
229
+ * analytics.pageView();
230
+ * ```
231
+ *
232
+ * @example
233
+ * ```typescript
234
+ * // Page view with server context
235
+ * analytics.pageView({
236
+ * loadTime: 250,
237
+ * template: 'product-detail',
238
+ * ssr: true
239
+ * }, {
240
+ * context: {
241
+ * page: {
242
+ * path: '/products/widget-123',
243
+ * title: 'Amazing Widget - Product Details'
244
+ * },
245
+ * device: {
246
+ * userAgent: req.headers['user-agent']
247
+ * },
248
+ * server: {
249
+ * renderTime: 45,
250
+ * cacheHit: false
251
+ * }
252
+ * }
253
+ * });
254
+ * ```
255
+ *
256
+ * @example
257
+ * ```typescript
258
+ * // In a Next.js API route or middleware
259
+ * export async function middleware(req) {
260
+ * if (req.nextUrl.pathname.startsWith('/product/')) {
261
+ * analytics.pageView({
262
+ * category: 'product',
263
+ * productId: req.nextUrl.pathname.split('/').pop()
264
+ * }, {
265
+ * context: {
266
+ * page: { path: req.nextUrl.pathname },
267
+ * device: { userAgent: req.headers.get('user-agent') },
268
+ * referrer: req.headers.get('referer')
269
+ * }
270
+ * });
271
+ * }
272
+ * }
273
+ * ```
274
+ */
15
275
  pageView(properties?: Record<string, unknown>, options?: {
16
276
  context?: EventContext;
17
277
  }): void;
278
+ /**
279
+ * Tracks when a user leaves a page from the server side.
280
+ *
281
+ * Server-side page leave tracking is less common than client-side but can be
282
+ * useful in certain scenarios like tracking session timeouts, or when combined
283
+ * with server-side session management.
284
+ *
285
+ * Note: Not all analytics providers support page leave events. The method
286
+ * will only call providers that implement the pageLeave method.
287
+ *
288
+ * @param properties Optional properties to include with the page leave event
289
+ * @param options Optional configuration including context
290
+ * @param options.context Additional context for this page leave
291
+ *
292
+ * @example
293
+ * ```typescript
294
+ * // Basic page leave tracking
295
+ * analytics.pageLeave();
296
+ * ```
297
+ *
298
+ * @example
299
+ * ```typescript
300
+ * // Page leave with session context
301
+ * analytics.pageLeave({
302
+ * sessionDuration: 45000, // 45 seconds
303
+ * pagesViewed: 3,
304
+ * exitReason: 'session_timeout'
305
+ * }, {
306
+ * context: {
307
+ * session: {
308
+ * id: 'session-123',
309
+ * startTime: sessionStartTime,
310
+ * endTime: Date.now()
311
+ * },
312
+ * server: {
313
+ * reason: 'inactivity_timeout'
314
+ * }
315
+ * }
316
+ * });
317
+ * ```
318
+ *
319
+ * @example
320
+ * ```typescript
321
+ * // In a session cleanup job
322
+ * async function cleanupExpiredSessions() {
323
+ * const expiredSessions = await getExpiredSessions();
324
+ *
325
+ * for (const session of expiredSessions) {
326
+ * analytics.pageLeave({
327
+ * sessionId: session.id,
328
+ * duration: session.duration,
329
+ * reason: 'expired'
330
+ * });
331
+ *
332
+ * await removeSession(session.id);
333
+ * }
334
+ * }
335
+ * ```
336
+ */
18
337
  pageLeave(properties?: Record<string, unknown>, options?: {
19
338
  context?: EventContext;
20
339
  }): void;
340
+ /**
341
+ * Shuts down all analytics providers and flushes pending events.
342
+ *
343
+ * This method is crucial for server environments, especially serverless functions,
344
+ * as it ensures all events are sent before the process terminates. Some providers
345
+ * batch events and need an explicit flush to send them.
346
+ *
347
+ * Always call this method before your server shuts down or before a serverless
348
+ * function completes execution.
349
+ *
350
+ * @returns Promise that resolves when all providers have been shut down
351
+ *
352
+ * @example
353
+ * ```typescript
354
+ * // Basic shutdown
355
+ * await analytics.shutdown();
356
+ * ```
357
+ *
358
+ * @example
359
+ * ```typescript
360
+ * // In a serverless function
361
+ * export async function handler(event, context) {
362
+ * const analytics = new ServerAnalytics({ providers: [] });
363
+ * analytics.initialize();
364
+ *
365
+ * try {
366
+ * // Process the event
367
+ * await processEvent(event);
368
+ *
369
+ * // Track completion
370
+ * await analytics.track('function_completed', {
371
+ * duration: Date.now() - startTime,
372
+ * success: true
373
+ * });
374
+ * } catch (error) {
375
+ * await analytics.track('function_failed', {
376
+ * error: error.message,
377
+ * duration: Date.now() - startTime
378
+ * });
379
+ * } finally {
380
+ * // Always shutdown to flush events
381
+ * await analytics.shutdown();
382
+ * }
383
+ *
384
+ * return { statusCode: 200 };
385
+ * }
386
+ * ```
387
+ *
388
+ * @example
389
+ * ```typescript
390
+ * // In an Express.js server
391
+ * const server = app.listen(3000);
392
+ *
393
+ * // Graceful shutdown
394
+ * process.on('SIGTERM', async () => {
395
+ * console.log('Shutting down gracefully...');
396
+ *
397
+ * server.close(async () => {
398
+ * // Flush analytics events before exit
399
+ * await analytics.shutdown();
400
+ * process.exit(0);
401
+ * });
402
+ * });
403
+ * ```
404
+ *
405
+ * @example
406
+ * ```typescript
407
+ * // With Vercel's waitUntil
408
+ * import { waitUntil } from '@vercel/functions';
409
+ *
410
+ * export default async function handler(req, res) {
411
+ * // Process request
412
+ * const result = await processRequest(req);
413
+ *
414
+ * // Track in background without blocking response
415
+ * waitUntil(
416
+ * analytics.track('api_request', { endpoint: req.url })
417
+ * .then(() => analytics.shutdown())
418
+ * );
419
+ *
420
+ * return res.json(result);
421
+ * }
422
+ * ```
423
+ */
21
424
  shutdown(): Promise<void>;
22
425
  private getCategoryFromEventName;
23
426
  }
@@ -1,6 +1,5 @@
1
1
  export type { EventCategory, BaseEvent, EventContext, AnalyticsProvider, AnalyticsConfig, } from './core/events/types.js';
2
2
  export type { CreateEventDefinition, ExtractEventNames, ExtractEventPropertiesFromCollection, EventCollection, AnyEventName, AnyEventProperties, EventMapFromCollection, } from './core/events/index.js';
3
3
  export { createAnalytics as createClientAnalytics, getAnalytics, track as trackClient, identify as identifyClient, pageView as pageViewClient, pageLeave as pageLeaveClient, reset as resetClient, type ClientAnalyticsConfig, } from './client.js';
4
- export { createServerAnalytics, ServerAnalytics, type ServerAnalyticsConfig, } from './server.js';
5
- export { BaseAnalyticsProvider, PostHogClientProvider, PostHogServerProvider, type PostHogConfig, type PostHogOptions, } from './providers/index.js';
4
+ export { BaseAnalyticsProvider, PostHogClientProvider, type PostHogConfig, } from './providers/client.js';
6
5
  export { BrowserAnalytics } from './adapters/client/browser-analytics.js';
@@ -0,0 +1,3 @@
1
+ export { BaseAnalyticsProvider } from './base.provider.js';
2
+ export { PostHogClientProvider } from './posthog/client.js';
3
+ export type { PostHogConfig } from 'posthog-js';
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stacksee/analytics",
3
- "version": "0.4.2",
3
+ "version": "0.4.3",
4
4
  "description": "A highly typed, provider-agnostic analytics library for TypeScript applications",
5
5
  "type": "module",
6
6
  "exports": {
@@ -1,184 +0,0 @@
1
- var c = Object.defineProperty;
2
- var u = (t, e, i) => e in t ? c(t, e, { enumerable: !0, configurable: !0, writable: !0, value: i }) : t[e] = i;
3
- var s = (t, e, i) => u(t, typeof e != "symbol" ? e + "" : e, i);
4
- import { i as h } from "./environment-Bnc8FqHv.js";
5
- class p {
6
- constructor(e) {
7
- s(this, "providers", []);
8
- s(this, "context", {});
9
- s(this, "userId");
10
- s(this, "sessionId");
11
- s(this, "initialized", !1);
12
- s(this, "initializePromise");
13
- this.providers = e.providers, e.defaultContext && (this.context = { ...e.defaultContext }), this.sessionId = this.generateSessionId();
14
- }
15
- async initialize() {
16
- if (h() && !this.initialized)
17
- return this.initializePromise ? this.initializePromise : (this.initializePromise = this._doInitialize(), this.initializePromise);
18
- }
19
- async _doInitialize() {
20
- const e = this.providers.map(
21
- (i) => i.initialize()
22
- );
23
- await Promise.all(e), this.initialized = !0, this.updateContext({
24
- page: {
25
- path: window.location.pathname,
26
- title: document.title,
27
- referrer: document.referrer
28
- },
29
- device: {
30
- type: this.getDeviceType(),
31
- os: this.getOS(),
32
- browser: this.getBrowser()
33
- }
34
- });
35
- }
36
- async ensureInitialized() {
37
- !this.initialized && !this.initializePromise ? await this.initialize() : this.initializePromise && await this.initializePromise;
38
- }
39
- identify(e, i) {
40
- this.userId = e, this.ensureInitialized().catch((r) => {
41
- console.error("[Analytics] Failed to initialize during identify:", r);
42
- });
43
- for (const r of this.providers)
44
- r.identify(e, i);
45
- }
46
- async track(e, i) {
47
- await this.ensureInitialized();
48
- const r = {
49
- action: e,
50
- category: this.getCategoryFromEventName(e),
51
- properties: i,
52
- timestamp: Date.now(),
53
- userId: this.userId,
54
- sessionId: this.sessionId
55
- }, o = this.providers.map(async (d) => {
56
- try {
57
- await d.track(r, this.context);
58
- } catch (l) {
59
- console.error(
60
- `[Analytics] Provider ${d.name} failed to track event:`,
61
- l
62
- );
63
- }
64
- });
65
- await Promise.all(o);
66
- }
67
- pageView(e) {
68
- this.ensureInitialized().catch((i) => {
69
- console.error("[Analytics] Failed to initialize during pageView:", i);
70
- }), this.updateContext({
71
- page: {
72
- path: window.location.pathname,
73
- title: document.title,
74
- referrer: document.referrer
75
- }
76
- });
77
- for (const i of this.providers)
78
- i.pageView(e, this.context);
79
- }
80
- pageLeave(e) {
81
- this.ensureInitialized().catch((i) => {
82
- console.error("[Analytics] Failed to initialize during pageLeave:", i);
83
- });
84
- for (const i of this.providers)
85
- i.pageLeave && i.pageLeave(e, this.context);
86
- }
87
- reset() {
88
- this.userId = void 0, this.sessionId = this.generateSessionId();
89
- for (const e of this.providers)
90
- e.reset();
91
- }
92
- updateContext(e) {
93
- var i, r, o;
94
- this.context = {
95
- ...this.context,
96
- ...e,
97
- page: e.page ? {
98
- path: e.page.path || ((i = this.context.page) == null ? void 0 : i.path) || window.location.pathname,
99
- title: e.page.title || ((r = this.context.page) == null ? void 0 : r.title),
100
- referrer: e.page.referrer || ((o = this.context.page) == null ? void 0 : o.referrer)
101
- } : this.context.page,
102
- device: {
103
- ...this.context.device,
104
- ...e.device
105
- },
106
- utm: {
107
- ...this.context.utm,
108
- ...e.utm
109
- }
110
- };
111
- }
112
- getCategoryFromEventName(e) {
113
- const i = e.split("_");
114
- return i.length > 1 && i[0] ? i[0] : "engagement";
115
- }
116
- generateSessionId() {
117
- return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
118
- }
119
- getDeviceType() {
120
- const e = navigator.userAgent;
121
- return /tablet|ipad|playbook|silk/i.test(e) ? "tablet" : /mobile|iphone|ipod|android|blackberry|opera|mini|windows\sce|palm|smartphone|iemobile/i.test(
122
- e
123
- ) ? "mobile" : "desktop";
124
- }
125
- getOS() {
126
- const e = navigator.userAgent;
127
- return e.indexOf("Win") !== -1 ? "Windows" : e.indexOf("Mac") !== -1 ? "macOS" : e.indexOf("Linux") !== -1 ? "Linux" : e.indexOf("Android") !== -1 ? "Android" : e.indexOf("iOS") !== -1 ? "iOS" : "Unknown";
128
- }
129
- getBrowser() {
130
- const e = navigator.userAgent;
131
- return e.indexOf("Chrome") !== -1 ? "Chrome" : e.indexOf("Safari") !== -1 ? "Safari" : e.indexOf("Firefox") !== -1 ? "Firefox" : e.indexOf("Edge") !== -1 ? "Edge" : "Unknown";
132
- }
133
- }
134
- let n = null;
135
- function m(t) {
136
- if (n)
137
- return console.warn("[Analytics] Already initialized"), n;
138
- const e = {
139
- providers: t.providers || [],
140
- debug: t.debug,
141
- enabled: t.enabled
142
- };
143
- return n = new p(
144
- e
145
- ), n.initialize().catch((i) => {
146
- console.error("[Analytics] Failed to initialize:", i);
147
- }), n;
148
- }
149
- function a() {
150
- if (!n)
151
- throw new Error(
152
- "[Analytics] Not initialized. Call createAnalytics() first."
153
- );
154
- return n;
155
- }
156
- function v(t, e) {
157
- return a().track(t, e);
158
- }
159
- function y(t, e) {
160
- a().identify(t, e);
161
- }
162
- function w(t) {
163
- a().pageView(t);
164
- }
165
- function z(t) {
166
- a().pageLeave(t);
167
- }
168
- function x() {
169
- a().reset();
170
- }
171
- function A() {
172
- n = null;
173
- }
174
- export {
175
- p as B,
176
- z as a,
177
- A as b,
178
- m as c,
179
- a as g,
180
- y as i,
181
- w as p,
182
- x as r,
183
- v as t
184
- };