@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.
@@ -0,0 +1,395 @@
1
+ import { Context, Next } from 'hono';
2
+ import { WideEvent } from '../index';
3
+ import { FullEvent } from '../client';
4
+
5
+ /**
6
+ * Configuration for tail-based sampling.
7
+ *
8
+ * @remarks
9
+ * Tail sampling makes the decision AFTER the request completes,
10
+ * allowing you to keep all errors and slow requests while sampling
11
+ * normal traffic. This gives you 100% visibility into problems.
12
+ *
13
+ * @example
14
+ * ```typescript
15
+ * const sampling: SamplingConfig = {
16
+ * defaultRate: 0.1, // Keep 10% of normal requests
17
+ * alwaysKeepErrors: true, // But 100% of errors
18
+ * slowRequestThresholdMs: 500, // And anything slow
19
+ * alwaysKeepPaths: ['/api/checkout', '/api/payment'],
20
+ * alwaysKeepUsers: ['usr_vip_123'],
21
+ * };
22
+ * ```
23
+ *
24
+ * @category Middleware
25
+ */
26
+ export interface SamplingConfig {
27
+ /**
28
+ * Base sample rate for normal successful requests.
29
+ *
30
+ * @remarks
31
+ * Value between 0.0 and 1.0. For example, 0.1 means 10% of
32
+ * successful requests are kept.
33
+ *
34
+ * @defaultValue `1.0` (keep all)
35
+ */
36
+ defaultRate?: number;
37
+
38
+ /**
39
+ * Always keep error responses (4xx/5xx status codes).
40
+ *
41
+ * @remarks
42
+ * Highly recommended to leave enabled. Errors are rare and
43
+ * you want 100% visibility into failures.
44
+ *
45
+ * @defaultValue `true`
46
+ */
47
+ alwaysKeepErrors?: boolean;
48
+
49
+ /**
50
+ * Always keep requests slower than this threshold.
51
+ *
52
+ * @remarks
53
+ * Slow requests often indicate problems. This ensures you
54
+ * capture them even at low sample rates.
55
+ *
56
+ * @defaultValue `2000` (2 seconds)
57
+ */
58
+ slowRequestThresholdMs?: number;
59
+
60
+ /**
61
+ * Always keep requests matching these paths.
62
+ *
63
+ * @remarks
64
+ * Uses substring matching. Useful for critical paths like
65
+ * checkout, payment, or authentication.
66
+ *
67
+ * @example
68
+ * ```typescript
69
+ * alwaysKeepPaths: ['/api/checkout', '/api/payment', '/auth']
70
+ * ```
71
+ */
72
+ alwaysKeepPaths?: string[];
73
+
74
+ /**
75
+ * Always keep requests from these user IDs.
76
+ *
77
+ * @remarks
78
+ * Useful for debugging specific users or VIP accounts.
79
+ * Requires `user_id` to be set on the event.
80
+ *
81
+ * @example
82
+ * ```typescript
83
+ * alwaysKeepUsers: ['usr_vip_123', 'usr_debug_456']
84
+ * ```
85
+ */
86
+ alwaysKeepUsers?: string[];
87
+ }
88
+
89
+ /**
90
+ * Configuration for the wide logger middleware.
91
+ *
92
+ * @example Minimal Config
93
+ * ```typescript
94
+ * wideLogger({
95
+ * apiKey: process.env.FULLEVENT_API_KEY!,
96
+ * service: 'checkout-api',
97
+ * })
98
+ * ```
99
+ *
100
+ * @example Full Config
101
+ * ```typescript
102
+ * wideLogger({
103
+ * apiKey: process.env.FULLEVENT_API_KEY!,
104
+ * service: 'checkout-api',
105
+ * environment: 'production',
106
+ * region: 'us-east-1',
107
+ * sampling: {
108
+ * defaultRate: 0.1,
109
+ * alwaysKeepErrors: true,
110
+ * slowRequestThresholdMs: 500,
111
+ * },
112
+ * })
113
+ * ```
114
+ *
115
+ * @category Middleware
116
+ */
117
+ export interface WideLoggerConfig {
118
+ /**
119
+ * Your FullEvent API key.
120
+ *
121
+ * @remarks
122
+ * Get this from your FullEvent dashboard under Project Settings → API Keys.
123
+ */
124
+ apiKey: string;
125
+
126
+ /**
127
+ * Service name to tag all events with.
128
+ *
129
+ * @remarks
130
+ * Used for filtering and grouping in the dashboard.
131
+ * Examples: 'checkout-api', 'user-service', 'payment-worker'
132
+ */
133
+ service: string;
134
+
135
+ /**
136
+ * Base URL for the FullEvent API.
137
+ *
138
+ * @defaultValue `'https://api.fullevent.io'`
139
+ *
140
+ * @remarks
141
+ * Only override for self-hosted deployments or local development.
142
+ */
143
+ baseUrl?: string;
144
+
145
+ /**
146
+ * Environment tag for all events.
147
+ *
148
+ * @defaultValue `process.env.NODE_ENV` or `'development'`
149
+ */
150
+ environment?: string;
151
+
152
+ /**
153
+ * Region tag for all events.
154
+ *
155
+ * @remarks
156
+ * Useful for multi-region deployments. Examples: 'us-east-1', 'eu-west-1'
157
+ */
158
+ region?: string;
159
+
160
+ /**
161
+ * Sampling configuration for tail-based sampling.
162
+ *
163
+ * @see {@link SamplingConfig}
164
+ */
165
+ sampling?: SamplingConfig;
166
+ }
167
+
168
+ /**
169
+ * Type for Hono context variables.
170
+ *
171
+ * @remarks
172
+ * Use this to type your Hono app for proper TypeScript support.
173
+ *
174
+ * @example
175
+ * ```typescript
176
+ * import { Hono } from 'hono';
177
+ * import { wideLogger, WideEventVariables } from '@fullevent/node';
178
+ *
179
+ * const app = new Hono<{ Variables: WideEventVariables }>();
180
+ *
181
+ * app.use(wideLogger({ apiKey: '...', service: 'my-api' }));
182
+ *
183
+ * app.get('/', (c) => {
184
+ * const event = c.get('wideEvent'); // Properly typed!
185
+ * event.user_id = 'usr_123';
186
+ * return c.text('Hello!');
187
+ * });
188
+ * ```
189
+ *
190
+ * @category Middleware
191
+ */
192
+ export type WideEventVariables = {
193
+ /** The wide event for the current request */
194
+ wideEvent: WideEvent;
195
+ };
196
+
197
+ /**
198
+ * Determines whether to keep this event based on sampling config.
199
+ * Uses tail-based sampling: decision made AFTER request completes.
200
+ *
201
+ * @internal
202
+ */
203
+ function shouldSample(event: WideEvent, config?: SamplingConfig): boolean {
204
+ const sampling = config ?? {};
205
+ const defaultRate = sampling.defaultRate ?? 1.0;
206
+ const alwaysKeepErrors = sampling.alwaysKeepErrors ?? true;
207
+ const slowThreshold = sampling.slowRequestThresholdMs ?? 2000;
208
+
209
+ // Always keep errors (4xx/5xx or explicit error outcome)
210
+ if (alwaysKeepErrors) {
211
+ if (event.outcome === 'error') return true;
212
+ if (event.status_code && event.status_code >= 400) return true;
213
+ }
214
+
215
+ // Always keep slow requests
216
+ if (event.duration_ms && event.duration_ms > slowThreshold) return true;
217
+
218
+ // Always keep specific paths
219
+ if (sampling.alwaysKeepPaths?.some(p => event.path.includes(p))) return true;
220
+
221
+ // Always keep specific users
222
+ if (event.user_id && sampling.alwaysKeepUsers?.includes(String(event.user_id))) return true;
223
+
224
+ // Consistent Sampling based on Trace ID
225
+ // DJB2 hash for simple, string-based consistency
226
+ if (event.trace_id) {
227
+ let hash = 5381;
228
+ const str = event.trace_id;
229
+ for (let i = 0; i < str.length; i++) {
230
+ hash = ((hash << 5) + hash) + str.charCodeAt(i);
231
+ }
232
+ // Normalize to 0.0 - 1.0 (using 10000 for 4 decimal precision)
233
+ const normalized = (hash >>> 0) % 10000 / 10000;
234
+ return normalized < defaultRate;
235
+ }
236
+
237
+ // Fallback if no trace ID (shouldn't happen usually)
238
+ return Math.random() < defaultRate;
239
+ }
240
+
241
+ /**
242
+ * Hono middleware for Wide Event logging.
243
+ *
244
+ * @remarks
245
+ * Creates a single, rich event per request that captures the complete context.
246
+ * The event is initialized with request data and automatically finalized with
247
+ * status, duration, and outcome. Your handlers enrich it with business context.
248
+ *
249
+ * ## How It Works
250
+ *
251
+ * 1. **Request Start**: Middleware creates the event with request context
252
+ * 2. **Handler Runs**: Your code enriches the event via `c.get('wideEvent')`
253
+ * 3. **Request End**: Middleware adds status/duration and sends to FullEvent
254
+ *
255
+ * ## Features
256
+ *
257
+ * - **Automatic context capture**: method, path, status, duration, errors
258
+ * - **Distributed tracing**: Propagates `x-fullevent-trace-id` headers
259
+ * - **Tail-based sampling**: 100% error visibility at any sample rate
260
+ * - **Fire and forget**: Non-blocking, won't slow down your responses
261
+ *
262
+ * @param config - Middleware configuration
263
+ * @returns Hono middleware function
264
+ *
265
+ * @example Quick Start
266
+ * ```typescript
267
+ * import { Hono } from 'hono';
268
+ * import { wideLogger, WideEventVariables } from '@fullevent/node';
269
+ *
270
+ * const app = new Hono<{ Variables: WideEventVariables }>();
271
+ *
272
+ * app.use(wideLogger({
273
+ * apiKey: process.env.FULLEVENT_API_KEY!,
274
+ * service: 'checkout-api',
275
+ * }));
276
+ * ```
277
+ *
278
+ * @example Enriching Events
279
+ * ```typescript
280
+ * app.post('/checkout', async (c) => {
281
+ * const event = c.get('wideEvent');
282
+ *
283
+ * // Add user context
284
+ * event.user = {
285
+ * id: user.id,
286
+ * subscription: user.plan,
287
+ * account_age_days: daysSince(user.createdAt),
288
+ * };
289
+ *
290
+ * // Add cart context
291
+ * event.cart = {
292
+ * id: cart.id,
293
+ * item_count: cart.items.length,
294
+ * total_cents: cart.total,
295
+ * };
296
+ *
297
+ * // Add payment timing
298
+ * const paymentStart = Date.now();
299
+ * const payment = await processPayment(cart);
300
+ * event.payment = {
301
+ * provider: 'stripe',
302
+ * latency_ms: Date.now() - paymentStart,
303
+ * };
304
+ *
305
+ * return c.json({ orderId: payment.orderId });
306
+ * });
307
+ * ```
308
+ *
309
+ * @example With Sampling
310
+ * ```typescript
311
+ * app.use(wideLogger({
312
+ * apiKey: process.env.FULLEVENT_API_KEY!,
313
+ * service: 'checkout-api',
314
+ * sampling: {
315
+ * defaultRate: 0.1, // 10% of normal traffic
316
+ * alwaysKeepErrors: true, // 100% of errors
317
+ * slowRequestThresholdMs: 500, // Slow requests
318
+ * alwaysKeepPaths: ['/api/checkout'], // Critical paths
319
+ * },
320
+ * }));
321
+ * ```
322
+ *
323
+ * @category Middleware
324
+ */
325
+ export const wideLogger = (config: WideLoggerConfig) => {
326
+ const client = new FullEvent({
327
+ apiKey: config.apiKey,
328
+ baseUrl: config.baseUrl,
329
+ });
330
+
331
+ return async (c: Context<{ Variables: WideEventVariables }>, next: Next) => {
332
+ const startTime = Date.now();
333
+
334
+ // Distributed Tracing: Use existing trace ID or generate a new one
335
+ const requestId = c.req.header('x-fullevent-trace-id')
336
+ || c.req.header('x-request-id')
337
+ || crypto.randomUUID();
338
+
339
+ // Initialize the Wide Event with request context
340
+ const event: WideEvent = {
341
+ request_id: requestId,
342
+ trace_id: requestId,
343
+ timestamp: new Date().toISOString(),
344
+ method: c.req.method,
345
+ path: c.req.path,
346
+ service: config.service,
347
+ environment: config.environment || process.env.NODE_ENV || 'development',
348
+ region: config.region,
349
+ };
350
+
351
+ // Make the event accessible to handlers for enrichment
352
+ c.set('wideEvent', event);
353
+
354
+ // Propagate the trace ID in response headers
355
+ c.res.headers.set('x-fullevent-trace-id', requestId);
356
+
357
+ try {
358
+ await next();
359
+ event.status_code = c.res.status;
360
+ event.outcome = c.res.status >= 400 ? 'error' : 'success';
361
+ } catch (err: unknown) {
362
+ event.status_code = 500;
363
+ event.outcome = 'error';
364
+
365
+ if (err instanceof Error) {
366
+ event.error = {
367
+ type: err.name || 'Error',
368
+ message: err.message || String(err),
369
+ stack: err.stack,
370
+ };
371
+ } else {
372
+ event.error = {
373
+ type: 'Error',
374
+ message: String(err),
375
+ };
376
+ }
377
+ throw err;
378
+ } finally {
379
+ event.duration_ms = Date.now() - startTime;
380
+
381
+ // Only send if we have an API key and event passes sampling
382
+ if (config.apiKey && shouldSample(event, config.sampling)) {
383
+ // Send to FullEvent API (fire and forget - don't block response)
384
+ client.ingest(
385
+ `${event.method} ${event.path}`,
386
+ event,
387
+ event.timestamp
388
+ ).catch(err => {
389
+ // Log but don't throw - observability shouldn't break your app
390
+ console.error('[FullEvent] Failed to send event:', err);
391
+ });
392
+ }
393
+ }
394
+ };
395
+ };
package/tsconfig.json ADDED
@@ -0,0 +1,11 @@
1
+ {
2
+ "extends": "../../tsconfig.json",
3
+ "include": [
4
+ "src/**/*"
5
+ ],
6
+ "compilerOptions": {
7
+ "module": "esnext",
8
+ "moduleResolution": "bundler",
9
+ "incremental": false
10
+ }
11
+ }