@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/src/builder.ts ADDED
@@ -0,0 +1,307 @@
1
+ import { WideEvent } from './index';
2
+
3
+ /**
4
+ * A fluent builder for enriching wide events throughout the request lifecycle.
5
+ *
6
+ * @remarks
7
+ * While you can modify the event object directly (and that's perfectly fine),
8
+ * the builder provides chainable methods for cleaner code and useful helpers.
9
+ *
10
+ * ## Two Approaches
11
+ *
12
+ * **Direct access (simple):**
13
+ * ```typescript
14
+ * const event = c.get('wideEvent');
15
+ * event.user = { id: user.id, plan: user.plan };
16
+ * event.cart = { total: cart.total, items: cart.items.length };
17
+ * ```
18
+ *
19
+ * **Builder pattern (chainable):**
20
+ * ```typescript
21
+ * new WideEventBuilder(c.get('wideEvent'))
22
+ * .setContext('user', { id: user.id, plan: user.plan })
23
+ * .setContext('cart', { total: cart.total, items: cart.items.length })
24
+ * .setTiming('db_latency_ms', dbStart);
25
+ * ```
26
+ *
27
+ * @example Full Example
28
+ * ```typescript
29
+ * const builder = new WideEventBuilder(c.get('wideEvent'));
30
+ *
31
+ * builder
32
+ * .setUser(user.id)
33
+ * .setContext('user', {
34
+ * subscription: user.plan,
35
+ * account_age_days: daysSince(user.createdAt),
36
+ * lifetime_value_cents: user.ltv,
37
+ * })
38
+ * .setContext('cart', {
39
+ * id: cart.id,
40
+ * item_count: cart.items.length,
41
+ * total_cents: cart.total,
42
+ * });
43
+ *
44
+ * // Later, after payment processing
45
+ * builder
46
+ * .setContext('payment', { method: 'card', provider: 'stripe' })
47
+ * .setTiming('payment_latency_ms', paymentStart);
48
+ *
49
+ * if (paymentError) {
50
+ * builder.setError({
51
+ * type: 'PaymentError',
52
+ * code: paymentError.code,
53
+ * message: paymentError.message,
54
+ * stripe_decline_code: paymentError.declineCode,
55
+ * });
56
+ * }
57
+ * ```
58
+ *
59
+ * @category Builder
60
+ */
61
+ export class WideEventBuilder {
62
+ /**
63
+ * Creates a new builder wrapping the given event.
64
+ *
65
+ * @param event - The wide event to enrich
66
+ */
67
+ constructor(private event: WideEvent) {}
68
+
69
+ /**
70
+ * Returns the underlying event object for direct access.
71
+ *
72
+ * @returns The wrapped WideEvent
73
+ *
74
+ * @example
75
+ * ```typescript
76
+ * const event = builder.getEvent();
77
+ * console.log(event.user_id);
78
+ * ```
79
+ */
80
+ getEvent(): WideEvent {
81
+ return this.event;
82
+ }
83
+
84
+ /**
85
+ * Sets any key-value pair on the event.
86
+ *
87
+ * @param key - Property name
88
+ * @param value - Property value (any type)
89
+ * @returns `this` for chaining
90
+ *
91
+ * @example
92
+ * ```typescript
93
+ * builder
94
+ * .set('order_id', 'ord_123')
95
+ * .set('llm_model', 'gpt-4')
96
+ * .set('tokens_used', 1500);
97
+ * ```
98
+ */
99
+ set(key: string, value: unknown): this {
100
+ this.event[key] = value;
101
+ return this;
102
+ }
103
+
104
+ /**
105
+ * Sets a named context object on the event.
106
+ *
107
+ * @remarks
108
+ * This is the primary method for adding structured business context.
109
+ * Each context is a nested object that groups related properties.
110
+ *
111
+ * @param name - Context name (e.g., 'user', 'cart', 'payment')
112
+ * @param data - Context data as key-value pairs
113
+ * @returns `this` for chaining
114
+ *
115
+ * @example
116
+ * ```typescript
117
+ * builder
118
+ * .setContext('user', {
119
+ * id: 'user_456',
120
+ * subscription: 'premium',
121
+ * account_age_days: 847,
122
+ * })
123
+ * .setContext('cart', {
124
+ * id: 'cart_xyz',
125
+ * item_count: 3,
126
+ * total_cents: 15999,
127
+ * })
128
+ * .setContext('payment', {
129
+ * method: 'card',
130
+ * provider: 'stripe',
131
+ * });
132
+ * ```
133
+ */
134
+ setContext(name: string, data: Record<string, unknown>): this {
135
+ this.event[name] = data;
136
+ return this;
137
+ }
138
+
139
+ /**
140
+ * Merges additional fields into an existing context object.
141
+ *
142
+ * @remarks
143
+ * Useful for progressively building context throughout the request.
144
+ * If the context doesn't exist, it will be created.
145
+ *
146
+ * @param name - Context name to merge into
147
+ * @param data - Additional data to merge
148
+ * @returns `this` for chaining
149
+ *
150
+ * @example
151
+ * ```typescript
152
+ * // Initial payment context
153
+ * builder.setContext('payment', { method: 'card', provider: 'stripe' });
154
+ *
155
+ * // After payment completes, add timing
156
+ * builder.mergeContext('payment', {
157
+ * latency_ms: Date.now() - paymentStart,
158
+ * attempt: 1,
159
+ * transaction_id: 'txn_123',
160
+ * });
161
+ *
162
+ * // Result: { method, provider, latency_ms, attempt, transaction_id }
163
+ * ```
164
+ */
165
+ mergeContext(name: string, data: Record<string, unknown>): this {
166
+ const existing = this.event[name];
167
+ if (existing && typeof existing === 'object' && !Array.isArray(existing)) {
168
+ this.event[name] = { ...(existing as Record<string, unknown>), ...data };
169
+ } else {
170
+ this.event[name] = data;
171
+ }
172
+ return this;
173
+ }
174
+
175
+ /**
176
+ * Sets the user ID on the event.
177
+ *
178
+ * @remarks
179
+ * This sets the top-level `user_id` field, which is commonly used
180
+ * for filtering and user-centric analytics. For richer user context,
181
+ * use `setContext('user', {...})` instead or in addition.
182
+ *
183
+ * @param userId - The user identifier
184
+ * @returns `this` for chaining
185
+ *
186
+ * @example
187
+ * ```typescript
188
+ * builder.setUser('usr_123');
189
+ *
190
+ * // For richer context, also set a user object:
191
+ * builder.setContext('user', {
192
+ * id: 'usr_123',
193
+ * plan: 'premium',
194
+ * ltv_cents: 50000,
195
+ * });
196
+ * ```
197
+ */
198
+ setUser(userId: string): this {
199
+ this.event.user_id = userId;
200
+ return this;
201
+ }
202
+
203
+ /**
204
+ * Captures an error with structured details.
205
+ *
206
+ * @remarks
207
+ * Automatically sets `outcome` to 'error'. Accepts either a native
208
+ * Error object or a custom error object with additional fields.
209
+ *
210
+ * @param err - Error object or custom error details
211
+ * @returns `this` for chaining
212
+ *
213
+ * @example Native Error
214
+ * ```typescript
215
+ * try {
216
+ * await riskyOperation();
217
+ * } catch (err) {
218
+ * builder.setError(err);
219
+ * }
220
+ * ```
221
+ *
222
+ * @example Custom Error with Context
223
+ * ```typescript
224
+ * builder.setError({
225
+ * type: 'PaymentError',
226
+ * message: 'Card declined by issuer',
227
+ * code: 'card_declined',
228
+ * stripe_decline_code: 'insufficient_funds',
229
+ * card_brand: 'visa',
230
+ * card_last4: '4242',
231
+ * });
232
+ * ```
233
+ */
234
+ setError(
235
+ err: Error | { type?: string; message: string; code?: string; stack?: string; [key: string]: unknown }
236
+ ): this {
237
+ this.event.outcome = 'error';
238
+
239
+ if (err instanceof Error) {
240
+ this.event.error = {
241
+ type: err.name,
242
+ message: err.message,
243
+ stack: err.stack,
244
+ };
245
+ } else {
246
+ const { type, message, code, stack, ...extra } = err;
247
+ this.event.error = {
248
+ type: type || 'Error',
249
+ message,
250
+ code,
251
+ stack,
252
+ ...extra,
253
+ };
254
+ }
255
+ return this;
256
+ }
257
+
258
+ /**
259
+ * Sets the HTTP status code and outcome.
260
+ *
261
+ * @remarks
262
+ * Automatically sets `outcome` based on the status code:
263
+ * - `< 400` → 'success'
264
+ * - `>= 400` → 'error'
265
+ *
266
+ * @param code - HTTP status code
267
+ * @returns `this` for chaining
268
+ *
269
+ * @example
270
+ * ```typescript
271
+ * builder.setStatus(404); // outcome = 'error'
272
+ * builder.setStatus(200); // outcome = 'success'
273
+ * ```
274
+ */
275
+ setStatus(code: number): this {
276
+ this.event.status_code = code;
277
+ this.event.outcome = code >= 400 ? 'error' : 'success';
278
+ return this;
279
+ }
280
+
281
+ /**
282
+ * Records timing for a specific operation.
283
+ *
284
+ * @remarks
285
+ * A convenience method for calculating and setting duration values.
286
+ * The value is calculated as `Date.now() - startTime`.
287
+ *
288
+ * @param key - Property name for the timing (e.g., 'db_latency_ms')
289
+ * @param startTime - Start timestamp from `Date.now()`
290
+ * @returns `this` for chaining
291
+ *
292
+ * @example
293
+ * ```typescript
294
+ * const dbStart = Date.now();
295
+ * const result = await db.query('SELECT ...');
296
+ * builder.setTiming('db_latency_ms', dbStart);
297
+ *
298
+ * const paymentStart = Date.now();
299
+ * await processPayment();
300
+ * builder.setTiming('payment_latency_ms', paymentStart);
301
+ * ```
302
+ */
303
+ setTiming(key: string, startTime: number): this {
304
+ this.event[key] = Date.now() - startTime;
305
+ return this;
306
+ }
307
+ }
package/src/client.ts ADDED
@@ -0,0 +1,325 @@
1
+ import { WideEvent } from './index';
2
+
3
+ /**
4
+ * Configuration options for the FullEvent client.
5
+ *
6
+ * @example
7
+ * ```typescript
8
+ * const client = new FullEvent({
9
+ * apiKey: process.env.FULLEVENT_API_KEY!,
10
+ * baseUrl: 'https://api.fullevent.io', // optional
11
+ * ping: true, // send a test ping on initialization
12
+ * });
13
+ * ```
14
+ *
15
+ * @category Client
16
+ */
17
+ export interface FullEventConfig {
18
+ /**
19
+ * Your FullEvent project API key.
20
+ *
21
+ * @remarks
22
+ * Get this from your FullEvent dashboard under Project Settings → API Keys.
23
+ */
24
+ apiKey: string;
25
+
26
+ /**
27
+ * Base URL for the FullEvent API.
28
+ *
29
+ * @defaultValue `'https://api.fullevent.io'`
30
+ *
31
+ * @remarks
32
+ * Only override this for self-hosted deployments or local development.
33
+ */
34
+ baseUrl?: string;
35
+
36
+ /**
37
+ * Send a ping event on initialization to verify connection.
38
+ *
39
+ * @defaultValue `false`
40
+ *
41
+ * @remarks
42
+ * Set to `true` during initial setup to verify your SDK configuration
43
+ * is working correctly. The ping is sent asynchronously and won't block
44
+ * your application startup.
45
+ */
46
+ ping?: boolean;
47
+ }
48
+
49
+ /**
50
+ * Standard properties for HTTP request events.
51
+ *
52
+ * These properties are automatically extracted by FullEvent for first-class
53
+ * filtering, dashboards, and error rate calculations.
54
+ *
55
+ * @example
56
+ * ```typescript
57
+ * const props: HttpRequestProperties = {
58
+ * status_code: 200,
59
+ * method: 'POST',
60
+ * path: '/api/checkout',
61
+ * duration_ms: 234,
62
+ * outcome: 'success',
63
+ * // Plus any custom properties
64
+ * user_id: 'usr_123',
65
+ * order_total: 9999,
66
+ * };
67
+ * ```
68
+ *
69
+ * @category Client
70
+ */
71
+ export interface HttpRequestProperties {
72
+ /**
73
+ * HTTP status code (e.g., 200, 404, 500).
74
+ *
75
+ * @remarks
76
+ * Used for automatic error rate calculations:
77
+ * - `< 400` = success
78
+ * - `>= 400` = error
79
+ */
80
+ status_code: number;
81
+
82
+ /**
83
+ * HTTP method (GET, POST, PUT, DELETE, etc.)
84
+ */
85
+ method?: string;
86
+
87
+ /**
88
+ * Request path (e.g., `/api/users`, `/checkout`)
89
+ */
90
+ path?: string;
91
+
92
+ /**
93
+ * Request duration in milliseconds.
94
+ */
95
+ duration_ms?: number;
96
+
97
+ /**
98
+ * Explicit success/error indicator.
99
+ *
100
+ * @remarks
101
+ * If set, this takes precedence over `status_code` for error rate calculations.
102
+ */
103
+ outcome?: 'success' | 'error';
104
+
105
+ /**
106
+ * Additional custom properties.
107
+ *
108
+ * @remarks
109
+ * Add any context relevant to your application.
110
+ */
111
+ [key: string]: unknown;
112
+ }
113
+
114
+ /**
115
+ * The main FullEvent client for ingesting events.
116
+ *
117
+ * @remarks
118
+ * Use this client to send events directly from your application.
119
+ * For automatic request logging, see the middleware integrations:
120
+ * - {@link wideLogger} for Hono
121
+ * - {@link expressWideLogger} for Express
122
+ *
123
+ * @example Basic Usage
124
+ * ```typescript
125
+ * import { FullEvent } from '@fullevent/node';
126
+ *
127
+ * const client = new FullEvent({
128
+ * apiKey: process.env.FULLEVENT_API_KEY!,
129
+ * });
130
+ *
131
+ * // Ingest a simple event
132
+ * await client.ingest('user.signup', {
133
+ * plan: 'pro',
134
+ * referral: 'newsletter',
135
+ * });
136
+ * ```
137
+ *
138
+ * @example Fire and Forget
139
+ * ```typescript
140
+ * // Don't await if you don't want to block
141
+ * client.ingest('page.view', { path: '/home' })
142
+ * .catch(err => console.error('FullEvent error:', err));
143
+ * ```
144
+ *
145
+ * @category Client
146
+ */
147
+ export class FullEvent {
148
+ private apiKey: string;
149
+ private baseUrl: string;
150
+
151
+ /**
152
+ * Creates a new FullEvent client instance.
153
+ *
154
+ * @param config - Configuration options
155
+ */
156
+ constructor(config: FullEventConfig) {
157
+ this.apiKey = config.apiKey;
158
+ this.baseUrl = config.baseUrl || 'https://api.fullevent.io';
159
+
160
+ // Auto-ping if enabled
161
+ if (config.ping) {
162
+ this.ping().catch((err) => {
163
+ console.error('[FullEvent SDK] Auto-ping failed:', err);
164
+ });
165
+ }
166
+ }
167
+
168
+ /**
169
+ * Ingest a generic event with any properties.
170
+ *
171
+ * @param event - Event name/type (e.g., 'user.signup', 'order.completed')
172
+ * @param properties - Key-value pairs of event data
173
+ * @param timestamp - Optional ISO timestamp (defaults to now)
174
+ * @returns Promise resolving to success/error status
175
+ *
176
+ * @remarks
177
+ * Events are processed asynchronously. The promise resolves when
178
+ * the event is accepted by the API, not when it's fully processed.
179
+ *
180
+ * @example
181
+ * ```typescript
182
+ * await client.ingest('checkout.completed', {
183
+ * order_id: 'ord_123',
184
+ * total_cents: 9999,
185
+ * items: 3,
186
+ * user: {
187
+ * id: 'usr_456',
188
+ * plan: 'premium',
189
+ * },
190
+ * });
191
+ * ```
192
+ */
193
+ async ingest(
194
+ event: string,
195
+ properties: Record<string, unknown> = {},
196
+ timestamp?: string
197
+ ): Promise<{ success: boolean; error?: unknown }> {
198
+ try {
199
+ const response = await fetch(`${this.baseUrl}/ingest`, {
200
+ method: 'POST',
201
+ headers: {
202
+ 'Content-Type': 'application/json',
203
+ 'Authorization': `Bearer ${this.apiKey}`,
204
+ },
205
+ body: JSON.stringify({
206
+ event,
207
+ properties,
208
+ timestamp: timestamp || new Date().toISOString(),
209
+ }),
210
+ });
211
+
212
+ if (!response.ok) {
213
+ const error = await response.json();
214
+ console.error('[FullEvent SDK] Ingestion failed:', error);
215
+ return { success: false, error };
216
+ }
217
+
218
+ return { success: true };
219
+ } catch (error) {
220
+ console.error('[FullEvent SDK] Network error during ingestion:', error);
221
+ return { success: false, error };
222
+ }
223
+ }
224
+
225
+ /**
226
+ * Ingest an HTTP request event with typed properties.
227
+ *
228
+ * @param properties - HTTP request properties with optional custom data
229
+ * @param timestamp - Optional ISO timestamp (defaults to now)
230
+ * @returns Promise resolving to success/error status
231
+ *
232
+ * @remarks
233
+ * This is a convenience method that provides TypeScript autocomplete
234
+ * for standard HTTP properties. Under the hood, it calls `ingest()`
235
+ * with the event type `'http_request'`.
236
+ *
237
+ * @example
238
+ * ```typescript
239
+ * await client.ingestHttpRequest({
240
+ * status_code: 200,
241
+ * method: 'POST',
242
+ * path: '/api/checkout',
243
+ * duration_ms: 234,
244
+ * outcome: 'success',
245
+ * // Custom properties
246
+ * user_id: 'usr_123',
247
+ * });
248
+ * ```
249
+ */
250
+ async ingestHttpRequest(
251
+ properties: HttpRequestProperties,
252
+ timestamp?: string
253
+ ): Promise<{ success: boolean; error?: unknown }> {
254
+ return this.ingest('http_request', properties, timestamp);
255
+ }
256
+
257
+ /**
258
+ * Ping the FullEvent API to verify connection.
259
+ *
260
+ * @returns Promise resolving to connection status with latency
261
+ *
262
+ * @remarks
263
+ * Use this method to verify your SDK is correctly configured.
264
+ * It sends a lightweight ping event and measures round-trip latency.
265
+ * Commonly used during setup or in health check endpoints.
266
+ *
267
+ * @example Basic ping
268
+ * ```typescript
269
+ * const result = await client.ping();
270
+ * if (result.success) {
271
+ * console.log(`Connected! Latency: ${result.latency_ms}ms`);
272
+ * } else {
273
+ * console.error('Connection failed:', result.error);
274
+ * }
275
+ * ```
276
+ *
277
+ * @example Health check endpoint
278
+ * ```typescript
279
+ * app.get('/health', async (c) => {
280
+ * const ping = await fullevent.ping();
281
+ * return c.json({
282
+ * status: ping.success ? 'healthy' : 'degraded',
283
+ * fullevent: ping,
284
+ * });
285
+ * });
286
+ * ```
287
+ */
288
+ async ping(): Promise<{ success: boolean; latency_ms?: number; error?: unknown }> {
289
+ const start = Date.now();
290
+
291
+ try {
292
+ const result = await this.ingest('fullevent.ping', {
293
+ // Standard wide event properties
294
+ status_code: 200,
295
+ outcome: 'success',
296
+ duration_ms: 0, // Will be updated after
297
+
298
+ // SDK info
299
+ sdk: '@fullevent/node',
300
+ sdk_version: '1.0.0',
301
+
302
+ // Runtime info
303
+ runtime: typeof process !== 'undefined' ? 'node' : 'browser',
304
+ node_version: typeof process !== 'undefined' ? process.version : undefined,
305
+ platform: typeof process !== 'undefined' ? process.platform : 'browser',
306
+
307
+ // Ping metadata
308
+ ping_type: 'connection_test',
309
+ message: '🎉 Your first event! FullEvent is connected and ready.',
310
+ });
311
+
312
+ const latency_ms = Date.now() - start;
313
+
314
+ if (result.success) {
315
+ return { success: true, latency_ms };
316
+ } else {
317
+ return { success: false, latency_ms, error: result.error };
318
+ }
319
+ } catch (error) {
320
+ const latency_ms = Date.now() - start;
321
+ return { success: false, latency_ms, error };
322
+ }
323
+ }
324
+ }
325
+