@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.mjs ADDED
@@ -0,0 +1,543 @@
1
+ // src/client.ts
2
+ var FullEvent = class {
3
+ /**
4
+ * Creates a new FullEvent client instance.
5
+ *
6
+ * @param config - Configuration options
7
+ */
8
+ constructor(config) {
9
+ this.apiKey = config.apiKey;
10
+ this.baseUrl = config.baseUrl || "https://api.fullevent.io";
11
+ if (config.ping) {
12
+ this.ping().catch((err) => {
13
+ console.error("[FullEvent SDK] Auto-ping failed:", err);
14
+ });
15
+ }
16
+ }
17
+ /**
18
+ * Ingest a generic event with any properties.
19
+ *
20
+ * @param event - Event name/type (e.g., 'user.signup', 'order.completed')
21
+ * @param properties - Key-value pairs of event data
22
+ * @param timestamp - Optional ISO timestamp (defaults to now)
23
+ * @returns Promise resolving to success/error status
24
+ *
25
+ * @remarks
26
+ * Events are processed asynchronously. The promise resolves when
27
+ * the event is accepted by the API, not when it's fully processed.
28
+ *
29
+ * @example
30
+ * ```typescript
31
+ * await client.ingest('checkout.completed', {
32
+ * order_id: 'ord_123',
33
+ * total_cents: 9999,
34
+ * items: 3,
35
+ * user: {
36
+ * id: 'usr_456',
37
+ * plan: 'premium',
38
+ * },
39
+ * });
40
+ * ```
41
+ */
42
+ async ingest(event, properties = {}, timestamp) {
43
+ try {
44
+ const response = await fetch(`${this.baseUrl}/ingest`, {
45
+ method: "POST",
46
+ headers: {
47
+ "Content-Type": "application/json",
48
+ "Authorization": `Bearer ${this.apiKey}`
49
+ },
50
+ body: JSON.stringify({
51
+ event,
52
+ properties,
53
+ timestamp: timestamp || (/* @__PURE__ */ new Date()).toISOString()
54
+ })
55
+ });
56
+ if (!response.ok) {
57
+ const error = await response.json();
58
+ console.error("[FullEvent SDK] Ingestion failed:", error);
59
+ return { success: false, error };
60
+ }
61
+ return { success: true };
62
+ } catch (error) {
63
+ console.error("[FullEvent SDK] Network error during ingestion:", error);
64
+ return { success: false, error };
65
+ }
66
+ }
67
+ /**
68
+ * Ingest an HTTP request event with typed properties.
69
+ *
70
+ * @param properties - HTTP request properties with optional custom data
71
+ * @param timestamp - Optional ISO timestamp (defaults to now)
72
+ * @returns Promise resolving to success/error status
73
+ *
74
+ * @remarks
75
+ * This is a convenience method that provides TypeScript autocomplete
76
+ * for standard HTTP properties. Under the hood, it calls `ingest()`
77
+ * with the event type `'http_request'`.
78
+ *
79
+ * @example
80
+ * ```typescript
81
+ * await client.ingestHttpRequest({
82
+ * status_code: 200,
83
+ * method: 'POST',
84
+ * path: '/api/checkout',
85
+ * duration_ms: 234,
86
+ * outcome: 'success',
87
+ * // Custom properties
88
+ * user_id: 'usr_123',
89
+ * });
90
+ * ```
91
+ */
92
+ async ingestHttpRequest(properties, timestamp) {
93
+ return this.ingest("http_request", properties, timestamp);
94
+ }
95
+ /**
96
+ * Ping the FullEvent API to verify connection.
97
+ *
98
+ * @returns Promise resolving to connection status with latency
99
+ *
100
+ * @remarks
101
+ * Use this method to verify your SDK is correctly configured.
102
+ * It sends a lightweight ping event and measures round-trip latency.
103
+ * Commonly used during setup or in health check endpoints.
104
+ *
105
+ * @example Basic ping
106
+ * ```typescript
107
+ * const result = await client.ping();
108
+ * if (result.success) {
109
+ * console.log(`Connected! Latency: ${result.latency_ms}ms`);
110
+ * } else {
111
+ * console.error('Connection failed:', result.error);
112
+ * }
113
+ * ```
114
+ *
115
+ * @example Health check endpoint
116
+ * ```typescript
117
+ * app.get('/health', async (c) => {
118
+ * const ping = await fullevent.ping();
119
+ * return c.json({
120
+ * status: ping.success ? 'healthy' : 'degraded',
121
+ * fullevent: ping,
122
+ * });
123
+ * });
124
+ * ```
125
+ */
126
+ async ping() {
127
+ const start = Date.now();
128
+ try {
129
+ const result = await this.ingest("fullevent.ping", {
130
+ // Standard wide event properties
131
+ status_code: 200,
132
+ outcome: "success",
133
+ duration_ms: 0,
134
+ // Will be updated after
135
+ // SDK info
136
+ sdk: "@fullevent/node-sdk",
137
+ sdk_version: "1.0.0",
138
+ // Runtime info
139
+ runtime: typeof process !== "undefined" ? "node" : "browser",
140
+ node_version: typeof process !== "undefined" ? process.version : void 0,
141
+ platform: typeof process !== "undefined" ? process.platform : "browser",
142
+ // Ping metadata
143
+ ping_type: "connection_test",
144
+ message: "\u{1F389} Your first event! FullEvent is connected and ready."
145
+ });
146
+ const latency_ms = Date.now() - start;
147
+ if (result.success) {
148
+ return { success: true, latency_ms };
149
+ } else {
150
+ return { success: false, latency_ms, error: result.error };
151
+ }
152
+ } catch (error) {
153
+ const latency_ms = Date.now() - start;
154
+ return { success: false, latency_ms, error };
155
+ }
156
+ }
157
+ };
158
+
159
+ // src/middleware/hono.ts
160
+ function shouldSample(event, config) {
161
+ const sampling = config ?? {};
162
+ const defaultRate = sampling.defaultRate ?? 1;
163
+ const alwaysKeepErrors = sampling.alwaysKeepErrors ?? true;
164
+ const slowThreshold = sampling.slowRequestThresholdMs ?? 2e3;
165
+ if (alwaysKeepErrors) {
166
+ if (event.outcome === "error") return true;
167
+ if (event.status_code && event.status_code >= 400) return true;
168
+ }
169
+ if (event.duration_ms && event.duration_ms > slowThreshold) return true;
170
+ if (sampling.alwaysKeepPaths?.some((p) => event.path.includes(p))) return true;
171
+ if (event.user_id && sampling.alwaysKeepUsers?.includes(String(event.user_id))) return true;
172
+ if (event.trace_id) {
173
+ let hash = 5381;
174
+ const str = event.trace_id;
175
+ for (let i = 0; i < str.length; i++) {
176
+ hash = (hash << 5) + hash + str.charCodeAt(i);
177
+ }
178
+ const normalized = (hash >>> 0) % 1e4 / 1e4;
179
+ return normalized < defaultRate;
180
+ }
181
+ return Math.random() < defaultRate;
182
+ }
183
+ var wideLogger = (config) => {
184
+ const client = new FullEvent({
185
+ apiKey: config.apiKey,
186
+ baseUrl: config.baseUrl
187
+ });
188
+ return async (c, next) => {
189
+ const startTime = Date.now();
190
+ const requestId = c.req.header("x-fullevent-trace-id") || c.req.header("x-request-id") || crypto.randomUUID();
191
+ const event = {
192
+ request_id: requestId,
193
+ trace_id: requestId,
194
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
195
+ method: c.req.method,
196
+ path: c.req.path,
197
+ service: config.service,
198
+ environment: config.environment || process.env.NODE_ENV || "development",
199
+ region: config.region
200
+ };
201
+ c.set("wideEvent", event);
202
+ c.res.headers.set("x-fullevent-trace-id", requestId);
203
+ try {
204
+ await next();
205
+ event.status_code = c.res.status;
206
+ event.outcome = c.res.status >= 400 ? "error" : "success";
207
+ } catch (err) {
208
+ event.status_code = 500;
209
+ event.outcome = "error";
210
+ if (err instanceof Error) {
211
+ event.error = {
212
+ type: err.name || "Error",
213
+ message: err.message || String(err),
214
+ stack: err.stack
215
+ };
216
+ } else {
217
+ event.error = {
218
+ type: "Error",
219
+ message: String(err)
220
+ };
221
+ }
222
+ throw err;
223
+ } finally {
224
+ event.duration_ms = Date.now() - startTime;
225
+ if (config.apiKey && shouldSample(event, config.sampling)) {
226
+ client.ingest(
227
+ `${event.method} ${event.path}`,
228
+ event,
229
+ event.timestamp
230
+ ).catch((err) => {
231
+ console.error("[FullEvent] Failed to send event:", err);
232
+ });
233
+ }
234
+ }
235
+ };
236
+ };
237
+
238
+ // src/middleware/express.ts
239
+ function shouldSample2(event, config) {
240
+ const sampling = config ?? {};
241
+ const defaultRate = sampling.defaultRate ?? 1;
242
+ const alwaysKeepErrors = sampling.alwaysKeepErrors ?? true;
243
+ const slowThreshold = sampling.slowRequestThresholdMs ?? 2e3;
244
+ if (alwaysKeepErrors) {
245
+ if (event.outcome === "error") return true;
246
+ if (event.status_code && event.status_code >= 400) return true;
247
+ }
248
+ if (event.duration_ms && event.duration_ms > slowThreshold) return true;
249
+ if (sampling.alwaysKeepPaths?.some((p) => event.path.includes(p))) return true;
250
+ if (event.user_id && sampling.alwaysKeepUsers?.includes(String(event.user_id))) return true;
251
+ if (event.trace_id) {
252
+ let hash = 5381;
253
+ const str = event.trace_id;
254
+ for (let i = 0; i < str.length; i++) {
255
+ hash = (hash << 5) + hash + str.charCodeAt(i);
256
+ }
257
+ const normalized = (hash >>> 0) % 1e4 / 1e4;
258
+ return normalized < defaultRate;
259
+ }
260
+ return Math.random() < defaultRate;
261
+ }
262
+ function expressWideLogger(config) {
263
+ const client = new FullEvent({
264
+ apiKey: config.apiKey,
265
+ baseUrl: config.baseUrl
266
+ });
267
+ return (req, res, next) => {
268
+ const startTime = Date.now();
269
+ const requestId = req.headers["x-fullevent-trace-id"] || req.headers["x-request-id"] || crypto.randomUUID();
270
+ const event = {
271
+ request_id: requestId,
272
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
273
+ method: req.method,
274
+ path: req.path,
275
+ service: config.service,
276
+ environment: config.environment || process.env.NODE_ENV || "development",
277
+ region: config.region
278
+ };
279
+ req.wideEvent = event;
280
+ res.setHeader("x-fullevent-trace-id", requestId);
281
+ res.on("finish", () => {
282
+ event.status_code = res.statusCode;
283
+ event.outcome = res.statusCode >= 400 ? "error" : "success";
284
+ event.duration_ms = Date.now() - startTime;
285
+ if (config.apiKey && shouldSample2(event, config.sampling)) {
286
+ client.ingest(
287
+ `${event.method} ${event.path}`,
288
+ event,
289
+ event.timestamp
290
+ ).catch((err) => {
291
+ console.error("[FullEvent] Failed to send event:", err);
292
+ });
293
+ }
294
+ });
295
+ next();
296
+ };
297
+ }
298
+
299
+ // src/builder.ts
300
+ var WideEventBuilder = class {
301
+ /**
302
+ * Creates a new builder wrapping the given event.
303
+ *
304
+ * @param event - The wide event to enrich
305
+ */
306
+ constructor(event) {
307
+ this.event = event;
308
+ }
309
+ /**
310
+ * Returns the underlying event object for direct access.
311
+ *
312
+ * @returns The wrapped WideEvent
313
+ *
314
+ * @example
315
+ * ```typescript
316
+ * const event = builder.getEvent();
317
+ * console.log(event.user_id);
318
+ * ```
319
+ */
320
+ getEvent() {
321
+ return this.event;
322
+ }
323
+ /**
324
+ * Sets any key-value pair on the event.
325
+ *
326
+ * @param key - Property name
327
+ * @param value - Property value (any type)
328
+ * @returns `this` for chaining
329
+ *
330
+ * @example
331
+ * ```typescript
332
+ * builder
333
+ * .set('order_id', 'ord_123')
334
+ * .set('llm_model', 'gpt-4')
335
+ * .set('tokens_used', 1500);
336
+ * ```
337
+ */
338
+ set(key, value) {
339
+ this.event[key] = value;
340
+ return this;
341
+ }
342
+ /**
343
+ * Sets a named context object on the event.
344
+ *
345
+ * @remarks
346
+ * This is the primary method for adding structured business context.
347
+ * Each context is a nested object that groups related properties.
348
+ *
349
+ * @param name - Context name (e.g., 'user', 'cart', 'payment')
350
+ * @param data - Context data as key-value pairs
351
+ * @returns `this` for chaining
352
+ *
353
+ * @example
354
+ * ```typescript
355
+ * builder
356
+ * .setContext('user', {
357
+ * id: 'user_456',
358
+ * subscription: 'premium',
359
+ * account_age_days: 847,
360
+ * })
361
+ * .setContext('cart', {
362
+ * id: 'cart_xyz',
363
+ * item_count: 3,
364
+ * total_cents: 15999,
365
+ * })
366
+ * .setContext('payment', {
367
+ * method: 'card',
368
+ * provider: 'stripe',
369
+ * });
370
+ * ```
371
+ */
372
+ setContext(name, data) {
373
+ this.event[name] = data;
374
+ return this;
375
+ }
376
+ /**
377
+ * Merges additional fields into an existing context object.
378
+ *
379
+ * @remarks
380
+ * Useful for progressively building context throughout the request.
381
+ * If the context doesn't exist, it will be created.
382
+ *
383
+ * @param name - Context name to merge into
384
+ * @param data - Additional data to merge
385
+ * @returns `this` for chaining
386
+ *
387
+ * @example
388
+ * ```typescript
389
+ * // Initial payment context
390
+ * builder.setContext('payment', { method: 'card', provider: 'stripe' });
391
+ *
392
+ * // After payment completes, add timing
393
+ * builder.mergeContext('payment', {
394
+ * latency_ms: Date.now() - paymentStart,
395
+ * attempt: 1,
396
+ * transaction_id: 'txn_123',
397
+ * });
398
+ *
399
+ * // Result: { method, provider, latency_ms, attempt, transaction_id }
400
+ * ```
401
+ */
402
+ mergeContext(name, data) {
403
+ const existing = this.event[name];
404
+ if (existing && typeof existing === "object" && !Array.isArray(existing)) {
405
+ this.event[name] = { ...existing, ...data };
406
+ } else {
407
+ this.event[name] = data;
408
+ }
409
+ return this;
410
+ }
411
+ /**
412
+ * Sets the user ID on the event.
413
+ *
414
+ * @remarks
415
+ * This sets the top-level `user_id` field, which is commonly used
416
+ * for filtering and user-centric analytics. For richer user context,
417
+ * use `setContext('user', {...})` instead or in addition.
418
+ *
419
+ * @param userId - The user identifier
420
+ * @returns `this` for chaining
421
+ *
422
+ * @example
423
+ * ```typescript
424
+ * builder.setUser('usr_123');
425
+ *
426
+ * // For richer context, also set a user object:
427
+ * builder.setContext('user', {
428
+ * id: 'usr_123',
429
+ * plan: 'premium',
430
+ * ltv_cents: 50000,
431
+ * });
432
+ * ```
433
+ */
434
+ setUser(userId) {
435
+ this.event.user_id = userId;
436
+ return this;
437
+ }
438
+ /**
439
+ * Captures an error with structured details.
440
+ *
441
+ * @remarks
442
+ * Automatically sets `outcome` to 'error'. Accepts either a native
443
+ * Error object or a custom error object with additional fields.
444
+ *
445
+ * @param err - Error object or custom error details
446
+ * @returns `this` for chaining
447
+ *
448
+ * @example Native Error
449
+ * ```typescript
450
+ * try {
451
+ * await riskyOperation();
452
+ * } catch (err) {
453
+ * builder.setError(err);
454
+ * }
455
+ * ```
456
+ *
457
+ * @example Custom Error with Context
458
+ * ```typescript
459
+ * builder.setError({
460
+ * type: 'PaymentError',
461
+ * message: 'Card declined by issuer',
462
+ * code: 'card_declined',
463
+ * stripe_decline_code: 'insufficient_funds',
464
+ * card_brand: 'visa',
465
+ * card_last4: '4242',
466
+ * });
467
+ * ```
468
+ */
469
+ setError(err) {
470
+ this.event.outcome = "error";
471
+ if (err instanceof Error) {
472
+ this.event.error = {
473
+ type: err.name,
474
+ message: err.message,
475
+ stack: err.stack
476
+ };
477
+ } else {
478
+ const { type, message, code, stack, ...extra } = err;
479
+ this.event.error = {
480
+ type: type || "Error",
481
+ message,
482
+ code,
483
+ stack,
484
+ ...extra
485
+ };
486
+ }
487
+ return this;
488
+ }
489
+ /**
490
+ * Sets the HTTP status code and outcome.
491
+ *
492
+ * @remarks
493
+ * Automatically sets `outcome` based on the status code:
494
+ * - `< 400` → 'success'
495
+ * - `>= 400` → 'error'
496
+ *
497
+ * @param code - HTTP status code
498
+ * @returns `this` for chaining
499
+ *
500
+ * @example
501
+ * ```typescript
502
+ * builder.setStatus(404); // outcome = 'error'
503
+ * builder.setStatus(200); // outcome = 'success'
504
+ * ```
505
+ */
506
+ setStatus(code) {
507
+ this.event.status_code = code;
508
+ this.event.outcome = code >= 400 ? "error" : "success";
509
+ return this;
510
+ }
511
+ /**
512
+ * Records timing for a specific operation.
513
+ *
514
+ * @remarks
515
+ * A convenience method for calculating and setting duration values.
516
+ * The value is calculated as `Date.now() - startTime`.
517
+ *
518
+ * @param key - Property name for the timing (e.g., 'db_latency_ms')
519
+ * @param startTime - Start timestamp from `Date.now()`
520
+ * @returns `this` for chaining
521
+ *
522
+ * @example
523
+ * ```typescript
524
+ * const dbStart = Date.now();
525
+ * const result = await db.query('SELECT ...');
526
+ * builder.setTiming('db_latency_ms', dbStart);
527
+ *
528
+ * const paymentStart = Date.now();
529
+ * await processPayment();
530
+ * builder.setTiming('payment_latency_ms', paymentStart);
531
+ * ```
532
+ */
533
+ setTiming(key, startTime) {
534
+ this.event[key] = Date.now() - startTime;
535
+ return this;
536
+ }
537
+ };
538
+ export {
539
+ FullEvent,
540
+ WideEventBuilder,
541
+ expressWideLogger,
542
+ wideLogger
543
+ };
package/package.json ADDED
@@ -0,0 +1,23 @@
1
+ {
2
+ "name": "@fullevent/node",
3
+ "version": "0.0.1",
4
+ "main": "./dist/index.js",
5
+ "module": "./dist/index.mjs",
6
+ "types": "./dist/index.d.ts",
7
+ "scripts": {
8
+ "build": "tsup src/index.ts --format cjs,esm --dts",
9
+ "dev": "tsup src/index.ts --format cjs,esm --dts --watch",
10
+ "lint": "tsc",
11
+ "generate:docs": "tsx scripts/generate-docs.ts"
12
+ },
13
+ "devDependencies": {
14
+ "@types/express": "^5.0.6",
15
+ "@types/node": "^20.0.0",
16
+ "tsup": "^8.0.0",
17
+ "tsx": "^4.7.0",
18
+ "typescript": "^5.0.0"
19
+ },
20
+ "peerDependencies": {
21
+ "hono": "^4.0.0"
22
+ }
23
+ }