@fullevent/node 0.0.1

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/src/index.ts ADDED
@@ -0,0 +1,272 @@
1
+ /**
2
+ * @packageDocumentation
3
+ *
4
+ * # FullEvent Node.js SDK
5
+ *
6
+ * The official Node.js SDK for FullEvent - the wide event analytics platform.
7
+ *
8
+ * ## Installation
9
+ *
10
+ * ```bash
11
+ * npm install @fullevent/node
12
+ * ```
13
+ *
14
+ * ## Quick Start
15
+ *
16
+ * ### Direct Event Ingestion
17
+ *
18
+ * ```typescript
19
+ * import { FullEvent } from '@fullevent/node';
20
+ *
21
+ * const client = new FullEvent({
22
+ * apiKey: process.env.FULLEVENT_API_KEY!,
23
+ * });
24
+ *
25
+ * await client.ingest('user.signup', { plan: 'pro' });
26
+ * ```
27
+ *
28
+ * ### Wide Event Middleware (Recommended)
29
+ *
30
+ * ```typescript
31
+ * import { Hono } from 'hono';
32
+ * import { wideLogger, WideEventVariables } from '@fullevent/node';
33
+ *
34
+ * const app = new Hono<{ Variables: WideEventVariables }>();
35
+ *
36
+ * app.use(wideLogger({
37
+ * apiKey: process.env.FULLEVENT_API_KEY!,
38
+ * service: 'my-api',
39
+ * }));
40
+ *
41
+ * app.post('/checkout', async (c) => {
42
+ * const event = c.get('wideEvent');
43
+ * event.user = { id: user.id, plan: user.plan };
44
+ * event.cart = { total_cents: cart.total, items: cart.items.length };
45
+ * return c.json({ success: true });
46
+ * });
47
+ * ```
48
+ *
49
+ * @module @fullevent/node
50
+ */
51
+
52
+ export * from './middleware/hono';
53
+ export * from './middleware/express';
54
+ export * from './client';
55
+ export * from './builder';
56
+
57
+ /**
58
+ * A Wide Event captures the complete context of a request in a single,
59
+ * self-describing record.
60
+ *
61
+ * @remarks
62
+ * Instead of emitting many small logs, you build one rich event throughout
63
+ * the request lifecycle and emit it at the end. This makes debugging and
64
+ * analytics significantly easier.
65
+ *
66
+ * ## The Wide Event Pattern
67
+ *
68
+ * 1. **Initialize**: Middleware creates the event with request context
69
+ * 2. **Enrich**: Handlers add business context throughout the request
70
+ * 3. **Emit**: Middleware sends the complete event when the request ends
71
+ *
72
+ * ## Auto-Captured Fields
73
+ *
74
+ * The middleware automatically captures:
75
+ * - `request_id`, `trace_id` - For distributed tracing
76
+ * - `timestamp` - When the request started
77
+ * - `method`, `path` - HTTP method and path
78
+ * - `status_code` - HTTP response status
79
+ * - `duration_ms` - Total request duration
80
+ * - `outcome` - 'success' or 'error'
81
+ * - `service`, `environment`, `region` - Infrastructure context
82
+ *
83
+ * ## Your Business Context
84
+ *
85
+ * Add any fields your application needs:
86
+ *
87
+ * ```typescript
88
+ * event.user = { id: 'usr_123', plan: 'premium', ltv_cents: 50000 };
89
+ * event.cart = { id: 'cart_xyz', items: 3, total_cents: 15999 };
90
+ * event.payment = { provider: 'stripe', method: 'card', latency_ms: 234 };
91
+ * event.feature_flags = { new_checkout: true };
92
+ * event.experiment = { name: 'pricing_v2', variant: 'control' };
93
+ * ```
94
+ *
95
+ * @example Basic Wide Event
96
+ * ```typescript
97
+ * const event: WideEvent = {
98
+ * // Auto-captured by middleware
99
+ * request_id: 'req_abc123',
100
+ * timestamp: '2024-01-15T10:23:45.612Z',
101
+ * method: 'POST',
102
+ * path: '/api/checkout',
103
+ * service: 'checkout-api',
104
+ * status_code: 200,
105
+ * duration_ms: 847,
106
+ * outcome: 'success',
107
+ *
108
+ * // Your business context
109
+ * user: { id: 'usr_456', plan: 'premium' },
110
+ * cart: { total_cents: 15999, items: 3 },
111
+ * payment: { provider: 'stripe', latency_ms: 234 },
112
+ * };
113
+ * ```
114
+ *
115
+ * @category Types
116
+ */
117
+ export interface WideEvent {
118
+ // ═══════════════════════════════════════════════════════════════════════════
119
+ // Core Request Context (auto-populated by middleware)
120
+ // ═══════════════════════════════════════════════════════════════════════════
121
+
122
+ /**
123
+ * Unique identifier for this request.
124
+ *
125
+ * @remarks
126
+ * Auto-generated or extracted from `x-request-id` / `x-fullevent-trace-id` headers.
127
+ */
128
+ request_id?: string;
129
+
130
+ /**
131
+ * Trace ID for distributed tracing.
132
+ *
133
+ * @remarks
134
+ * Propagated across service boundaries via the `x-fullevent-trace-id` header.
135
+ * Same as `request_id` by default.
136
+ *
137
+ * @see {@link https://fullevent.io/docs/distributed-tracing | Distributed Tracing Guide}
138
+ */
139
+ trace_id?: string;
140
+
141
+ /**
142
+ * ISO timestamp when the request started.
143
+ */
144
+ timestamp: string;
145
+
146
+ /**
147
+ * HTTP method (GET, POST, PUT, DELETE, etc.)
148
+ */
149
+ method: string;
150
+
151
+ /**
152
+ * Request path (e.g., `/api/checkout`)
153
+ */
154
+ path: string;
155
+
156
+ /**
157
+ * HTTP status code.
158
+ *
159
+ * @remarks
160
+ * Auto-captured from the response. Used for error rate calculations.
161
+ */
162
+ status_code?: number;
163
+
164
+ /**
165
+ * Request duration in milliseconds.
166
+ *
167
+ * @remarks
168
+ * Auto-captured by middleware in the `finally` block.
169
+ */
170
+ duration_ms?: number;
171
+
172
+ /**
173
+ * Request outcome: 'success' or 'error'.
174
+ *
175
+ * @remarks
176
+ * Auto-set based on `status_code` (>= 400 = error).
177
+ * Can be overridden manually.
178
+ */
179
+ outcome?: 'success' | 'error';
180
+
181
+ // ═══════════════════════════════════════════════════════════════════════════
182
+ // Infrastructure Context
183
+ // ═══════════════════════════════════════════════════════════════════════════
184
+
185
+ /**
186
+ * Service name (e.g., 'checkout-api', 'user-service').
187
+ *
188
+ * @remarks
189
+ * Set via middleware config. Used for filtering and grouping.
190
+ */
191
+ service: string;
192
+
193
+ /**
194
+ * Deployment region (e.g., 'us-east-1', 'eu-west-1').
195
+ */
196
+ region?: string;
197
+
198
+ /**
199
+ * Environment (e.g., 'production', 'staging', 'development').
200
+ *
201
+ * @remarks
202
+ * Defaults to `NODE_ENV` if not specified.
203
+ */
204
+ environment?: string;
205
+
206
+ // ═══════════════════════════════════════════════════════════════════════════
207
+ // Common Context Fields
208
+ // ═══════════════════════════════════════════════════════════════════════════
209
+
210
+ /**
211
+ * User identifier.
212
+ *
213
+ * @remarks
214
+ * The most commonly enriched field. For richer user context,
215
+ * add a `user` object with additional properties.
216
+ */
217
+ user_id?: string;
218
+
219
+ // ═══════════════════════════════════════════════════════════════════════════
220
+ // Error Details
221
+ // ═══════════════════════════════════════════════════════════════════════════
222
+
223
+ /**
224
+ * Structured error information.
225
+ *
226
+ * @remarks
227
+ * Auto-populated when an exception is thrown. Can also be set manually
228
+ * for handled errors (e.g., payment failures).
229
+ *
230
+ * @example
231
+ * ```typescript
232
+ * event.error = {
233
+ * type: 'PaymentError',
234
+ * code: 'card_declined',
235
+ * message: 'Your card was declined',
236
+ * stripe_decline_code: 'insufficient_funds',
237
+ * };
238
+ * ```
239
+ */
240
+ error?: {
241
+ /** Error class/type name (e.g., 'TypeError', 'PaymentError') */
242
+ type: string;
243
+ /** Human-readable error message */
244
+ message: string;
245
+ /** Stack trace (auto-captured for thrown errors) */
246
+ stack?: string;
247
+ /** Error code (e.g., 'ECONNREFUSED', 'card_declined') */
248
+ code?: string;
249
+ /** Additional error context */
250
+ [key: string]: unknown;
251
+ };
252
+
253
+ // ═══════════════════════════════════════════════════════════════════════════
254
+ // Your Business Context
255
+ // ═══════════════════════════════════════════════════════════════════════════
256
+
257
+ /**
258
+ * Dynamic business context.
259
+ *
260
+ * @remarks
261
+ * Add any fields your application needs. Common patterns:
262
+ *
263
+ * - `user` - User context (id, plan, ltv, etc.)
264
+ * - `cart` - Shopping cart (id, items, total, coupon)
265
+ * - `payment` - Payment details (provider, method, latency)
266
+ * - `order` - Order details (id, status, items)
267
+ * - `feature_flags` - Feature flag states
268
+ * - `experiment` - A/B test assignments
269
+ * - `llm` - LLM usage (model, tokens, latency)
270
+ */
271
+ [key: string]: unknown;
272
+ }
@@ -0,0 +1,193 @@
1
+ import { Request, Response, NextFunction } from 'express';
2
+ import { WideEvent } from '../index';
3
+ import { FullEvent } from '../client';
4
+ import { SamplingConfig, WideLoggerConfig } from './hono';
5
+
6
+ // Extend Express Request to include wideEvent
7
+ declare global {
8
+ namespace Express {
9
+ interface Request {
10
+ /** The wide event for the current request */
11
+ wideEvent: WideEvent;
12
+ }
13
+ }
14
+ }
15
+
16
+ /**
17
+ * Determines whether to keep this event based on sampling config.
18
+ * Uses tail-based sampling: decision made AFTER request completes.
19
+ *
20
+ * @internal
21
+ */
22
+ function shouldSample(event: WideEvent, config?: SamplingConfig): boolean {
23
+ const sampling = config ?? {};
24
+ const defaultRate = sampling.defaultRate ?? 1.0;
25
+ const alwaysKeepErrors = sampling.alwaysKeepErrors ?? true;
26
+ const slowThreshold = sampling.slowRequestThresholdMs ?? 2000;
27
+
28
+ // Always keep errors (4xx/5xx or explicit error outcome)
29
+ if (alwaysKeepErrors) {
30
+ if (event.outcome === 'error') return true;
31
+ if (event.status_code && event.status_code >= 400) return true;
32
+ }
33
+
34
+ // Always keep slow requests
35
+ if (event.duration_ms && event.duration_ms > slowThreshold) return true;
36
+
37
+ // Always keep specific paths
38
+ if (sampling.alwaysKeepPaths?.some(p => event.path.includes(p))) return true;
39
+
40
+ // Always keep specific users
41
+ if (event.user_id && sampling.alwaysKeepUsers?.includes(String(event.user_id))) return true;
42
+
43
+ // Consistent Sampling based on Trace ID
44
+ // DJB2 hash for simple, string-based consistency
45
+ if (event.trace_id) {
46
+ let hash = 5381;
47
+ const str = event.trace_id;
48
+ for (let i = 0; i < str.length; i++) {
49
+ hash = ((hash << 5) + hash) + str.charCodeAt(i);
50
+ }
51
+ // Normalize to 0.0 - 1.0 (using 10000 for 4 decimal precision)
52
+ const normalized = (hash >>> 0) % 10000 / 10000;
53
+ return normalized < defaultRate;
54
+ }
55
+
56
+ // Fallback if no trace ID (shouldn't happen usually)
57
+ return Math.random() < defaultRate;
58
+ }
59
+
60
+ /**
61
+ * Express middleware for Wide Event logging.
62
+ *
63
+ * @remarks
64
+ * Creates a single, rich event per request that captures the complete context.
65
+ * The event is initialized with request data and automatically finalized with
66
+ * status, duration, and outcome. Your handlers enrich it with business context.
67
+ *
68
+ * ## How It Works
69
+ *
70
+ * 1. **Request Start**: Middleware creates the event with request context
71
+ * 2. **Handler Runs**: Your code enriches the event via `req.wideEvent`
72
+ * 3. **Response Finish**: Middleware adds status/duration and sends to FullEvent
73
+ *
74
+ * ## Features
75
+ *
76
+ * - **Automatic context capture**: method, path, status, duration
77
+ * - **Distributed tracing**: Propagates `x-fullevent-trace-id` headers
78
+ * - **Tail-based sampling**: 100% error visibility at any sample rate
79
+ * - **Fire and forget**: Non-blocking, won't slow down your responses
80
+ *
81
+ * @param config - Middleware configuration
82
+ * @returns Express middleware function
83
+ *
84
+ * @example Quick Start
85
+ * ```typescript
86
+ * import express from 'express';
87
+ * import { expressWideLogger } from '@fullevent/node';
88
+ *
89
+ * const app = express();
90
+ *
91
+ * app.use(expressWideLogger({
92
+ * apiKey: process.env.FULLEVENT_API_KEY!,
93
+ * service: 'checkout-api',
94
+ * }));
95
+ * ```
96
+ *
97
+ * @example Enriching Events
98
+ * ```typescript
99
+ * app.post('/checkout', async (req, res) => {
100
+ * const event = req.wideEvent;
101
+ *
102
+ * // Add user context
103
+ * event.user = {
104
+ * id: req.user.id,
105
+ * subscription: req.user.plan,
106
+ * };
107
+ *
108
+ * // Add cart context
109
+ * event.cart = {
110
+ * id: cart.id,
111
+ * item_count: cart.items.length,
112
+ * total_cents: cart.total,
113
+ * };
114
+ *
115
+ * // Add payment timing
116
+ * const paymentStart = Date.now();
117
+ * const payment = await processPayment(cart);
118
+ * event.payment = {
119
+ * provider: 'stripe',
120
+ * latency_ms: Date.now() - paymentStart,
121
+ * };
122
+ *
123
+ * res.json({ orderId: payment.orderId });
124
+ * });
125
+ * ```
126
+ *
127
+ * @example With Sampling
128
+ * ```typescript
129
+ * app.use(expressWideLogger({
130
+ * apiKey: process.env.FULLEVENT_API_KEY!,
131
+ * service: 'checkout-api',
132
+ * sampling: {
133
+ * defaultRate: 0.1, // 10% of normal traffic
134
+ * alwaysKeepErrors: true, // 100% of errors
135
+ * slowRequestThresholdMs: 500, // Slow requests
136
+ * },
137
+ * }));
138
+ * ```
139
+ *
140
+ * @category Middleware
141
+ */
142
+ export function expressWideLogger(config: WideLoggerConfig) {
143
+ const client = new FullEvent({
144
+ apiKey: config.apiKey,
145
+ baseUrl: config.baseUrl,
146
+ });
147
+
148
+ return (req: Request, res: Response, next: NextFunction) => {
149
+ const startTime = Date.now();
150
+
151
+ // Distributed Tracing: Use existing trace ID or generate a new one
152
+ const requestId = (req.headers['x-fullevent-trace-id'] as string)
153
+ || (req.headers['x-request-id'] as string)
154
+ || crypto.randomUUID();
155
+
156
+ // Initialize the Wide Event with request context
157
+ const event: WideEvent = {
158
+ request_id: requestId,
159
+ timestamp: new Date().toISOString(),
160
+ method: req.method,
161
+ path: req.path,
162
+ service: config.service,
163
+ environment: config.environment || process.env.NODE_ENV || 'development',
164
+ region: config.region,
165
+ };
166
+
167
+ // Make the event accessible to handlers for enrichment
168
+ req.wideEvent = event;
169
+
170
+ // Propagate the trace ID in response headers
171
+ res.setHeader('x-fullevent-trace-id', requestId);
172
+
173
+ // Capture response finish to record status and send event
174
+ res.on('finish', () => {
175
+ event.status_code = res.statusCode;
176
+ event.outcome = res.statusCode >= 400 ? 'error' : 'success';
177
+ event.duration_ms = Date.now() - startTime;
178
+
179
+ // Only send if we have an API key and event passes sampling
180
+ if (config.apiKey && shouldSample(event, config.sampling)) {
181
+ client.ingest(
182
+ `${event.method} ${event.path}`,
183
+ event,
184
+ event.timestamp
185
+ ).catch(err => {
186
+ console.error('[FullEvent] Failed to send event:', err);
187
+ });
188
+ }
189
+ });
190
+
191
+ next();
192
+ };
193
+ }