@appmachina/node 0.0.1 → 2.2.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/README.md +427 -0
- package/dist/express-CeETK4sI.js +123 -0
- package/dist/express-CeETK4sI.js.map +1 -0
- package/dist/express-D5w2Pl15.d.ts +377 -0
- package/dist/express-D5w2Pl15.d.ts.map +1 -0
- package/dist/express.d.ts +3 -0
- package/dist/express.js +3 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +550 -0
- package/dist/index.js.map +1 -0
- package/dist/nextjs.d.ts +59 -0
- package/dist/nextjs.d.ts.map +1 -0
- package/dist/nextjs.js +112 -0
- package/dist/nextjs.js.map +1 -0
- package/dist/persistence-ChBsyTkr.d.ts +22 -0
- package/dist/persistence-ChBsyTkr.d.ts.map +1 -0
- package/dist/persistence-D_zFNklE.js +47 -0
- package/dist/persistence-D_zFNklE.js.map +1 -0
- package/dist/persistence.d.ts +2 -0
- package/dist/persistence.js +3 -0
- package/package.json +52 -5
- package/index.js +0 -1
package/README.md
ADDED
|
@@ -0,0 +1,427 @@
|
|
|
1
|
+
# AppMachina Node.js SDK
|
|
2
|
+
|
|
3
|
+
`@appmachina/node` is the AppMachina analytics SDK for server-side Node.js applications. It provides event tracking, screen tracking, user property management, consent management, Express middleware for automatic HTTP request tracking, and Next.js integration with `AsyncLocalStorage` for request context propagation.
|
|
4
|
+
|
|
5
|
+
Unlike client-side SDKs that store a user ID per session, the Node.js SDK requires a `distinctId` on every call. This is the correct pattern for servers handling concurrent requests from many users.
|
|
6
|
+
|
|
7
|
+
## Requirements
|
|
8
|
+
|
|
9
|
+
- Node.js 18+
|
|
10
|
+
|
|
11
|
+
## Installation
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npm install @appmachina/node
|
|
15
|
+
# or
|
|
16
|
+
yarn add @appmachina/node
|
|
17
|
+
# or
|
|
18
|
+
pnpm add @appmachina/node
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Quick Start
|
|
22
|
+
|
|
23
|
+
```typescript
|
|
24
|
+
import { AppMachinaNode } from '@appmachina/node';
|
|
25
|
+
|
|
26
|
+
const appmachina = new AppMachinaNode({
|
|
27
|
+
appId: 'your-app-id',
|
|
28
|
+
environment: 'production'
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
// Track events with a distinctId
|
|
32
|
+
appmachina.track('user_123', 'page_view', { path: '/dashboard' });
|
|
33
|
+
|
|
34
|
+
// Track screen views
|
|
35
|
+
appmachina.screen('user_123', 'Dashboard');
|
|
36
|
+
|
|
37
|
+
// Set user properties
|
|
38
|
+
appmachina.setUserProperties('user_123', { plan: 'premium' });
|
|
39
|
+
|
|
40
|
+
// Flush on demand
|
|
41
|
+
await appmachina.flush();
|
|
42
|
+
|
|
43
|
+
// Graceful shutdown
|
|
44
|
+
await appmachina.shutdown();
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Configuration
|
|
48
|
+
|
|
49
|
+
### AppMachinaNodeConfig
|
|
50
|
+
|
|
51
|
+
```typescript
|
|
52
|
+
interface AppMachinaNodeConfig {
|
|
53
|
+
appId: string;
|
|
54
|
+
environment: 'development' | 'staging' | 'production';
|
|
55
|
+
enableDebug?: boolean;
|
|
56
|
+
baseUrl?: string;
|
|
57
|
+
flushIntervalMs?: number;
|
|
58
|
+
flushThreshold?: number;
|
|
59
|
+
maxQueueSize?: number;
|
|
60
|
+
maxBatchSize?: number;
|
|
61
|
+
shutdownFlushTimeoutMs?: number;
|
|
62
|
+
handleSignals?: boolean;
|
|
63
|
+
persistenceDir?: string;
|
|
64
|
+
}
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
| Option | Type | Default | Description |
|
|
68
|
+
| ------------------------ | ------------- | ------------------------------------ | --------------------------------------------------------------------------------------- |
|
|
69
|
+
| `appId` | `string` | _required_ | Your AppMachina application identifier. |
|
|
70
|
+
| `environment` | `Environment` | _required_ | `'development'`, `'staging'`, or `'production'`. |
|
|
71
|
+
| `enableDebug` | `boolean` | `false` | Enable verbose console logging. |
|
|
72
|
+
| `baseUrl` | `string` | `"https://in.appmachina.com"` | Custom ingest API endpoint. |
|
|
73
|
+
| `flushIntervalMs` | `number` | `10000` | Automatic flush interval in milliseconds. Server SDKs default to 10s for lower latency. |
|
|
74
|
+
| `flushThreshold` | `number` | `20` | Queue size that triggers an automatic flush. |
|
|
75
|
+
| `maxQueueSize` | `number` | `10000` | Maximum events in the queue before dropping. |
|
|
76
|
+
| `maxBatchSize` | `number` | `undefined` | Maximum events sent in a single HTTP batch. |
|
|
77
|
+
| `shutdownFlushTimeoutMs` | `number` | `5000` | Maximum time (ms) to wait for a final flush during shutdown. |
|
|
78
|
+
| `handleSignals` | `boolean` | `true` | Register SIGTERM/SIGINT handlers for graceful shutdown. |
|
|
79
|
+
| `persistenceDir` | `string` | `os.tmpdir()/appmachina-sdk/<appId>` | Directory for event persistence files. |
|
|
80
|
+
|
|
81
|
+
```typescript
|
|
82
|
+
const appmachina = new AppMachinaNode({
|
|
83
|
+
appId: 'your-app-id',
|
|
84
|
+
environment: 'production',
|
|
85
|
+
flushIntervalMs: 5000,
|
|
86
|
+
flushThreshold: 50,
|
|
87
|
+
maxQueueSize: 50000,
|
|
88
|
+
shutdownFlushTimeoutMs: 10000,
|
|
89
|
+
handleSignals: true
|
|
90
|
+
});
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
## Core API
|
|
94
|
+
|
|
95
|
+
### Event Tracking
|
|
96
|
+
|
|
97
|
+
```typescript
|
|
98
|
+
track(distinctId: string, eventName: string, properties?: EventProperties): void
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Track an event for a specific user. The `distinctId` is required on every call.
|
|
102
|
+
|
|
103
|
+
```typescript
|
|
104
|
+
appmachina.track('user_123', 'purchase_completed', {
|
|
105
|
+
product_id: 'sku_123',
|
|
106
|
+
price: 9.99,
|
|
107
|
+
currency: 'USD'
|
|
108
|
+
});
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
### Screen Tracking
|
|
112
|
+
|
|
113
|
+
```typescript
|
|
114
|
+
screen(distinctId: string, screenName: string, properties?: EventProperties): void
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
```typescript
|
|
118
|
+
appmachina.screen('user_123', 'Dashboard', { tab: 'overview' });
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
### User Properties
|
|
122
|
+
|
|
123
|
+
```typescript
|
|
124
|
+
setUserProperties(distinctId: string, properties: UserProperties): void
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
```typescript
|
|
128
|
+
appmachina.setUserProperties('user_123', {
|
|
129
|
+
email: 'user@example.com',
|
|
130
|
+
plan: 'premium',
|
|
131
|
+
company: 'Acme Corp'
|
|
132
|
+
});
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
### Consent Management
|
|
136
|
+
|
|
137
|
+
```typescript
|
|
138
|
+
setConsent(consent: ConsentState): void
|
|
139
|
+
getConsentState(): ConsentState
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
Consent applies globally to the SDK instance, not per-user.
|
|
143
|
+
|
|
144
|
+
```typescript
|
|
145
|
+
appmachina.setConsent({ analytics: true, advertising: false });
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
### Session & Queue
|
|
149
|
+
|
|
150
|
+
```typescript
|
|
151
|
+
getSessionId(): string
|
|
152
|
+
queueDepth(): number
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
### Flush & Shutdown
|
|
156
|
+
|
|
157
|
+
```typescript
|
|
158
|
+
// Flush all queued events
|
|
159
|
+
async flush(): Promise<void>
|
|
160
|
+
|
|
161
|
+
// Graceful shutdown: flush remaining events (with timeout), then stop
|
|
162
|
+
async shutdown(): Promise<void>
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
### Error Handling
|
|
166
|
+
|
|
167
|
+
```typescript
|
|
168
|
+
on(event: 'error', listener: (error: Error) => void): this
|
|
169
|
+
off(event: 'error', listener: (error: Error) => void): this
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
```typescript
|
|
173
|
+
appmachina.on('error', (error) => {
|
|
174
|
+
console.error('AppMachina error:', error.message);
|
|
175
|
+
});
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
## Express Middleware
|
|
179
|
+
|
|
180
|
+
Import from `@appmachina/node/express`:
|
|
181
|
+
|
|
182
|
+
```typescript
|
|
183
|
+
import { AppMachinaNode } from '@appmachina/node';
|
|
184
|
+
import { appMachinaExpressMiddleware } from '@appmachina/node/express';
|
|
185
|
+
import express from 'express';
|
|
186
|
+
|
|
187
|
+
const app = express();
|
|
188
|
+
const appmachina = new AppMachinaNode({
|
|
189
|
+
appId: 'your-app-id',
|
|
190
|
+
environment: 'production'
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
app.use(appMachinaExpressMiddleware(appmachina));
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
### Middleware Options
|
|
197
|
+
|
|
198
|
+
```typescript
|
|
199
|
+
interface AppMachinaExpressOptions {
|
|
200
|
+
trackRequests?: boolean; // Track each HTTP request. Default: true
|
|
201
|
+
trackResponseTime?: boolean; // Include response_time_ms. Default: true
|
|
202
|
+
ignorePaths?: string[]; // Paths to skip. Default: ['/health', '/healthz', '/ready', '/favicon.ico']
|
|
203
|
+
}
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
```typescript
|
|
207
|
+
app.use(
|
|
208
|
+
appMachinaExpressMiddleware(appmachina, {
|
|
209
|
+
trackRequests: true,
|
|
210
|
+
trackResponseTime: true,
|
|
211
|
+
ignorePaths: ['/health', '/healthz', '/ready', '/favicon.ico', '/metrics']
|
|
212
|
+
})
|
|
213
|
+
);
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
### What It Tracks
|
|
217
|
+
|
|
218
|
+
For each HTTP request (excluding ignored paths), the middleware tracks an `http_request` event with:
|
|
219
|
+
|
|
220
|
+
- `method` -- HTTP method (GET, POST, etc.)
|
|
221
|
+
- `path` -- Request path
|
|
222
|
+
- `status_code` -- Response status code
|
|
223
|
+
- `response_time_ms` -- Response time in milliseconds (if `trackResponseTime` is true)
|
|
224
|
+
|
|
225
|
+
### User Identification
|
|
226
|
+
|
|
227
|
+
The middleware resolves the `distinctId` from request headers:
|
|
228
|
+
|
|
229
|
+
1. `X-User-Id` header
|
|
230
|
+
2. `X-App-User-Id` header
|
|
231
|
+
3. Falls back to `'anonymous'`
|
|
232
|
+
|
|
233
|
+
The header value is sanitized: must match `[a-zA-Z0-9_@.+-]` and be at most 128 characters.
|
|
234
|
+
|
|
235
|
+
## Next.js Integration
|
|
236
|
+
|
|
237
|
+
Import from `@appmachina/node/nextjs`:
|
|
238
|
+
|
|
239
|
+
```typescript
|
|
240
|
+
import { AppMachinaNode } from '@appmachina/node';
|
|
241
|
+
import {
|
|
242
|
+
getAppMachinaContext,
|
|
243
|
+
trackServerAction,
|
|
244
|
+
trackServerPageView,
|
|
245
|
+
withAppMachinaContext
|
|
246
|
+
} from '@appmachina/node/nextjs';
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
### AsyncLocalStorage Context
|
|
250
|
+
|
|
251
|
+
The Next.js integration uses `AsyncLocalStorage` to propagate request context (user identity and properties) through the request lifecycle.
|
|
252
|
+
|
|
253
|
+
```typescript
|
|
254
|
+
// In middleware or API route
|
|
255
|
+
withAppMachinaContext({ distinctId: userId, properties: { role: 'admin' } }, async () => {
|
|
256
|
+
// Inside this callback, getAppMachinaContext() returns the context
|
|
257
|
+
const ctx = getAppMachinaContext();
|
|
258
|
+
appmachina.track(ctx!.distinctId, 'api_call', { endpoint: '/users' });
|
|
259
|
+
});
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
### Request Context
|
|
263
|
+
|
|
264
|
+
```typescript
|
|
265
|
+
interface RequestContext {
|
|
266
|
+
distinctId: string;
|
|
267
|
+
properties: Record<string, unknown>;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function withAppMachinaContext<T>(context: RequestContext, fn: () => T): T;
|
|
271
|
+
function getAppMachinaContext(): RequestContext | undefined;
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
### Server Page View Tracking
|
|
275
|
+
|
|
276
|
+
```typescript
|
|
277
|
+
function trackServerPageView(appmachina: AppMachinaNode, path: string, distinctId?: string): void;
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
Tracks a `page_view` event. Resolves `distinctId` from the current `AsyncLocalStorage` context, falling back to the provided `distinctId` or `'anonymous'`. Automatically adds `server_rendered: true` and any context properties.
|
|
281
|
+
|
|
282
|
+
```typescript
|
|
283
|
+
// In a Next.js Server Component or API route
|
|
284
|
+
trackServerPageView(appmachina, '/dashboard');
|
|
285
|
+
|
|
286
|
+
// With explicit distinctId
|
|
287
|
+
trackServerPageView(appmachina, '/dashboard', 'user_123');
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
### Server Action Tracking
|
|
291
|
+
|
|
292
|
+
```typescript
|
|
293
|
+
function trackServerAction(
|
|
294
|
+
appmachina: AppMachinaNode,
|
|
295
|
+
actionName: string,
|
|
296
|
+
properties?: Record<string, unknown>,
|
|
297
|
+
distinctId?: string
|
|
298
|
+
): void;
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
Tracks a `server_action` event with `action_name` as a property.
|
|
302
|
+
|
|
303
|
+
```typescript
|
|
304
|
+
// In a Next.js Server Action
|
|
305
|
+
'use server';
|
|
306
|
+
|
|
307
|
+
export async function createPost(formData: FormData) {
|
|
308
|
+
trackServerAction(appmachina, 'create_post', {
|
|
309
|
+
title: formData.get('title')
|
|
310
|
+
});
|
|
311
|
+
// ... create post
|
|
312
|
+
}
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
### Next.js Middleware Example
|
|
316
|
+
|
|
317
|
+
```typescript
|
|
318
|
+
// middleware.ts
|
|
319
|
+
import { withAppMachinaContext } from '@appmachina/node/nextjs';
|
|
320
|
+
import { NextResponse } from 'next/server';
|
|
321
|
+
import type { NextRequest } from 'next/server';
|
|
322
|
+
|
|
323
|
+
export function middleware(request: NextRequest) {
|
|
324
|
+
const userId = request.headers.get('x-user-id') ?? 'anonymous';
|
|
325
|
+
|
|
326
|
+
return withAppMachinaContext(
|
|
327
|
+
{ distinctId: userId, properties: { path: request.nextUrl.pathname } },
|
|
328
|
+
() => NextResponse.next()
|
|
329
|
+
);
|
|
330
|
+
}
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
### Next.js API Route Example
|
|
334
|
+
|
|
335
|
+
```typescript
|
|
336
|
+
// app/api/users/route.ts
|
|
337
|
+
import { appmachina } from '@/lib/appmachina';
|
|
338
|
+
|
|
339
|
+
export async function GET(request: Request) {
|
|
340
|
+
const userId = request.headers.get('x-user-id') ?? 'anonymous';
|
|
341
|
+
|
|
342
|
+
appmachina.track(userId, 'api_call', {
|
|
343
|
+
method: 'GET',
|
|
344
|
+
path: '/api/users'
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
return Response.json({ users: [] });
|
|
348
|
+
}
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
## Signal Handling
|
|
352
|
+
|
|
353
|
+
By default (`handleSignals: true`), the SDK registers `SIGTERM` and `SIGINT` handlers that flush remaining events before the process exits. This is critical for:
|
|
354
|
+
|
|
355
|
+
- Kubernetes pod termination
|
|
356
|
+
- Docker container shutdown
|
|
357
|
+
- Ctrl+C during development
|
|
358
|
+
|
|
359
|
+
All `AppMachinaNode` instances are flushed on signal receipt. Set `handleSignals: false` if you manage process signals yourself.
|
|
360
|
+
|
|
361
|
+
```typescript
|
|
362
|
+
// Disable automatic signal handling
|
|
363
|
+
const appmachina = new AppMachinaNode({
|
|
364
|
+
appId: 'your-app-id',
|
|
365
|
+
environment: 'production',
|
|
366
|
+
handleSignals: false
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
// Handle shutdown yourself
|
|
370
|
+
process.on('SIGTERM', async () => {
|
|
371
|
+
await appmachina.shutdown();
|
|
372
|
+
process.exit(0);
|
|
373
|
+
});
|
|
374
|
+
```
|
|
375
|
+
|
|
376
|
+
## Event Persistence
|
|
377
|
+
|
|
378
|
+
Events are persisted to the filesystem (default: `os.tmpdir()/appmachina-sdk/<appId>`) so they survive process restarts. Configure the directory:
|
|
379
|
+
|
|
380
|
+
```typescript
|
|
381
|
+
const appmachina = new AppMachinaNode({
|
|
382
|
+
appId: 'your-app-id',
|
|
383
|
+
environment: 'production',
|
|
384
|
+
persistenceDir: '/var/data/appmachina'
|
|
385
|
+
});
|
|
386
|
+
```
|
|
387
|
+
|
|
388
|
+
## Automatic Behaviors
|
|
389
|
+
|
|
390
|
+
- **Periodic flush**: Events are flushed every `flushIntervalMs` (default 10s).
|
|
391
|
+
- **Signal handling**: SIGTERM/SIGINT trigger graceful shutdown with flush.
|
|
392
|
+
- **Event persistence**: Events are persisted to disk and rehydrated on restart.
|
|
393
|
+
- **Retry with backoff**: Failed network requests are retried automatically.
|
|
394
|
+
- **Circuit breaker**: Repeated failures temporarily disable network calls.
|
|
395
|
+
|
|
396
|
+
## Multiple Instances
|
|
397
|
+
|
|
398
|
+
You can create multiple `AppMachinaNode` instances for different apps:
|
|
399
|
+
|
|
400
|
+
```typescript
|
|
401
|
+
const appMachinaApp1 = new AppMachinaNode({
|
|
402
|
+
appId: 'app-1',
|
|
403
|
+
environment: 'production'
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
const appMachinaApp2 = new AppMachinaNode({
|
|
407
|
+
appId: 'app-2',
|
|
408
|
+
environment: 'production'
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
// Both are flushed on SIGTERM if handleSignals is true
|
|
412
|
+
```
|
|
413
|
+
|
|
414
|
+
## TypeScript Types
|
|
415
|
+
|
|
416
|
+
```typescript
|
|
417
|
+
import type {
|
|
418
|
+
AppMachinaNodeConfig,
|
|
419
|
+
ConsentState,
|
|
420
|
+
Environment,
|
|
421
|
+
ErrorListener,
|
|
422
|
+
EventProperties,
|
|
423
|
+
UserProperties
|
|
424
|
+
} from '@appmachina/node';
|
|
425
|
+
import type { AppMachinaExpressOptions } from '@appmachina/node/express';
|
|
426
|
+
import type { RequestContext } from '@appmachina/node/nextjs';
|
|
427
|
+
```
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
//#region src/express.ts
|
|
2
|
+
/** Attribution URL parameters captured from incoming requests. */
|
|
3
|
+
const CLICK_ID_PARAMS = [
|
|
4
|
+
"fbclid",
|
|
5
|
+
"gclid",
|
|
6
|
+
"gbraid",
|
|
7
|
+
"wbraid",
|
|
8
|
+
"ttclid",
|
|
9
|
+
"msclkid",
|
|
10
|
+
"rclid"
|
|
11
|
+
];
|
|
12
|
+
const UTM_PARAMS = [
|
|
13
|
+
"utm_source",
|
|
14
|
+
"utm_medium",
|
|
15
|
+
"utm_campaign",
|
|
16
|
+
"utm_content",
|
|
17
|
+
"utm_term"
|
|
18
|
+
];
|
|
19
|
+
const ALL_ATTRIBUTION_PARAMS = [...CLICK_ID_PARAMS, ...UTM_PARAMS];
|
|
20
|
+
/**
|
|
21
|
+
* Extract attribution parameters from the request URL query string.
|
|
22
|
+
*
|
|
23
|
+
* Tries `req.query` first (populated by Express's query parser) and falls
|
|
24
|
+
* back to parsing `req.url` with `URLSearchParams` for minimal frameworks.
|
|
25
|
+
*/
|
|
26
|
+
function extractUrlParams(req) {
|
|
27
|
+
const params = {};
|
|
28
|
+
if (req.query && typeof req.query === "object") {
|
|
29
|
+
for (const param of ALL_ATTRIBUTION_PARAMS) {
|
|
30
|
+
const value = req.query[param];
|
|
31
|
+
if (typeof value === "string" && value) params[`$url_${param}`] = value;
|
|
32
|
+
}
|
|
33
|
+
if (Object.keys(params).length > 0) return params;
|
|
34
|
+
}
|
|
35
|
+
try {
|
|
36
|
+
const queryStart = req.url.indexOf("?");
|
|
37
|
+
if (queryStart === -1) return params;
|
|
38
|
+
const searchParams = new URLSearchParams(req.url.slice(queryStart));
|
|
39
|
+
for (const param of ALL_ATTRIBUTION_PARAMS) {
|
|
40
|
+
const value = searchParams.get(param);
|
|
41
|
+
if (value) params[`$url_${param}`] = value;
|
|
42
|
+
}
|
|
43
|
+
} catch {}
|
|
44
|
+
return params;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Express middleware that automatically tracks HTTP requests as events.
|
|
48
|
+
*
|
|
49
|
+
* Usage:
|
|
50
|
+
* import { AppMachinaNode } from '@appmachina/node';
|
|
51
|
+
* import { appMachinaExpressMiddleware } from '@appmachina/node/express';
|
|
52
|
+
*
|
|
53
|
+
* const sdk = new AppMachinaNode({ appId: '...', environment: 'production' });
|
|
54
|
+
* app.use(appMachinaExpressMiddleware(sdk));
|
|
55
|
+
*/
|
|
56
|
+
function appMachinaExpressMiddleware(sdk, options = {}) {
|
|
57
|
+
const { trackRequests = true, trackResponseTime = true, captureUrlParams = true, ignorePaths = [
|
|
58
|
+
"/health",
|
|
59
|
+
"/healthz",
|
|
60
|
+
"/ready",
|
|
61
|
+
"/favicon.ico"
|
|
62
|
+
] } = options;
|
|
63
|
+
const ignoreSet = new Set(ignorePaths);
|
|
64
|
+
return (req, res, next) => {
|
|
65
|
+
if (!trackRequests || ignoreSet.has(req.path)) {
|
|
66
|
+
next();
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
const startTime = Date.now();
|
|
70
|
+
const urlParams = captureUrlParams ? extractUrlParams(req) : {};
|
|
71
|
+
const referer = resolveHeader(req.headers, "referer") ?? resolveHeader(req.headers, "referrer");
|
|
72
|
+
res.on("finish", () => {
|
|
73
|
+
const distinctId = sanitizeDistinctId(resolveHeader(req.headers, "x-user-id") ?? resolveHeader(req.headers, "x-app-user-id"));
|
|
74
|
+
const properties = {
|
|
75
|
+
method: req.method,
|
|
76
|
+
path: req.path,
|
|
77
|
+
status_code: res.statusCode,
|
|
78
|
+
...urlParams
|
|
79
|
+
};
|
|
80
|
+
if (referer) properties["$url_referrer"] = referer;
|
|
81
|
+
if (trackResponseTime) properties.response_time_ms = Date.now() - startTime;
|
|
82
|
+
sdk.track(distinctId, "http_request", properties);
|
|
83
|
+
});
|
|
84
|
+
next();
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
function resolveHeader(headers, name) {
|
|
88
|
+
const value = headers[name];
|
|
89
|
+
if (typeof value === "string") return value;
|
|
90
|
+
if (Array.isArray(value)) return value[0];
|
|
91
|
+
}
|
|
92
|
+
const VALID_DISTINCT_ID = /^[a-zA-Z0-9_@.+\-]+$/;
|
|
93
|
+
const MAX_DISTINCT_ID_LENGTH = 128;
|
|
94
|
+
function sanitizeDistinctId(raw) {
|
|
95
|
+
if (!raw || raw.length > MAX_DISTINCT_ID_LENGTH || !VALID_DISTINCT_ID.test(raw)) return "anonymous";
|
|
96
|
+
return raw;
|
|
97
|
+
}
|
|
98
|
+
const DEBUG_SDK_VERSION = typeof __APPMACHINA_NODE_VERSION__ !== "undefined" ? __APPMACHINA_NODE_VERSION__ : "0.1.0";
|
|
99
|
+
/**
|
|
100
|
+
* Express route handler that returns SDK debug information as JSON.
|
|
101
|
+
*
|
|
102
|
+
* Usage:
|
|
103
|
+
* import { appMachinaDebugMiddleware } from '@appmachina/node/express';
|
|
104
|
+
*
|
|
105
|
+
* app.get('/__appmachina/debug', appMachinaDebugMiddleware(sdk));
|
|
106
|
+
*/
|
|
107
|
+
function appMachinaDebugMiddleware(sdk) {
|
|
108
|
+
return (_req, res) => {
|
|
109
|
+
res.json({
|
|
110
|
+
sdk: "@appmachina/node",
|
|
111
|
+
version: DEBUG_SDK_VERSION,
|
|
112
|
+
queueDepth: sdk.queueDepth(),
|
|
113
|
+
sessionId: sdk.getSessionId(),
|
|
114
|
+
instanceId: sdk.getInstanceId(),
|
|
115
|
+
consentState: sdk.getConsentState(),
|
|
116
|
+
uptime: process.uptime()
|
|
117
|
+
});
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
//#endregion
|
|
122
|
+
export { appMachinaExpressMiddleware as n, extractUrlParams as r, appMachinaDebugMiddleware as t };
|
|
123
|
+
//# sourceMappingURL=express-CeETK4sI.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"express-CeETK4sI.js","names":["params: Record<string, string>","properties: Record<string, unknown>","DEBUG_SDK_VERSION: string"],"sources":["../src/express.ts"],"sourcesContent":["// @appmachina/node/express — Express middleware for automatic request tracking.\nimport type { AppMachinaNode } from './index.js';\n\n/** Attribution URL parameters captured from incoming requests. */\nconst CLICK_ID_PARAMS = [\n 'fbclid',\n 'gclid',\n 'gbraid',\n 'wbraid',\n 'ttclid',\n 'msclkid',\n 'rclid'\n] as const;\nconst UTM_PARAMS = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_content', 'utm_term'] as const;\nconst ALL_ATTRIBUTION_PARAMS = [...CLICK_ID_PARAMS, ...UTM_PARAMS] as const;\n\nexport interface AppMachinaExpressOptions {\n /** Track each HTTP request as an event. @default true */\n trackRequests?: boolean;\n /** Include response time in request event properties. @default true */\n trackResponseTime?: boolean;\n /** Capture attribution URL parameters (fbclid, gclid, utm_source, etc.) from request query strings. @default true */\n captureUrlParams?: boolean;\n /** URL paths to exclude from tracking. @default ['/health', '/healthz', '/ready', '/favicon.ico'] */\n ignorePaths?: string[];\n}\n\ninterface Request {\n method: string;\n path: string;\n url: string;\n headers: Record<string, string | string[] | undefined>;\n query?: Record<string, unknown>;\n ip?: string;\n}\n\ninterface Response {\n statusCode: number;\n on(event: string, callback: () => void): void;\n}\n\ntype NextFunction = (err?: unknown) => void;\n\n/**\n * Extract attribution parameters from the request URL query string.\n *\n * Tries `req.query` first (populated by Express's query parser) and falls\n * back to parsing `req.url` with `URLSearchParams` for minimal frameworks.\n */\nexport function extractUrlParams(req: Request): Record<string, string> {\n const params: Record<string, string> = {};\n\n // Try req.query first (Express populates this)\n if (req.query && typeof req.query === 'object') {\n for (const param of ALL_ATTRIBUTION_PARAMS) {\n const value = req.query[param];\n if (typeof value === 'string' && value) {\n params[`$url_${param}`] = value;\n }\n }\n if (Object.keys(params).length > 0) return params;\n }\n\n // Fallback: parse from req.url\n try {\n const queryStart = req.url.indexOf('?');\n if (queryStart === -1) return params;\n\n const searchParams = new URLSearchParams(req.url.slice(queryStart));\n for (const param of ALL_ATTRIBUTION_PARAMS) {\n const value = searchParams.get(param);\n if (value) {\n params[`$url_${param}`] = value;\n }\n }\n } catch {\n // URL parsing failed — return empty\n }\n\n return params;\n}\n\n/**\n * Express middleware that automatically tracks HTTP requests as events.\n *\n * Usage:\n * import { AppMachinaNode } from '@appmachina/node';\n * import { appMachinaExpressMiddleware } from '@appmachina/node/express';\n *\n * const sdk = new AppMachinaNode({ appId: '...', environment: 'production' });\n * app.use(appMachinaExpressMiddleware(sdk));\n */\nexport function appMachinaExpressMiddleware(\n sdk: AppMachinaNode,\n options: AppMachinaExpressOptions = {}\n) {\n const {\n trackRequests = true,\n trackResponseTime = true,\n captureUrlParams = true,\n ignorePaths = ['/health', '/healthz', '/ready', '/favicon.ico']\n } = options;\n\n const ignoreSet = new Set(ignorePaths);\n\n return (req: Request, res: Response, next: NextFunction) => {\n if (!trackRequests || ignoreSet.has(req.path)) {\n next();\n return;\n }\n\n const startTime = Date.now();\n\n // Capture URL params eagerly (before response finishes, while req is fresh)\n const urlParams = captureUrlParams ? extractUrlParams(req) : {};\n\n // Capture Referer header (supports both spellings)\n const referer = resolveHeader(req.headers, 'referer') ?? resolveHeader(req.headers, 'referrer');\n\n res.on('finish', () => {\n const rawDistinctId =\n resolveHeader(req.headers, 'x-user-id') ?? resolveHeader(req.headers, 'x-app-user-id');\n const distinctId = sanitizeDistinctId(rawDistinctId);\n\n const properties: Record<string, unknown> = {\n method: req.method,\n path: req.path,\n status_code: res.statusCode,\n ...urlParams\n };\n\n if (referer) {\n properties['$url_referrer'] = referer;\n }\n\n if (trackResponseTime) {\n properties.response_time_ms = Date.now() - startTime;\n }\n\n sdk.track(distinctId, 'http_request', properties);\n });\n\n next();\n };\n}\n\nfunction resolveHeader(\n headers: Record<string, string | string[] | undefined>,\n name: string\n): string | undefined {\n const value = headers[name];\n if (typeof value === 'string') return value;\n if (Array.isArray(value)) return value[0];\n return undefined;\n}\n\nconst VALID_DISTINCT_ID = /^[a-zA-Z0-9_@.+\\-]+$/; // eslint-disable-line no-useless-escape\nconst MAX_DISTINCT_ID_LENGTH = 128;\n\nfunction sanitizeDistinctId(raw: string | undefined): string {\n if (!raw || raw.length > MAX_DISTINCT_ID_LENGTH || !VALID_DISTINCT_ID.test(raw)) {\n return 'anonymous';\n }\n return raw;\n}\n\n// SDK version injected at build time\ndeclare const __APPMACHINA_NODE_VERSION__: string;\nconst DEBUG_SDK_VERSION: string =\n typeof __APPMACHINA_NODE_VERSION__ !== 'undefined' ? __APPMACHINA_NODE_VERSION__ : '0.1.0';\n\n/**\n * Express route handler that returns SDK debug information as JSON.\n *\n * Usage:\n * import { appMachinaDebugMiddleware } from '@appmachina/node/express';\n *\n * app.get('/__appmachina/debug', appMachinaDebugMiddleware(sdk));\n */\nexport function appMachinaDebugMiddleware(sdk: AppMachinaNode) {\n return (_req: unknown, res: { json: (body: unknown) => void }) => {\n res.json({\n sdk: '@appmachina/node',\n version: DEBUG_SDK_VERSION,\n queueDepth: sdk.queueDepth(),\n sessionId: sdk.getSessionId(),\n instanceId: sdk.getInstanceId(),\n consentState: sdk.getConsentState(),\n uptime: process.uptime()\n });\n };\n}\n"],"mappings":";;AAIA,MAAM,kBAAkB;CACtB;CACA;CACA;CACA;CACA;CACA;CACA;CACD;AACD,MAAM,aAAa;CAAC;CAAc;CAAc;CAAgB;CAAe;CAAW;AAC1F,MAAM,yBAAyB,CAAC,GAAG,iBAAiB,GAAG,WAAW;;;;;;;AAmClE,SAAgB,iBAAiB,KAAsC;CACrE,MAAMA,SAAiC,EAAE;AAGzC,KAAI,IAAI,SAAS,OAAO,IAAI,UAAU,UAAU;AAC9C,OAAK,MAAM,SAAS,wBAAwB;GAC1C,MAAM,QAAQ,IAAI,MAAM;AACxB,OAAI,OAAO,UAAU,YAAY,MAC/B,QAAO,QAAQ,WAAW;;AAG9B,MAAI,OAAO,KAAK,OAAO,CAAC,SAAS,EAAG,QAAO;;AAI7C,KAAI;EACF,MAAM,aAAa,IAAI,IAAI,QAAQ,IAAI;AACvC,MAAI,eAAe,GAAI,QAAO;EAE9B,MAAM,eAAe,IAAI,gBAAgB,IAAI,IAAI,MAAM,WAAW,CAAC;AACnE,OAAK,MAAM,SAAS,wBAAwB;GAC1C,MAAM,QAAQ,aAAa,IAAI,MAAM;AACrC,OAAI,MACF,QAAO,QAAQ,WAAW;;SAGxB;AAIR,QAAO;;;;;;;;;;;;AAaT,SAAgB,4BACd,KACA,UAAoC,EAAE,EACtC;CACA,MAAM,EACJ,gBAAgB,MAChB,oBAAoB,MACpB,mBAAmB,MACnB,cAAc;EAAC;EAAW;EAAY;EAAU;EAAe,KAC7D;CAEJ,MAAM,YAAY,IAAI,IAAI,YAAY;AAEtC,SAAQ,KAAc,KAAe,SAAuB;AAC1D,MAAI,CAAC,iBAAiB,UAAU,IAAI,IAAI,KAAK,EAAE;AAC7C,SAAM;AACN;;EAGF,MAAM,YAAY,KAAK,KAAK;EAG5B,MAAM,YAAY,mBAAmB,iBAAiB,IAAI,GAAG,EAAE;EAG/D,MAAM,UAAU,cAAc,IAAI,SAAS,UAAU,IAAI,cAAc,IAAI,SAAS,WAAW;AAE/F,MAAI,GAAG,gBAAgB;GAGrB,MAAM,aAAa,mBADjB,cAAc,IAAI,SAAS,YAAY,IAAI,cAAc,IAAI,SAAS,gBAAgB,CACpC;GAEpD,MAAMC,aAAsC;IAC1C,QAAQ,IAAI;IACZ,MAAM,IAAI;IACV,aAAa,IAAI;IACjB,GAAG;IACJ;AAED,OAAI,QACF,YAAW,mBAAmB;AAGhC,OAAI,kBACF,YAAW,mBAAmB,KAAK,KAAK,GAAG;AAG7C,OAAI,MAAM,YAAY,gBAAgB,WAAW;IACjD;AAEF,QAAM;;;AAIV,SAAS,cACP,SACA,MACoB;CACpB,MAAM,QAAQ,QAAQ;AACtB,KAAI,OAAO,UAAU,SAAU,QAAO;AACtC,KAAI,MAAM,QAAQ,MAAM,CAAE,QAAO,MAAM;;AAIzC,MAAM,oBAAoB;AAC1B,MAAM,yBAAyB;AAE/B,SAAS,mBAAmB,KAAiC;AAC3D,KAAI,CAAC,OAAO,IAAI,SAAS,0BAA0B,CAAC,kBAAkB,KAAK,IAAI,CAC7E,QAAO;AAET,QAAO;;AAKT,MAAMC,oBACJ,OAAO,gCAAgC,cAAc,8BAA8B;;;;;;;;;AAUrF,SAAgB,0BAA0B,KAAqB;AAC7D,SAAQ,MAAe,QAA2C;AAChE,MAAI,KAAK;GACP,KAAK;GACL,SAAS;GACT,YAAY,IAAI,YAAY;GAC5B,WAAW,IAAI,cAAc;GAC7B,YAAY,IAAI,eAAe;GAC/B,cAAc,IAAI,iBAAiB;GACnC,QAAQ,QAAQ,QAAQ;GACzB,CAAC"}
|