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