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