@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/dist/index.d.mts +1096 -0
- package/dist/index.d.ts +1096 -0
- package/dist/index.js +573 -0
- package/dist/index.mjs +543 -0
- package/package.json +23 -0
- package/scripts/generate-docs.ts +917 -0
- package/src/builder.ts +307 -0
- package/src/client.ts +325 -0
- package/src/index.ts +272 -0
- package/src/middleware/express.ts +193 -0
- package/src/middleware/hono.ts +395 -0
- package/tsconfig.json +11 -0
|
@@ -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
|
+
};
|