@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 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"}