@outlit/node 1.4.3 → 1.5.0

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 CHANGED
@@ -13,7 +13,7 @@ interface StageOptions extends ServerIdentity {
13
13
  }
14
14
  /**
15
15
  * Options for billing status events.
16
- * Requires at least one customer identifier (customerId, stripeCustomerId, or domain).
16
+ * Public billing calls should use customerId.
17
17
  */
18
18
  interface BillingOptions extends CustomerIdentifier {
19
19
  properties?: Record<string, string | number | boolean | null>;
@@ -47,7 +47,7 @@ interface OutlitOptions {
47
47
  /**
48
48
  * Outlit server-side tracking client.
49
49
  *
50
- * Supports tracking with fingerprint (device ID), email, or userId.
50
+ * Supports tracking with fingerprint (device ID), email, userId, or customer attribution.
51
51
  * Use fingerprint for anonymous tracking that can be linked to users later.
52
52
  *
53
53
  * @example
@@ -63,6 +63,14 @@ interface OutlitOptions {
63
63
  * properties: { page: '/pricing' }
64
64
  * })
65
65
  *
66
+ * // Track with customer attribution only
67
+ * outlit.track({
68
+ * customerId: 'cust_123',
69
+ * eventName: 'account_synced'
70
+ * })
71
+ * // `customerId`-only events stay attributable immediately and can later
72
+ * // link to the email-resolved customer when identify() uses the same customerId.
73
+ *
66
74
  * // Track with email (resolves immediately)
67
75
  * outlit.track({
68
76
  * email: 'user@example.com',
@@ -93,11 +101,15 @@ declare class Outlit {
93
101
  /**
94
102
  * Track a custom event.
95
103
  *
96
- * Requires at least one of: `fingerprint`, `email`, or `userId`.
104
+ * Requires at least one of: `fingerprint`, `email`, `userId`, or `customerId`.
97
105
  *
98
106
  * - Use `fingerprint` for anonymous tracking (events linked later via identify)
99
- * - Use `email` for immediate user resolution
100
- * - Use `userId` for app-specific user identification
107
+ * - Use `email` or `userId` for user-scoped attribution
108
+ * - Use `customerId` for customer/account/workspace-scoped attribution
109
+ * - If you later call `identify({ email, customerId })`, Outlit can link earlier
110
+ * `customerId`-only events to the customer resolved from email
111
+ * - `userId` is your system-owned user/contact ID
112
+ * - `customerId` is your system-owned customer/account/workspace ID
101
113
  *
102
114
  * @throws Error if no identity is provided
103
115
  */
@@ -105,8 +117,11 @@ declare class Outlit {
105
117
  /**
106
118
  * Identify or update a user.
107
119
  *
108
- * Requires `email` to establish definitive identity.
109
- * Optionally include `fingerprint` and/or `userId` to link them to this email.
120
+ * Requires `email` or `userId` to establish user-scoped identity.
121
+ * Optionally include `fingerprint` and customer attribution fields to link them.
122
+ * `userId` is your system-owned user/contact ID and `customerId` is your system-owned
123
+ * customer/account/workspace ID. When `email` and `customerId` are sent together,
124
+ * Outlit can link that account/workspace to the customer resolved from email.
110
125
  *
111
126
  * This is how you link anonymous fingerprint-tracked events to a real user:
112
127
  * ```typescript
@@ -117,7 +132,7 @@ declare class Outlit {
117
132
  * });
118
133
  * ```
119
134
  *
120
- * @throws Error if email is not provided
135
+ * @throws Error if neither email nor userId is provided
121
136
  */
122
137
  identify(options: ServerIdentifyOptions): void;
123
138
  /**
package/dist/index.d.ts CHANGED
@@ -13,7 +13,7 @@ interface StageOptions extends ServerIdentity {
13
13
  }
14
14
  /**
15
15
  * Options for billing status events.
16
- * Requires at least one customer identifier (customerId, stripeCustomerId, or domain).
16
+ * Public billing calls should use customerId.
17
17
  */
18
18
  interface BillingOptions extends CustomerIdentifier {
19
19
  properties?: Record<string, string | number | boolean | null>;
@@ -47,7 +47,7 @@ interface OutlitOptions {
47
47
  /**
48
48
  * Outlit server-side tracking client.
49
49
  *
50
- * Supports tracking with fingerprint (device ID), email, or userId.
50
+ * Supports tracking with fingerprint (device ID), email, userId, or customer attribution.
51
51
  * Use fingerprint for anonymous tracking that can be linked to users later.
52
52
  *
53
53
  * @example
@@ -63,6 +63,14 @@ interface OutlitOptions {
63
63
  * properties: { page: '/pricing' }
64
64
  * })
65
65
  *
66
+ * // Track with customer attribution only
67
+ * outlit.track({
68
+ * customerId: 'cust_123',
69
+ * eventName: 'account_synced'
70
+ * })
71
+ * // `customerId`-only events stay attributable immediately and can later
72
+ * // link to the email-resolved customer when identify() uses the same customerId.
73
+ *
66
74
  * // Track with email (resolves immediately)
67
75
  * outlit.track({
68
76
  * email: 'user@example.com',
@@ -93,11 +101,15 @@ declare class Outlit {
93
101
  /**
94
102
  * Track a custom event.
95
103
  *
96
- * Requires at least one of: `fingerprint`, `email`, or `userId`.
104
+ * Requires at least one of: `fingerprint`, `email`, `userId`, or `customerId`.
97
105
  *
98
106
  * - Use `fingerprint` for anonymous tracking (events linked later via identify)
99
- * - Use `email` for immediate user resolution
100
- * - Use `userId` for app-specific user identification
107
+ * - Use `email` or `userId` for user-scoped attribution
108
+ * - Use `customerId` for customer/account/workspace-scoped attribution
109
+ * - If you later call `identify({ email, customerId })`, Outlit can link earlier
110
+ * `customerId`-only events to the customer resolved from email
111
+ * - `userId` is your system-owned user/contact ID
112
+ * - `customerId` is your system-owned customer/account/workspace ID
101
113
  *
102
114
  * @throws Error if no identity is provided
103
115
  */
@@ -105,8 +117,11 @@ declare class Outlit {
105
117
  /**
106
118
  * Identify or update a user.
107
119
  *
108
- * Requires `email` to establish definitive identity.
109
- * Optionally include `fingerprint` and/or `userId` to link them to this email.
120
+ * Requires `email` or `userId` to establish user-scoped identity.
121
+ * Optionally include `fingerprint` and customer attribution fields to link them.
122
+ * `userId` is your system-owned user/contact ID and `customerId` is your system-owned
123
+ * customer/account/workspace ID. When `email` and `customerId` are sent together,
124
+ * Outlit can link that account/workspace to the customer resolved from email.
110
125
  *
111
126
  * This is how you link anonymous fingerprint-tracked events to a real user:
112
127
  * ```typescript
@@ -117,7 +132,7 @@ declare class Outlit {
117
132
  * });
118
133
  * ```
119
134
  *
120
- * @throws Error if email is not provided
135
+ * @throws Error if neither email nor userId is provided
121
136
  */
122
137
  identify(options: ServerIdentifyOptions): void;
123
138
  /**
package/dist/index.js CHANGED
@@ -188,36 +188,41 @@ var Outlit = class {
188
188
  /**
189
189
  * Track a custom event.
190
190
  *
191
- * Requires at least one of: `fingerprint`, `email`, or `userId`.
191
+ * Requires at least one of: `fingerprint`, `email`, `userId`, or `customerId`.
192
192
  *
193
193
  * - Use `fingerprint` for anonymous tracking (events linked later via identify)
194
- * - Use `email` for immediate user resolution
195
- * - Use `userId` for app-specific user identification
194
+ * - Use `email` or `userId` for user-scoped attribution
195
+ * - Use `customerId` for customer/account/workspace-scoped attribution
196
+ * - If you later call `identify({ email, customerId })`, Outlit can link earlier
197
+ * `customerId`-only events to the customer resolved from email
198
+ * - `userId` is your system-owned user/contact ID
199
+ * - `customerId` is your system-owned customer/account/workspace ID
196
200
  *
197
201
  * @throws Error if no identity is provided
198
202
  */
199
203
  track(options) {
200
204
  this.ensureNotShutdown();
201
- (0, import_core.validateServerIdentity)(options.fingerprint, options.email, options.userId);
205
+ (0, import_core.validateServerIdentity)(options.fingerprint, options.email, options.userId, options.customerId);
202
206
  const event = (0, import_core.buildCustomEvent)({
203
- url: `server://${options.email ?? options.userId ?? options.fingerprint}`,
207
+ url: `server://${options.email ?? options.userId ?? options.customerId ?? options.fingerprint}`,
204
208
  timestamp: options.timestamp,
205
209
  eventName: options.eventName,
206
- properties: {
207
- ...options.properties,
208
- // Include identity in properties for server-side resolution
209
- __fingerprint: options.fingerprint ?? null,
210
- __email: options.email ?? null,
211
- __userId: options.userId ?? null
212
- }
210
+ email: options.email,
211
+ userId: options.userId,
212
+ fingerprint: options.fingerprint,
213
+ customerId: options.customerId,
214
+ properties: options.properties
213
215
  });
214
216
  this.queue.enqueue(event);
215
217
  }
216
218
  /**
217
219
  * Identify or update a user.
218
220
  *
219
- * Requires `email` to establish definitive identity.
220
- * Optionally include `fingerprint` and/or `userId` to link them to this email.
221
+ * Requires `email` or `userId` to establish user-scoped identity.
222
+ * Optionally include `fingerprint` and customer attribution fields to link them.
223
+ * `userId` is your system-owned user/contact ID and `customerId` is your system-owned
224
+ * customer/account/workspace ID. When `email` and `customerId` are sent together,
225
+ * Outlit can link that account/workspace to the customer resolved from email.
221
226
  *
222
227
  * This is how you link anonymous fingerprint-tracked events to a real user:
223
228
  * ```typescript
@@ -228,21 +233,23 @@ var Outlit = class {
228
233
  * });
229
234
  * ```
230
235
  *
231
- * @throws Error if email is not provided
236
+ * @throws Error if neither email nor userId is provided
232
237
  */
233
238
  identify(options) {
234
239
  this.ensureNotShutdown();
235
- if (!options.email) {
240
+ if (!options.email && !options.userId) {
236
241
  throw new Error(
237
- "identify() requires email to establish definitive identity. Use fingerprint and/or userId as optional fields to link them to the email."
242
+ "identify() requires email or userId to establish user-scoped identity. Use customerId as the optional account/workspace identifier for linking attribution."
238
243
  );
239
244
  }
240
245
  const event = (0, import_core.buildIdentifyEvent)({
241
- url: `server://${options.email}`,
246
+ url: `server://${options.email ?? options.userId}`,
242
247
  email: options.email,
243
248
  userId: options.userId,
244
249
  fingerprint: options.fingerprint,
245
- traits: options.traits
250
+ traits: options.traits,
251
+ customerId: options.customerId,
252
+ customerTraits: options.customerTraits
246
253
  });
247
254
  this.queue.enqueue(event);
248
255
  }
@@ -274,7 +281,6 @@ var Outlit = class {
274
281
  stage,
275
282
  properties: {
276
283
  ...options.properties,
277
- // Include identity in properties for server-side resolution
278
284
  __fingerprint: options.fingerprint ?? null,
279
285
  __email: options.email ?? null,
280
286
  __userId: options.userId ?? null
@@ -284,12 +290,12 @@ var Outlit = class {
284
290
  }
285
291
  sendBillingEvent(status, options) {
286
292
  this.ensureNotShutdown();
293
+ (0, import_core.validateCustomerIdentity)(options.customerId, options.stripeCustomerId);
287
294
  const event = (0, import_core.buildBillingEvent)({
288
- url: `server://${options.domain}`,
295
+ url: `server://${options.customerId ?? options.stripeCustomerId}`,
289
296
  status,
290
297
  customerId: options.customerId,
291
298
  stripeCustomerId: options.stripeCustomerId,
292
- domain: options.domain,
293
299
  properties: options.properties
294
300
  });
295
301
  this.queue.enqueue(event);
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/client.ts","../src/queue.ts","../src/transport.ts"],"sourcesContent":["// Main export\nexport { Outlit } from \"./client\"\nexport type { OutlitOptions, StageOptions, BillingOptions } from \"./client\"\n\n// Re-export useful types from core\nexport type {\n ServerTrackOptions,\n ServerIdentifyOptions,\n TrackerConfig,\n IngestResponse,\n ExplicitJourneyStage,\n} from \"@outlit/core\"\n","import {\n type BillingStatus,\n type CustomerIdentifier,\n DEFAULT_API_HOST,\n type ExplicitJourneyStage,\n type IngestPayload,\n type ServerIdentifyOptions,\n type ServerIdentity,\n type ServerTrackOptions,\n type TrackerEvent,\n buildBillingEvent,\n buildCustomEvent,\n buildIdentifyEvent,\n buildStageEvent,\n validateServerIdentity,\n} from \"@outlit/core\"\nimport { EventQueue } from \"./queue\"\nimport { HttpTransport, TransportError } from \"./transport\"\n\n// ============================================\n// STAGE OPTIONS\n// ============================================\n\n/**\n * Options for stage transition events (activate, engaged, inactive).\n * Server-side stage events require at least one identifier (fingerprint, email, or userId).\n */\nexport interface StageOptions extends ServerIdentity {\n /**\n * Optional properties for context.\n */\n properties?: Record<string, string | number | boolean | null>\n}\n\n/**\n * Options for billing status events.\n * Requires at least one customer identifier (customerId, stripeCustomerId, or domain).\n */\nexport interface BillingOptions extends CustomerIdentifier {\n properties?: Record<string, string | number | boolean | null>\n}\n\n// ============================================\n// OUTLIT CLIENT\n// ============================================\n\nexport interface OutlitOptions {\n /**\n * Your Outlit public key.\n */\n publicKey: string\n\n /**\n * API host URL.\n * @default \"https://app.outlit.ai\"\n */\n apiHost?: string\n\n /**\n * How often to flush events (in milliseconds).\n * @default 10000 (10 seconds)\n */\n flushInterval?: number\n\n /**\n * Maximum number of events to batch before flushing.\n * @default 100\n */\n maxBatchSize?: number\n\n /**\n * Request timeout in milliseconds.\n * @default 10000 (10 seconds)\n */\n timeout?: number\n}\n\n/**\n * Outlit server-side tracking client.\n *\n * Supports tracking with fingerprint (device ID), email, or userId.\n * Use fingerprint for anonymous tracking that can be linked to users later.\n *\n * @example\n * ```typescript\n * import { Outlit } from '@outlit/node'\n *\n * const outlit = new Outlit({ publicKey: 'pk_xxx' })\n *\n * // Track with fingerprint only (anonymous, stored for later backfill)\n * outlit.track({\n * fingerprint: deviceId,\n * eventName: 'page_view',\n * properties: { page: '/pricing' }\n * })\n *\n * // Track with email (resolves immediately)\n * outlit.track({\n * email: 'user@example.com',\n * eventName: 'subscription_created',\n * properties: { plan: 'pro' }\n * })\n *\n * // Identify user and link fingerprint to email\n * outlit.identify({\n * email: 'user@example.com',\n * fingerprint: deviceId, // Links this device to the user\n * userId: 'usr_123',\n * traits: { name: 'John Doe' }\n * })\n *\n * // Flush before shutdown (important for serverless)\n * await outlit.flush()\n * ```\n */\nexport class Outlit {\n private transport: HttpTransport\n private queue: EventQueue\n private flushTimer: ReturnType<typeof setInterval> | null = null\n private flushInterval: number\n private isShutdown = false\n private fatalTransportError: TransportError | null = null\n\n constructor(options: OutlitOptions) {\n const apiHost = options.apiHost ?? DEFAULT_API_HOST\n this.flushInterval = options.flushInterval ?? 10000\n\n this.transport = new HttpTransport({\n apiHost,\n publicKey: options.publicKey,\n timeout: options.timeout,\n })\n\n this.queue = new EventQueue({\n maxSize: options.maxBatchSize ?? 100,\n onFlush: async (events) => {\n await this.sendEvents(events)\n },\n })\n\n // Start flush timer\n this.startFlushTimer()\n }\n\n /**\n * Track a custom event.\n *\n * Requires at least one of: `fingerprint`, `email`, or `userId`.\n *\n * - Use `fingerprint` for anonymous tracking (events linked later via identify)\n * - Use `email` for immediate user resolution\n * - Use `userId` for app-specific user identification\n *\n * @throws Error if no identity is provided\n */\n track(options: ServerTrackOptions): void {\n this.ensureNotShutdown()\n validateServerIdentity(options.fingerprint, options.email, options.userId)\n\n const event = buildCustomEvent({\n url: `server://${options.email ?? options.userId ?? options.fingerprint}`,\n timestamp: options.timestamp,\n eventName: options.eventName,\n properties: {\n ...options.properties,\n // Include identity in properties for server-side resolution\n __fingerprint: options.fingerprint ?? null,\n __email: options.email ?? null,\n __userId: options.userId ?? null,\n },\n })\n\n this.queue.enqueue(event)\n }\n\n /**\n * Identify or update a user.\n *\n * Requires `email` to establish definitive identity.\n * Optionally include `fingerprint` and/or `userId` to link them to this email.\n *\n * This is how you link anonymous fingerprint-tracked events to a real user:\n * ```typescript\n * outlit.identify({\n * email: 'user@example.com',\n * fingerprint: deviceId, // Links this device to the user\n * userId: 'usr_123', // Links this app user ID to the user\n * });\n * ```\n *\n * @throws Error if email is not provided\n */\n identify(options: ServerIdentifyOptions): void {\n this.ensureNotShutdown()\n\n // Identify requires email (definitive identity)\n if (!options.email) {\n throw new Error(\n \"identify() requires email to establish definitive identity. \" +\n \"Use fingerprint and/or userId as optional fields to link them to the email.\",\n )\n }\n\n const event = buildIdentifyEvent({\n url: `server://${options.email}`,\n email: options.email,\n userId: options.userId,\n fingerprint: options.fingerprint,\n traits: options.traits,\n })\n\n this.queue.enqueue(event)\n }\n\n /**\n * User namespace methods for contact journey stages.\n */\n readonly user = {\n identify: (options: ServerIdentifyOptions) => this.identify(options),\n activate: (options: StageOptions) => this.sendStageEvent(\"activated\", options),\n engaged: (options: StageOptions) => this.sendStageEvent(\"engaged\", options),\n inactive: (options: StageOptions) => this.sendStageEvent(\"inactive\", options),\n }\n\n /**\n * Customer namespace methods for billing status.\n */\n readonly customer = {\n trialing: (options: BillingOptions) => this.sendBillingEvent(\"trialing\", options),\n paid: (options: BillingOptions) => this.sendBillingEvent(\"paid\", options),\n churned: (options: BillingOptions) => this.sendBillingEvent(\"churned\", options),\n }\n\n /**\n * Internal method to send a stage event.\n */\n private sendStageEvent(stage: ExplicitJourneyStage, options: StageOptions): void {\n this.ensureNotShutdown()\n validateServerIdentity(options.fingerprint, options.email, options.userId)\n\n const event = buildStageEvent({\n url: `server://${options.email ?? options.userId ?? options.fingerprint}`,\n stage,\n properties: {\n ...options.properties,\n // Include identity in properties for server-side resolution\n __fingerprint: options.fingerprint ?? null,\n __email: options.email ?? null,\n __userId: options.userId ?? null,\n },\n })\n\n this.queue.enqueue(event)\n }\n\n private sendBillingEvent(status: BillingStatus, options: BillingOptions): void {\n this.ensureNotShutdown()\n\n const event = buildBillingEvent({\n url: `server://${options.domain}`,\n status,\n customerId: options.customerId,\n stripeCustomerId: options.stripeCustomerId,\n domain: options.domain,\n properties: options.properties,\n })\n\n this.queue.enqueue(event)\n }\n\n /**\n * Flush all pending events immediately.\n *\n * Important: Call this before your serverless function exits!\n */\n async flush(): Promise<void> {\n await this.queue.flush()\n }\n\n /**\n * Shutdown the client gracefully.\n *\n * Flushes remaining events and stops the flush timer.\n */\n async shutdown(): Promise<void> {\n if (this.isShutdown) return\n\n this.isShutdown = true\n\n if (this.flushTimer) {\n clearInterval(this.flushTimer)\n this.flushTimer = null\n }\n\n await this.flush()\n }\n\n /**\n * Get the number of events waiting to be sent.\n */\n get queueSize(): number {\n return this.queue.size\n }\n\n // ============================================\n // INTERNAL METHODS\n // ============================================\n\n private startFlushTimer(): void {\n if (this.flushTimer) return\n\n this.flushTimer = setInterval(() => {\n this.flush().catch((error) => {\n if (this.isNonRetryableTransportError(error)) {\n // Already handled and logged in sendEvents().\n return\n }\n console.error(\"[Outlit] Flush error:\", error)\n })\n }, this.flushInterval)\n\n // Don't block process exit\n if (this.flushTimer.unref) {\n this.flushTimer.unref()\n }\n }\n\n private async sendEvents(events: TrackerEvent[]): Promise<void> {\n if (events.length === 0) return\n if (this.fatalTransportError) return\n\n // For server events, we don't use visitorId - the API resolves identity\n // directly from the event data (email/userId)\n const payload: IngestPayload = {\n source: \"server\",\n events,\n // visitorId is intentionally omitted for server events\n }\n\n try {\n await this.transport.send(payload)\n } catch (error) {\n if (this.isNonRetryableTransportError(error)) {\n this.handleFatalTransportError(error)\n }\n throw error\n }\n }\n\n private ensureNotShutdown(): void {\n if (this.isShutdown) {\n throw new Error(\n \"[Outlit] Client has been shutdown. Create a new instance to continue tracking.\",\n )\n }\n }\n\n private isNonRetryableTransportError(error: unknown): error is TransportError {\n return error instanceof TransportError && error.retryable === false\n }\n\n private handleFatalTransportError(error: TransportError): void {\n if (this.fatalTransportError) return\n\n this.fatalTransportError = error\n\n if (this.flushTimer) {\n clearInterval(this.flushTimer)\n this.flushTimer = null\n }\n\n console.error(\n \"[Outlit] Non-retryable ingest error. Automatic flush retries disabled until client restart:\",\n error,\n )\n }\n}\n","import type { TrackerEvent } from \"@outlit/core\"\n\n// ============================================\n// EVENT QUEUE\n// ============================================\n\nfunction isNonRetryableError(error: unknown): boolean {\n if (!error || typeof error !== \"object\") return false\n if (!(\"retryable\" in error)) return false\n\n return (error as { retryable?: unknown }).retryable === false\n}\n\nexport interface QueueOptions {\n maxSize?: number\n onFlush: (events: TrackerEvent[]) => Promise<void>\n}\n\nexport class EventQueue {\n private queue: TrackerEvent[] = []\n private maxSize: number\n private onFlush: (events: TrackerEvent[]) => Promise<void>\n private isFlushing = false\n\n constructor(options: QueueOptions) {\n this.maxSize = options.maxSize ?? 100\n this.onFlush = options.onFlush\n }\n\n /**\n * Add an event to the queue.\n * Triggers flush if queue reaches max size.\n */\n async enqueue(event: TrackerEvent): Promise<void> {\n this.queue.push(event)\n\n if (this.queue.length >= this.maxSize) {\n await this.flush()\n }\n }\n\n /**\n * Flush all events in the queue.\n */\n async flush(): Promise<void> {\n if (this.isFlushing || this.queue.length === 0) return\n\n this.isFlushing = true\n const events = [...this.queue]\n this.queue = []\n\n try {\n await this.onFlush(events)\n } catch (error) {\n // Re-add events for retryable failures only.\n // Non-retryable failures (e.g. invalid config 4xx) are dropped to avoid infinite retry loops.\n if (!isNonRetryableError(error)) {\n this.queue = [...events, ...this.queue]\n }\n throw error\n } finally {\n this.isFlushing = false\n }\n }\n\n /**\n * Get the number of events in the queue.\n */\n get size(): number {\n return this.queue.length\n }\n\n /**\n * Check if the queue is currently flushing.\n */\n get flushing(): boolean {\n return this.isFlushing\n }\n}\n","import type { IngestPayload, IngestResponse } from \"@outlit/core\"\n\n// ============================================\n// HTTP TRANSPORT\n// ============================================\n\ninterface TransportErrorOptions {\n status?: number\n retryable?: boolean\n cause?: unknown\n}\n\n/**\n * Transport-level error enriched with retryability metadata.\n */\nexport class TransportError extends Error {\n readonly status?: number\n readonly retryable: boolean\n\n constructor(message: string, options?: TransportErrorOptions) {\n super(message)\n this.name = \"TransportError\"\n this.status = options?.status\n this.retryable = options?.retryable ?? true\n\n if (options?.cause !== undefined) {\n this.cause = options.cause\n }\n }\n}\n\nfunction isRetryableStatus(status: number): boolean {\n // 429 is rate limiting (transient). Most other 4xx are configuration/input errors.\n return status === 429 || status >= 500\n}\n\nexport interface TransportOptions {\n apiHost: string\n publicKey: string\n timeout?: number\n}\n\nexport class HttpTransport {\n private apiHost: string\n private publicKey: string\n private timeout: number\n\n constructor(options: TransportOptions) {\n this.apiHost = options.apiHost\n this.publicKey = options.publicKey\n this.timeout = options.timeout ?? 10000\n }\n\n /**\n * Send events to the ingest API.\n */\n async send(payload: IngestPayload): Promise<IngestResponse> {\n const url = `${this.apiHost}/api/i/v1/${this.publicKey}/events`\n\n const controller = new AbortController()\n const timeoutId = setTimeout(() => controller.abort(), this.timeout)\n\n try {\n const response = await fetch(url, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n },\n body: JSON.stringify(payload),\n signal: controller.signal,\n })\n\n if (!response.ok) {\n const errorBody = await response.text().catch(() => \"Unknown error\")\n throw new TransportError(`HTTP ${response.status}: ${errorBody}`, {\n status: response.status,\n retryable: isRetryableStatus(response.status),\n })\n }\n\n return (await response.json()) as IngestResponse\n } catch (error) {\n if (error instanceof TransportError) {\n throw error\n }\n\n if (error instanceof Error && error.name === \"AbortError\") {\n throw new TransportError(`Request timed out after ${this.timeout}ms`, {\n retryable: true,\n cause: error,\n })\n }\n\n if (error instanceof Error) {\n throw new TransportError(error.message, {\n retryable: true,\n cause: error,\n })\n }\n\n throw new TransportError(\"Unknown transport error\", {\n retryable: true,\n cause: error,\n })\n } finally {\n clearTimeout(timeoutId)\n }\n }\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/index.ts","../src/client.ts","../src/queue.ts","../src/transport.ts"],"sourcesContent":["// Main export\n\n// Re-export useful types from core\nexport type {\n ExplicitJourneyStage,\n IngestResponse,\n ServerIdentifyOptions,\n ServerTrackOptions,\n TrackerConfig,\n} from \"@outlit/core\"\nexport type { BillingOptions, OutlitOptions, StageOptions } from \"./client\"\nexport { Outlit } from \"./client\"\n","import {\n type BillingStatus,\n buildBillingEvent,\n buildCustomEvent,\n buildIdentifyEvent,\n buildStageEvent,\n type CustomerIdentifier,\n DEFAULT_API_HOST,\n type ExplicitJourneyStage,\n type IngestPayload,\n type ServerIdentifyOptions,\n type ServerIdentity,\n type ServerTrackOptions,\n type TrackerEvent,\n validateCustomerIdentity,\n validateServerIdentity,\n} from \"@outlit/core\"\nimport { EventQueue } from \"./queue\"\nimport { HttpTransport, TransportError } from \"./transport\"\n\n// ============================================\n// STAGE OPTIONS\n// ============================================\n\n/**\n * Options for stage transition events (activate, engaged, inactive).\n * Server-side stage events require at least one identifier (fingerprint, email, or userId).\n */\nexport interface StageOptions extends ServerIdentity {\n /**\n * Optional properties for context.\n */\n properties?: Record<string, string | number | boolean | null>\n}\n\n/**\n * Options for billing status events.\n * Public billing calls should use customerId.\n */\nexport interface BillingOptions extends CustomerIdentifier {\n properties?: Record<string, string | number | boolean | null>\n}\n\n// ============================================\n// OUTLIT CLIENT\n// ============================================\n\nexport interface OutlitOptions {\n /**\n * Your Outlit public key.\n */\n publicKey: string\n\n /**\n * API host URL.\n * @default \"https://app.outlit.ai\"\n */\n apiHost?: string\n\n /**\n * How often to flush events (in milliseconds).\n * @default 10000 (10 seconds)\n */\n flushInterval?: number\n\n /**\n * Maximum number of events to batch before flushing.\n * @default 100\n */\n maxBatchSize?: number\n\n /**\n * Request timeout in milliseconds.\n * @default 10000 (10 seconds)\n */\n timeout?: number\n}\n\n/**\n * Outlit server-side tracking client.\n *\n * Supports tracking with fingerprint (device ID), email, userId, or customer attribution.\n * Use fingerprint for anonymous tracking that can be linked to users later.\n *\n * @example\n * ```typescript\n * import { Outlit } from '@outlit/node'\n *\n * const outlit = new Outlit({ publicKey: 'pk_xxx' })\n *\n * // Track with fingerprint only (anonymous, stored for later backfill)\n * outlit.track({\n * fingerprint: deviceId,\n * eventName: 'page_view',\n * properties: { page: '/pricing' }\n * })\n *\n * // Track with customer attribution only\n * outlit.track({\n * customerId: 'cust_123',\n * eventName: 'account_synced'\n * })\n * // `customerId`-only events stay attributable immediately and can later\n * // link to the email-resolved customer when identify() uses the same customerId.\n *\n * // Track with email (resolves immediately)\n * outlit.track({\n * email: 'user@example.com',\n * eventName: 'subscription_created',\n * properties: { plan: 'pro' }\n * })\n *\n * // Identify user and link fingerprint to email\n * outlit.identify({\n * email: 'user@example.com',\n * fingerprint: deviceId, // Links this device to the user\n * userId: 'usr_123',\n * traits: { name: 'John Doe' }\n * })\n *\n * // Flush before shutdown (important for serverless)\n * await outlit.flush()\n * ```\n */\nexport class Outlit {\n private transport: HttpTransport\n private queue: EventQueue\n private flushTimer: ReturnType<typeof setInterval> | null = null\n private flushInterval: number\n private isShutdown = false\n private fatalTransportError: TransportError | null = null\n\n constructor(options: OutlitOptions) {\n const apiHost = options.apiHost ?? DEFAULT_API_HOST\n this.flushInterval = options.flushInterval ?? 10000\n\n this.transport = new HttpTransport({\n apiHost,\n publicKey: options.publicKey,\n timeout: options.timeout,\n })\n\n this.queue = new EventQueue({\n maxSize: options.maxBatchSize ?? 100,\n onFlush: async (events) => {\n await this.sendEvents(events)\n },\n })\n\n // Start flush timer\n this.startFlushTimer()\n }\n\n /**\n * Track a custom event.\n *\n * Requires at least one of: `fingerprint`, `email`, `userId`, or `customerId`.\n *\n * - Use `fingerprint` for anonymous tracking (events linked later via identify)\n * - Use `email` or `userId` for user-scoped attribution\n * - Use `customerId` for customer/account/workspace-scoped attribution\n * - If you later call `identify({ email, customerId })`, Outlit can link earlier\n * `customerId`-only events to the customer resolved from email\n * - `userId` is your system-owned user/contact ID\n * - `customerId` is your system-owned customer/account/workspace ID\n *\n * @throws Error if no identity is provided\n */\n track(options: ServerTrackOptions): void {\n this.ensureNotShutdown()\n validateServerIdentity(options.fingerprint, options.email, options.userId, options.customerId)\n\n const event = buildCustomEvent({\n url: `server://${\n options.email ?? options.userId ?? options.customerId ?? options.fingerprint\n }`,\n timestamp: options.timestamp,\n eventName: options.eventName,\n email: options.email,\n userId: options.userId,\n fingerprint: options.fingerprint,\n customerId: options.customerId,\n properties: options.properties,\n })\n\n this.queue.enqueue(event)\n }\n\n /**\n * Identify or update a user.\n *\n * Requires `email` or `userId` to establish user-scoped identity.\n * Optionally include `fingerprint` and customer attribution fields to link them.\n * `userId` is your system-owned user/contact ID and `customerId` is your system-owned\n * customer/account/workspace ID. When `email` and `customerId` are sent together,\n * Outlit can link that account/workspace to the customer resolved from email.\n *\n * This is how you link anonymous fingerprint-tracked events to a real user:\n * ```typescript\n * outlit.identify({\n * email: 'user@example.com',\n * fingerprint: deviceId, // Links this device to the user\n * userId: 'usr_123', // Links this app user ID to the user\n * });\n * ```\n *\n * @throws Error if neither email nor userId is provided\n */\n identify(options: ServerIdentifyOptions): void {\n this.ensureNotShutdown()\n\n // Identify requires user-scoped identity.\n if (!options.email && !options.userId) {\n throw new Error(\n \"identify() requires email or userId to establish user-scoped identity. \" +\n \"Use customerId as the optional account/workspace identifier for linking attribution.\",\n )\n }\n\n const event = buildIdentifyEvent({\n url: `server://${options.email ?? options.userId}`,\n email: options.email,\n userId: options.userId,\n fingerprint: options.fingerprint,\n traits: options.traits,\n customerId: options.customerId,\n customerTraits: options.customerTraits,\n })\n\n this.queue.enqueue(event)\n }\n\n /**\n * User namespace methods for contact journey stages.\n */\n readonly user = {\n identify: (options: ServerIdentifyOptions) => this.identify(options),\n activate: (options: StageOptions) => this.sendStageEvent(\"activated\", options),\n engaged: (options: StageOptions) => this.sendStageEvent(\"engaged\", options),\n inactive: (options: StageOptions) => this.sendStageEvent(\"inactive\", options),\n }\n\n /**\n * Customer namespace methods for billing status.\n */\n readonly customer = {\n trialing: (options: BillingOptions) => this.sendBillingEvent(\"trialing\", options),\n paid: (options: BillingOptions) => this.sendBillingEvent(\"paid\", options),\n churned: (options: BillingOptions) => this.sendBillingEvent(\"churned\", options),\n }\n\n /**\n * Internal method to send a stage event.\n */\n private sendStageEvent(stage: ExplicitJourneyStage, options: StageOptions): void {\n this.ensureNotShutdown()\n validateServerIdentity(options.fingerprint, options.email, options.userId)\n\n const event = buildStageEvent({\n url: `server://${options.email ?? options.userId ?? options.fingerprint}`,\n stage,\n properties: {\n ...options.properties,\n __fingerprint: options.fingerprint ?? null,\n __email: options.email ?? null,\n __userId: options.userId ?? null,\n },\n })\n\n this.queue.enqueue(event)\n }\n\n private sendBillingEvent(status: BillingStatus, options: BillingOptions): void {\n this.ensureNotShutdown()\n validateCustomerIdentity(options.customerId, options.stripeCustomerId)\n\n const event = buildBillingEvent({\n url: `server://${options.customerId ?? options.stripeCustomerId}`,\n status,\n customerId: options.customerId,\n stripeCustomerId: options.stripeCustomerId,\n properties: options.properties,\n })\n\n this.queue.enqueue(event)\n }\n\n /**\n * Flush all pending events immediately.\n *\n * Important: Call this before your serverless function exits!\n */\n async flush(): Promise<void> {\n await this.queue.flush()\n }\n\n /**\n * Shutdown the client gracefully.\n *\n * Flushes remaining events and stops the flush timer.\n */\n async shutdown(): Promise<void> {\n if (this.isShutdown) return\n\n this.isShutdown = true\n\n if (this.flushTimer) {\n clearInterval(this.flushTimer)\n this.flushTimer = null\n }\n\n await this.flush()\n }\n\n /**\n * Get the number of events waiting to be sent.\n */\n get queueSize(): number {\n return this.queue.size\n }\n\n // ============================================\n // INTERNAL METHODS\n // ============================================\n\n private startFlushTimer(): void {\n if (this.flushTimer) return\n\n this.flushTimer = setInterval(() => {\n this.flush().catch((error) => {\n if (this.isNonRetryableTransportError(error)) {\n // Already handled and logged in sendEvents().\n return\n }\n console.error(\"[Outlit] Flush error:\", error)\n })\n }, this.flushInterval)\n\n // Don't block process exit\n if (this.flushTimer.unref) {\n this.flushTimer.unref()\n }\n }\n\n private async sendEvents(events: TrackerEvent[]): Promise<void> {\n if (events.length === 0) return\n if (this.fatalTransportError) return\n\n // For server events, we don't use visitorId - the API resolves identity\n // directly from the event data (email/userId)\n const payload: IngestPayload = {\n source: \"server\",\n events,\n // visitorId is intentionally omitted for server events\n }\n\n try {\n await this.transport.send(payload)\n } catch (error) {\n if (this.isNonRetryableTransportError(error)) {\n this.handleFatalTransportError(error)\n }\n throw error\n }\n }\n\n private ensureNotShutdown(): void {\n if (this.isShutdown) {\n throw new Error(\n \"[Outlit] Client has been shutdown. Create a new instance to continue tracking.\",\n )\n }\n }\n\n private isNonRetryableTransportError(error: unknown): error is TransportError {\n return error instanceof TransportError && error.retryable === false\n }\n\n private handleFatalTransportError(error: TransportError): void {\n if (this.fatalTransportError) return\n\n this.fatalTransportError = error\n\n if (this.flushTimer) {\n clearInterval(this.flushTimer)\n this.flushTimer = null\n }\n\n console.error(\n \"[Outlit] Non-retryable ingest error. Automatic flush retries disabled until client restart:\",\n error,\n )\n }\n}\n","import type { TrackerEvent } from \"@outlit/core\"\n\n// ============================================\n// EVENT QUEUE\n// ============================================\n\nfunction isNonRetryableError(error: unknown): boolean {\n if (!error || typeof error !== \"object\") return false\n if (!(\"retryable\" in error)) return false\n\n return (error as { retryable?: unknown }).retryable === false\n}\n\nexport interface QueueOptions {\n maxSize?: number\n onFlush: (events: TrackerEvent[]) => Promise<void>\n}\n\nexport class EventQueue {\n private queue: TrackerEvent[] = []\n private maxSize: number\n private onFlush: (events: TrackerEvent[]) => Promise<void>\n private isFlushing = false\n\n constructor(options: QueueOptions) {\n this.maxSize = options.maxSize ?? 100\n this.onFlush = options.onFlush\n }\n\n /**\n * Add an event to the queue.\n * Triggers flush if queue reaches max size.\n */\n async enqueue(event: TrackerEvent): Promise<void> {\n this.queue.push(event)\n\n if (this.queue.length >= this.maxSize) {\n await this.flush()\n }\n }\n\n /**\n * Flush all events in the queue.\n */\n async flush(): Promise<void> {\n if (this.isFlushing || this.queue.length === 0) return\n\n this.isFlushing = true\n const events = [...this.queue]\n this.queue = []\n\n try {\n await this.onFlush(events)\n } catch (error) {\n // Re-add events for retryable failures only.\n // Non-retryable failures (e.g. invalid config 4xx) are dropped to avoid infinite retry loops.\n if (!isNonRetryableError(error)) {\n this.queue = [...events, ...this.queue]\n }\n throw error\n } finally {\n this.isFlushing = false\n }\n }\n\n /**\n * Get the number of events in the queue.\n */\n get size(): number {\n return this.queue.length\n }\n\n /**\n * Check if the queue is currently flushing.\n */\n get flushing(): boolean {\n return this.isFlushing\n }\n}\n","import type { IngestPayload, IngestResponse } from \"@outlit/core\"\n\n// ============================================\n// HTTP TRANSPORT\n// ============================================\n\ninterface TransportErrorOptions {\n status?: number\n retryable?: boolean\n cause?: unknown\n}\n\n/**\n * Transport-level error enriched with retryability metadata.\n */\nexport class TransportError extends Error {\n readonly status?: number\n readonly retryable: boolean\n\n constructor(message: string, options?: TransportErrorOptions) {\n super(message)\n this.name = \"TransportError\"\n this.status = options?.status\n this.retryable = options?.retryable ?? true\n\n if (options?.cause !== undefined) {\n this.cause = options.cause\n }\n }\n}\n\nfunction isRetryableStatus(status: number): boolean {\n // 429 is rate limiting (transient). Most other 4xx are configuration/input errors.\n return status === 429 || status >= 500\n}\n\nexport interface TransportOptions {\n apiHost: string\n publicKey: string\n timeout?: number\n}\n\nexport class HttpTransport {\n private apiHost: string\n private publicKey: string\n private timeout: number\n\n constructor(options: TransportOptions) {\n this.apiHost = options.apiHost\n this.publicKey = options.publicKey\n this.timeout = options.timeout ?? 10000\n }\n\n /**\n * Send events to the ingest API.\n */\n async send(payload: IngestPayload): Promise<IngestResponse> {\n const url = `${this.apiHost}/api/i/v1/${this.publicKey}/events`\n\n const controller = new AbortController()\n const timeoutId = setTimeout(() => controller.abort(), this.timeout)\n\n try {\n const response = await fetch(url, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n },\n body: JSON.stringify(payload),\n signal: controller.signal,\n })\n\n if (!response.ok) {\n const errorBody = await response.text().catch(() => \"Unknown error\")\n throw new TransportError(`HTTP ${response.status}: ${errorBody}`, {\n status: response.status,\n retryable: isRetryableStatus(response.status),\n })\n }\n\n return (await response.json()) as IngestResponse\n } catch (error) {\n if (error instanceof TransportError) {\n throw error\n }\n\n if (error instanceof Error && error.name === \"AbortError\") {\n throw new TransportError(`Request timed out after ${this.timeout}ms`, {\n retryable: true,\n cause: error,\n })\n }\n\n if (error instanceof Error) {\n throw new TransportError(error.message, {\n retryable: true,\n cause: error,\n })\n }\n\n throw new TransportError(\"Unknown transport error\", {\n retryable: true,\n cause: error,\n })\n } finally {\n clearTimeout(timeoutId)\n }\n }\n}\n"],"mappings":"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","names":[]}
package/dist/index.mjs CHANGED
@@ -1,10 +1,11 @@
1
1
  // src/client.ts
2
2
  import {
3
- DEFAULT_API_HOST,
4
3
  buildBillingEvent,
5
4
  buildCustomEvent,
6
5
  buildIdentifyEvent,
7
6
  buildStageEvent,
7
+ DEFAULT_API_HOST,
8
+ validateCustomerIdentity,
8
9
  validateServerIdentity
9
10
  } from "@outlit/core";
10
11
 
@@ -169,36 +170,41 @@ var Outlit = class {
169
170
  /**
170
171
  * Track a custom event.
171
172
  *
172
- * Requires at least one of: `fingerprint`, `email`, or `userId`.
173
+ * Requires at least one of: `fingerprint`, `email`, `userId`, or `customerId`.
173
174
  *
174
175
  * - Use `fingerprint` for anonymous tracking (events linked later via identify)
175
- * - Use `email` for immediate user resolution
176
- * - Use `userId` for app-specific user identification
176
+ * - Use `email` or `userId` for user-scoped attribution
177
+ * - Use `customerId` for customer/account/workspace-scoped attribution
178
+ * - If you later call `identify({ email, customerId })`, Outlit can link earlier
179
+ * `customerId`-only events to the customer resolved from email
180
+ * - `userId` is your system-owned user/contact ID
181
+ * - `customerId` is your system-owned customer/account/workspace ID
177
182
  *
178
183
  * @throws Error if no identity is provided
179
184
  */
180
185
  track(options) {
181
186
  this.ensureNotShutdown();
182
- validateServerIdentity(options.fingerprint, options.email, options.userId);
187
+ validateServerIdentity(options.fingerprint, options.email, options.userId, options.customerId);
183
188
  const event = buildCustomEvent({
184
- url: `server://${options.email ?? options.userId ?? options.fingerprint}`,
189
+ url: `server://${options.email ?? options.userId ?? options.customerId ?? options.fingerprint}`,
185
190
  timestamp: options.timestamp,
186
191
  eventName: options.eventName,
187
- properties: {
188
- ...options.properties,
189
- // Include identity in properties for server-side resolution
190
- __fingerprint: options.fingerprint ?? null,
191
- __email: options.email ?? null,
192
- __userId: options.userId ?? null
193
- }
192
+ email: options.email,
193
+ userId: options.userId,
194
+ fingerprint: options.fingerprint,
195
+ customerId: options.customerId,
196
+ properties: options.properties
194
197
  });
195
198
  this.queue.enqueue(event);
196
199
  }
197
200
  /**
198
201
  * Identify or update a user.
199
202
  *
200
- * Requires `email` to establish definitive identity.
201
- * Optionally include `fingerprint` and/or `userId` to link them to this email.
203
+ * Requires `email` or `userId` to establish user-scoped identity.
204
+ * Optionally include `fingerprint` and customer attribution fields to link them.
205
+ * `userId` is your system-owned user/contact ID and `customerId` is your system-owned
206
+ * customer/account/workspace ID. When `email` and `customerId` are sent together,
207
+ * Outlit can link that account/workspace to the customer resolved from email.
202
208
  *
203
209
  * This is how you link anonymous fingerprint-tracked events to a real user:
204
210
  * ```typescript
@@ -209,21 +215,23 @@ var Outlit = class {
209
215
  * });
210
216
  * ```
211
217
  *
212
- * @throws Error if email is not provided
218
+ * @throws Error if neither email nor userId is provided
213
219
  */
214
220
  identify(options) {
215
221
  this.ensureNotShutdown();
216
- if (!options.email) {
222
+ if (!options.email && !options.userId) {
217
223
  throw new Error(
218
- "identify() requires email to establish definitive identity. Use fingerprint and/or userId as optional fields to link them to the email."
224
+ "identify() requires email or userId to establish user-scoped identity. Use customerId as the optional account/workspace identifier for linking attribution."
219
225
  );
220
226
  }
221
227
  const event = buildIdentifyEvent({
222
- url: `server://${options.email}`,
228
+ url: `server://${options.email ?? options.userId}`,
223
229
  email: options.email,
224
230
  userId: options.userId,
225
231
  fingerprint: options.fingerprint,
226
- traits: options.traits
232
+ traits: options.traits,
233
+ customerId: options.customerId,
234
+ customerTraits: options.customerTraits
227
235
  });
228
236
  this.queue.enqueue(event);
229
237
  }
@@ -255,7 +263,6 @@ var Outlit = class {
255
263
  stage,
256
264
  properties: {
257
265
  ...options.properties,
258
- // Include identity in properties for server-side resolution
259
266
  __fingerprint: options.fingerprint ?? null,
260
267
  __email: options.email ?? null,
261
268
  __userId: options.userId ?? null
@@ -265,12 +272,12 @@ var Outlit = class {
265
272
  }
266
273
  sendBillingEvent(status, options) {
267
274
  this.ensureNotShutdown();
275
+ validateCustomerIdentity(options.customerId, options.stripeCustomerId);
268
276
  const event = buildBillingEvent({
269
- url: `server://${options.domain}`,
277
+ url: `server://${options.customerId ?? options.stripeCustomerId}`,
270
278
  status,
271
279
  customerId: options.customerId,
272
280
  stripeCustomerId: options.stripeCustomerId,
273
- domain: options.domain,
274
281
  properties: options.properties
275
282
  });
276
283
  this.queue.enqueue(event);
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/client.ts","../src/queue.ts","../src/transport.ts"],"sourcesContent":["import {\n type BillingStatus,\n type CustomerIdentifier,\n DEFAULT_API_HOST,\n type ExplicitJourneyStage,\n type IngestPayload,\n type ServerIdentifyOptions,\n type ServerIdentity,\n type ServerTrackOptions,\n type TrackerEvent,\n buildBillingEvent,\n buildCustomEvent,\n buildIdentifyEvent,\n buildStageEvent,\n validateServerIdentity,\n} from \"@outlit/core\"\nimport { EventQueue } from \"./queue\"\nimport { HttpTransport, TransportError } from \"./transport\"\n\n// ============================================\n// STAGE OPTIONS\n// ============================================\n\n/**\n * Options for stage transition events (activate, engaged, inactive).\n * Server-side stage events require at least one identifier (fingerprint, email, or userId).\n */\nexport interface StageOptions extends ServerIdentity {\n /**\n * Optional properties for context.\n */\n properties?: Record<string, string | number | boolean | null>\n}\n\n/**\n * Options for billing status events.\n * Requires at least one customer identifier (customerId, stripeCustomerId, or domain).\n */\nexport interface BillingOptions extends CustomerIdentifier {\n properties?: Record<string, string | number | boolean | null>\n}\n\n// ============================================\n// OUTLIT CLIENT\n// ============================================\n\nexport interface OutlitOptions {\n /**\n * Your Outlit public key.\n */\n publicKey: string\n\n /**\n * API host URL.\n * @default \"https://app.outlit.ai\"\n */\n apiHost?: string\n\n /**\n * How often to flush events (in milliseconds).\n * @default 10000 (10 seconds)\n */\n flushInterval?: number\n\n /**\n * Maximum number of events to batch before flushing.\n * @default 100\n */\n maxBatchSize?: number\n\n /**\n * Request timeout in milliseconds.\n * @default 10000 (10 seconds)\n */\n timeout?: number\n}\n\n/**\n * Outlit server-side tracking client.\n *\n * Supports tracking with fingerprint (device ID), email, or userId.\n * Use fingerprint for anonymous tracking that can be linked to users later.\n *\n * @example\n * ```typescript\n * import { Outlit } from '@outlit/node'\n *\n * const outlit = new Outlit({ publicKey: 'pk_xxx' })\n *\n * // Track with fingerprint only (anonymous, stored for later backfill)\n * outlit.track({\n * fingerprint: deviceId,\n * eventName: 'page_view',\n * properties: { page: '/pricing' }\n * })\n *\n * // Track with email (resolves immediately)\n * outlit.track({\n * email: 'user@example.com',\n * eventName: 'subscription_created',\n * properties: { plan: 'pro' }\n * })\n *\n * // Identify user and link fingerprint to email\n * outlit.identify({\n * email: 'user@example.com',\n * fingerprint: deviceId, // Links this device to the user\n * userId: 'usr_123',\n * traits: { name: 'John Doe' }\n * })\n *\n * // Flush before shutdown (important for serverless)\n * await outlit.flush()\n * ```\n */\nexport class Outlit {\n private transport: HttpTransport\n private queue: EventQueue\n private flushTimer: ReturnType<typeof setInterval> | null = null\n private flushInterval: number\n private isShutdown = false\n private fatalTransportError: TransportError | null = null\n\n constructor(options: OutlitOptions) {\n const apiHost = options.apiHost ?? DEFAULT_API_HOST\n this.flushInterval = options.flushInterval ?? 10000\n\n this.transport = new HttpTransport({\n apiHost,\n publicKey: options.publicKey,\n timeout: options.timeout,\n })\n\n this.queue = new EventQueue({\n maxSize: options.maxBatchSize ?? 100,\n onFlush: async (events) => {\n await this.sendEvents(events)\n },\n })\n\n // Start flush timer\n this.startFlushTimer()\n }\n\n /**\n * Track a custom event.\n *\n * Requires at least one of: `fingerprint`, `email`, or `userId`.\n *\n * - Use `fingerprint` for anonymous tracking (events linked later via identify)\n * - Use `email` for immediate user resolution\n * - Use `userId` for app-specific user identification\n *\n * @throws Error if no identity is provided\n */\n track(options: ServerTrackOptions): void {\n this.ensureNotShutdown()\n validateServerIdentity(options.fingerprint, options.email, options.userId)\n\n const event = buildCustomEvent({\n url: `server://${options.email ?? options.userId ?? options.fingerprint}`,\n timestamp: options.timestamp,\n eventName: options.eventName,\n properties: {\n ...options.properties,\n // Include identity in properties for server-side resolution\n __fingerprint: options.fingerprint ?? null,\n __email: options.email ?? null,\n __userId: options.userId ?? null,\n },\n })\n\n this.queue.enqueue(event)\n }\n\n /**\n * Identify or update a user.\n *\n * Requires `email` to establish definitive identity.\n * Optionally include `fingerprint` and/or `userId` to link them to this email.\n *\n * This is how you link anonymous fingerprint-tracked events to a real user:\n * ```typescript\n * outlit.identify({\n * email: 'user@example.com',\n * fingerprint: deviceId, // Links this device to the user\n * userId: 'usr_123', // Links this app user ID to the user\n * });\n * ```\n *\n * @throws Error if email is not provided\n */\n identify(options: ServerIdentifyOptions): void {\n this.ensureNotShutdown()\n\n // Identify requires email (definitive identity)\n if (!options.email) {\n throw new Error(\n \"identify() requires email to establish definitive identity. \" +\n \"Use fingerprint and/or userId as optional fields to link them to the email.\",\n )\n }\n\n const event = buildIdentifyEvent({\n url: `server://${options.email}`,\n email: options.email,\n userId: options.userId,\n fingerprint: options.fingerprint,\n traits: options.traits,\n })\n\n this.queue.enqueue(event)\n }\n\n /**\n * User namespace methods for contact journey stages.\n */\n readonly user = {\n identify: (options: ServerIdentifyOptions) => this.identify(options),\n activate: (options: StageOptions) => this.sendStageEvent(\"activated\", options),\n engaged: (options: StageOptions) => this.sendStageEvent(\"engaged\", options),\n inactive: (options: StageOptions) => this.sendStageEvent(\"inactive\", options),\n }\n\n /**\n * Customer namespace methods for billing status.\n */\n readonly customer = {\n trialing: (options: BillingOptions) => this.sendBillingEvent(\"trialing\", options),\n paid: (options: BillingOptions) => this.sendBillingEvent(\"paid\", options),\n churned: (options: BillingOptions) => this.sendBillingEvent(\"churned\", options),\n }\n\n /**\n * Internal method to send a stage event.\n */\n private sendStageEvent(stage: ExplicitJourneyStage, options: StageOptions): void {\n this.ensureNotShutdown()\n validateServerIdentity(options.fingerprint, options.email, options.userId)\n\n const event = buildStageEvent({\n url: `server://${options.email ?? options.userId ?? options.fingerprint}`,\n stage,\n properties: {\n ...options.properties,\n // Include identity in properties for server-side resolution\n __fingerprint: options.fingerprint ?? null,\n __email: options.email ?? null,\n __userId: options.userId ?? null,\n },\n })\n\n this.queue.enqueue(event)\n }\n\n private sendBillingEvent(status: BillingStatus, options: BillingOptions): void {\n this.ensureNotShutdown()\n\n const event = buildBillingEvent({\n url: `server://${options.domain}`,\n status,\n customerId: options.customerId,\n stripeCustomerId: options.stripeCustomerId,\n domain: options.domain,\n properties: options.properties,\n })\n\n this.queue.enqueue(event)\n }\n\n /**\n * Flush all pending events immediately.\n *\n * Important: Call this before your serverless function exits!\n */\n async flush(): Promise<void> {\n await this.queue.flush()\n }\n\n /**\n * Shutdown the client gracefully.\n *\n * Flushes remaining events and stops the flush timer.\n */\n async shutdown(): Promise<void> {\n if (this.isShutdown) return\n\n this.isShutdown = true\n\n if (this.flushTimer) {\n clearInterval(this.flushTimer)\n this.flushTimer = null\n }\n\n await this.flush()\n }\n\n /**\n * Get the number of events waiting to be sent.\n */\n get queueSize(): number {\n return this.queue.size\n }\n\n // ============================================\n // INTERNAL METHODS\n // ============================================\n\n private startFlushTimer(): void {\n if (this.flushTimer) return\n\n this.flushTimer = setInterval(() => {\n this.flush().catch((error) => {\n if (this.isNonRetryableTransportError(error)) {\n // Already handled and logged in sendEvents().\n return\n }\n console.error(\"[Outlit] Flush error:\", error)\n })\n }, this.flushInterval)\n\n // Don't block process exit\n if (this.flushTimer.unref) {\n this.flushTimer.unref()\n }\n }\n\n private async sendEvents(events: TrackerEvent[]): Promise<void> {\n if (events.length === 0) return\n if (this.fatalTransportError) return\n\n // For server events, we don't use visitorId - the API resolves identity\n // directly from the event data (email/userId)\n const payload: IngestPayload = {\n source: \"server\",\n events,\n // visitorId is intentionally omitted for server events\n }\n\n try {\n await this.transport.send(payload)\n } catch (error) {\n if (this.isNonRetryableTransportError(error)) {\n this.handleFatalTransportError(error)\n }\n throw error\n }\n }\n\n private ensureNotShutdown(): void {\n if (this.isShutdown) {\n throw new Error(\n \"[Outlit] Client has been shutdown. Create a new instance to continue tracking.\",\n )\n }\n }\n\n private isNonRetryableTransportError(error: unknown): error is TransportError {\n return error instanceof TransportError && error.retryable === false\n }\n\n private handleFatalTransportError(error: TransportError): void {\n if (this.fatalTransportError) return\n\n this.fatalTransportError = error\n\n if (this.flushTimer) {\n clearInterval(this.flushTimer)\n this.flushTimer = null\n }\n\n console.error(\n \"[Outlit] Non-retryable ingest error. Automatic flush retries disabled until client restart:\",\n error,\n )\n }\n}\n","import type { TrackerEvent } from \"@outlit/core\"\n\n// ============================================\n// EVENT QUEUE\n// ============================================\n\nfunction isNonRetryableError(error: unknown): boolean {\n if (!error || typeof error !== \"object\") return false\n if (!(\"retryable\" in error)) return false\n\n return (error as { retryable?: unknown }).retryable === false\n}\n\nexport interface QueueOptions {\n maxSize?: number\n onFlush: (events: TrackerEvent[]) => Promise<void>\n}\n\nexport class EventQueue {\n private queue: TrackerEvent[] = []\n private maxSize: number\n private onFlush: (events: TrackerEvent[]) => Promise<void>\n private isFlushing = false\n\n constructor(options: QueueOptions) {\n this.maxSize = options.maxSize ?? 100\n this.onFlush = options.onFlush\n }\n\n /**\n * Add an event to the queue.\n * Triggers flush if queue reaches max size.\n */\n async enqueue(event: TrackerEvent): Promise<void> {\n this.queue.push(event)\n\n if (this.queue.length >= this.maxSize) {\n await this.flush()\n }\n }\n\n /**\n * Flush all events in the queue.\n */\n async flush(): Promise<void> {\n if (this.isFlushing || this.queue.length === 0) return\n\n this.isFlushing = true\n const events = [...this.queue]\n this.queue = []\n\n try {\n await this.onFlush(events)\n } catch (error) {\n // Re-add events for retryable failures only.\n // Non-retryable failures (e.g. invalid config 4xx) are dropped to avoid infinite retry loops.\n if (!isNonRetryableError(error)) {\n this.queue = [...events, ...this.queue]\n }\n throw error\n } finally {\n this.isFlushing = false\n }\n }\n\n /**\n * Get the number of events in the queue.\n */\n get size(): number {\n return this.queue.length\n }\n\n /**\n * Check if the queue is currently flushing.\n */\n get flushing(): boolean {\n return this.isFlushing\n }\n}\n","import type { IngestPayload, IngestResponse } from \"@outlit/core\"\n\n// ============================================\n// HTTP TRANSPORT\n// ============================================\n\ninterface TransportErrorOptions {\n status?: number\n retryable?: boolean\n cause?: unknown\n}\n\n/**\n * Transport-level error enriched with retryability metadata.\n */\nexport class TransportError extends Error {\n readonly status?: number\n readonly retryable: boolean\n\n constructor(message: string, options?: TransportErrorOptions) {\n super(message)\n this.name = \"TransportError\"\n this.status = options?.status\n this.retryable = options?.retryable ?? true\n\n if (options?.cause !== undefined) {\n this.cause = options.cause\n }\n }\n}\n\nfunction isRetryableStatus(status: number): boolean {\n // 429 is rate limiting (transient). Most other 4xx are configuration/input errors.\n return status === 429 || status >= 500\n}\n\nexport interface TransportOptions {\n apiHost: string\n publicKey: string\n timeout?: number\n}\n\nexport class HttpTransport {\n private apiHost: string\n private publicKey: string\n private timeout: number\n\n constructor(options: TransportOptions) {\n this.apiHost = options.apiHost\n this.publicKey = options.publicKey\n this.timeout = options.timeout ?? 10000\n }\n\n /**\n * Send events to the ingest API.\n */\n async send(payload: IngestPayload): Promise<IngestResponse> {\n const url = `${this.apiHost}/api/i/v1/${this.publicKey}/events`\n\n const controller = new AbortController()\n const timeoutId = setTimeout(() => controller.abort(), this.timeout)\n\n try {\n const response = await fetch(url, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n },\n body: JSON.stringify(payload),\n signal: controller.signal,\n })\n\n if (!response.ok) {\n const errorBody = await response.text().catch(() => \"Unknown error\")\n throw new TransportError(`HTTP ${response.status}: ${errorBody}`, {\n status: response.status,\n retryable: isRetryableStatus(response.status),\n })\n }\n\n return (await response.json()) as IngestResponse\n } catch (error) {\n if (error instanceof TransportError) {\n throw error\n }\n\n if (error instanceof Error && error.name === \"AbortError\") {\n throw new TransportError(`Request timed out after ${this.timeout}ms`, {\n retryable: true,\n cause: error,\n })\n }\n\n if (error instanceof Error) {\n throw new TransportError(error.message, {\n retryable: true,\n cause: error,\n })\n }\n\n throw new TransportError(\"Unknown transport error\", {\n retryable: true,\n cause: error,\n })\n } finally {\n clearTimeout(timeoutId)\n }\n }\n}\n"],"mappings":";AAAA;AAAA,EAGE;AAAA,EAOA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;;;ACTP,SAAS,oBAAoB,OAAyB;AACpD,MAAI,CAAC,SAAS,OAAO,UAAU,SAAU,QAAO;AAChD,MAAI,EAAE,eAAe,OAAQ,QAAO;AAEpC,SAAQ,MAAkC,cAAc;AAC1D;AAOO,IAAM,aAAN,MAAiB;AAAA,EACd,QAAwB,CAAC;AAAA,EACzB;AAAA,EACA;AAAA,EACA,aAAa;AAAA,EAErB,YAAY,SAAuB;AACjC,SAAK,UAAU,QAAQ,WAAW;AAClC,SAAK,UAAU,QAAQ;AAAA,EACzB;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,MAAM,QAAQ,OAAoC;AAChD,SAAK,MAAM,KAAK,KAAK;AAErB,QAAI,KAAK,MAAM,UAAU,KAAK,SAAS;AACrC,YAAM,KAAK,MAAM;AAAA,IACnB;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,QAAuB;AAC3B,QAAI,KAAK,cAAc,KAAK,MAAM,WAAW,EAAG;AAEhD,SAAK,aAAa;AAClB,UAAM,SAAS,CAAC,GAAG,KAAK,KAAK;AAC7B,SAAK,QAAQ,CAAC;AAEd,QAAI;AACF,YAAM,KAAK,QAAQ,MAAM;AAAA,IAC3B,SAAS,OAAO;AAGd,UAAI,CAAC,oBAAoB,KAAK,GAAG;AAC/B,aAAK,QAAQ,CAAC,GAAG,QAAQ,GAAG,KAAK,KAAK;AAAA,MACxC;AACA,YAAM;AAAA,IACR,UAAE;AACA,WAAK,aAAa;AAAA,IACpB;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKA,IAAI,OAAe;AACjB,WAAO,KAAK,MAAM;AAAA,EACpB;AAAA;AAAA;AAAA;AAAA,EAKA,IAAI,WAAoB;AACtB,WAAO,KAAK;AAAA,EACd;AACF;;;AC/DO,IAAM,iBAAN,cAA6B,MAAM;AAAA,EAC/B;AAAA,EACA;AAAA,EAET,YAAY,SAAiB,SAAiC;AAC5D,UAAM,OAAO;AACb,SAAK,OAAO;AACZ,SAAK,SAAS,SAAS;AACvB,SAAK,YAAY,SAAS,aAAa;AAEvC,QAAI,SAAS,UAAU,QAAW;AAChC,WAAK,QAAQ,QAAQ;AAAA,IACvB;AAAA,EACF;AACF;AAEA,SAAS,kBAAkB,QAAyB;AAElD,SAAO,WAAW,OAAO,UAAU;AACrC;AAQO,IAAM,gBAAN,MAAoB;AAAA,EACjB;AAAA,EACA;AAAA,EACA;AAAA,EAER,YAAY,SAA2B;AACrC,SAAK,UAAU,QAAQ;AACvB,SAAK,YAAY,QAAQ;AACzB,SAAK,UAAU,QAAQ,WAAW;AAAA,EACpC;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,KAAK,SAAiD;AAC1D,UAAM,MAAM,GAAG,KAAK,OAAO,aAAa,KAAK,SAAS;AAEtD,UAAM,aAAa,IAAI,gBAAgB;AACvC,UAAM,YAAY,WAAW,MAAM,WAAW,MAAM,GAAG,KAAK,OAAO;AAEnE,QAAI;AACF,YAAM,WAAW,MAAM,MAAM,KAAK;AAAA,QAChC,QAAQ;AAAA,QACR,SAAS;AAAA,UACP,gBAAgB;AAAA,QAClB;AAAA,QACA,MAAM,KAAK,UAAU,OAAO;AAAA,QAC5B,QAAQ,WAAW;AAAA,MACrB,CAAC;AAED,UAAI,CAAC,SAAS,IAAI;AAChB,cAAM,YAAY,MAAM,SAAS,KAAK,EAAE,MAAM,MAAM,eAAe;AACnE,cAAM,IAAI,eAAe,QAAQ,SAAS,MAAM,KAAK,SAAS,IAAI;AAAA,UAChE,QAAQ,SAAS;AAAA,UACjB,WAAW,kBAAkB,SAAS,MAAM;AAAA,QAC9C,CAAC;AAAA,MACH;AAEA,aAAQ,MAAM,SAAS,KAAK;AAAA,IAC9B,SAAS,OAAO;AACd,UAAI,iBAAiB,gBAAgB;AACnC,cAAM;AAAA,MACR;AAEA,UAAI,iBAAiB,SAAS,MAAM,SAAS,cAAc;AACzD,cAAM,IAAI,eAAe,2BAA2B,KAAK,OAAO,MAAM;AAAA,UACpE,WAAW;AAAA,UACX,OAAO;AAAA,QACT,CAAC;AAAA,MACH;AAEA,UAAI,iBAAiB,OAAO;AAC1B,cAAM,IAAI,eAAe,MAAM,SAAS;AAAA,UACtC,WAAW;AAAA,UACX,OAAO;AAAA,QACT,CAAC;AAAA,MACH;AAEA,YAAM,IAAI,eAAe,2BAA2B;AAAA,QAClD,WAAW;AAAA,QACX,OAAO;AAAA,MACT,CAAC;AAAA,IACH,UAAE;AACA,mBAAa,SAAS;AAAA,IACxB;AAAA,EACF;AACF;;;AFOO,IAAM,SAAN,MAAa;AAAA,EACV;AAAA,EACA;AAAA,EACA,aAAoD;AAAA,EACpD;AAAA,EACA,aAAa;AAAA,EACb,sBAA6C;AAAA,EAErD,YAAY,SAAwB;AAClC,UAAM,UAAU,QAAQ,WAAW;AACnC,SAAK,gBAAgB,QAAQ,iBAAiB;AAE9C,SAAK,YAAY,IAAI,cAAc;AAAA,MACjC;AAAA,MACA,WAAW,QAAQ;AAAA,MACnB,SAAS,QAAQ;AAAA,IACnB,CAAC;AAED,SAAK,QAAQ,IAAI,WAAW;AAAA,MAC1B,SAAS,QAAQ,gBAAgB;AAAA,MACjC,SAAS,OAAO,WAAW;AACzB,cAAM,KAAK,WAAW,MAAM;AAAA,MAC9B;AAAA,IACF,CAAC;AAGD,SAAK,gBAAgB;AAAA,EACvB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAaA,MAAM,SAAmC;AACvC,SAAK,kBAAkB;AACvB,2BAAuB,QAAQ,aAAa,QAAQ,OAAO,QAAQ,MAAM;AAEzE,UAAM,QAAQ,iBAAiB;AAAA,MAC7B,KAAK,YAAY,QAAQ,SAAS,QAAQ,UAAU,QAAQ,WAAW;AAAA,MACvE,WAAW,QAAQ;AAAA,MACnB,WAAW,QAAQ;AAAA,MACnB,YAAY;AAAA,QACV,GAAG,QAAQ;AAAA;AAAA,QAEX,eAAe,QAAQ,eAAe;AAAA,QACtC,SAAS,QAAQ,SAAS;AAAA,QAC1B,UAAU,QAAQ,UAAU;AAAA,MAC9B;AAAA,IACF,CAAC;AAED,SAAK,MAAM,QAAQ,KAAK;AAAA,EAC1B;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAmBA,SAAS,SAAsC;AAC7C,SAAK,kBAAkB;AAGvB,QAAI,CAAC,QAAQ,OAAO;AAClB,YAAM,IAAI;AAAA,QACR;AAAA,MAEF;AAAA,IACF;AAEA,UAAM,QAAQ,mBAAmB;AAAA,MAC/B,KAAK,YAAY,QAAQ,KAAK;AAAA,MAC9B,OAAO,QAAQ;AAAA,MACf,QAAQ,QAAQ;AAAA,MAChB,aAAa,QAAQ;AAAA,MACrB,QAAQ,QAAQ;AAAA,IAClB,CAAC;AAED,SAAK,MAAM,QAAQ,KAAK;AAAA,EAC1B;AAAA;AAAA;AAAA;AAAA,EAKS,OAAO;AAAA,IACd,UAAU,CAAC,YAAmC,KAAK,SAAS,OAAO;AAAA,IACnE,UAAU,CAAC,YAA0B,KAAK,eAAe,aAAa,OAAO;AAAA,IAC7E,SAAS,CAAC,YAA0B,KAAK,eAAe,WAAW,OAAO;AAAA,IAC1E,UAAU,CAAC,YAA0B,KAAK,eAAe,YAAY,OAAO;AAAA,EAC9E;AAAA;AAAA;AAAA;AAAA,EAKS,WAAW;AAAA,IAClB,UAAU,CAAC,YAA4B,KAAK,iBAAiB,YAAY,OAAO;AAAA,IAChF,MAAM,CAAC,YAA4B,KAAK,iBAAiB,QAAQ,OAAO;AAAA,IACxE,SAAS,CAAC,YAA4B,KAAK,iBAAiB,WAAW,OAAO;AAAA,EAChF;AAAA;AAAA;AAAA;AAAA,EAKQ,eAAe,OAA6B,SAA6B;AAC/E,SAAK,kBAAkB;AACvB,2BAAuB,QAAQ,aAAa,QAAQ,OAAO,QAAQ,MAAM;AAEzE,UAAM,QAAQ,gBAAgB;AAAA,MAC5B,KAAK,YAAY,QAAQ,SAAS,QAAQ,UAAU,QAAQ,WAAW;AAAA,MACvE;AAAA,MACA,YAAY;AAAA,QACV,GAAG,QAAQ;AAAA;AAAA,QAEX,eAAe,QAAQ,eAAe;AAAA,QACtC,SAAS,QAAQ,SAAS;AAAA,QAC1B,UAAU,QAAQ,UAAU;AAAA,MAC9B;AAAA,IACF,CAAC;AAED,SAAK,MAAM,QAAQ,KAAK;AAAA,EAC1B;AAAA,EAEQ,iBAAiB,QAAuB,SAA+B;AAC7E,SAAK,kBAAkB;AAEvB,UAAM,QAAQ,kBAAkB;AAAA,MAC9B,KAAK,YAAY,QAAQ,MAAM;AAAA,MAC/B;AAAA,MACA,YAAY,QAAQ;AAAA,MACpB,kBAAkB,QAAQ;AAAA,MAC1B,QAAQ,QAAQ;AAAA,MAChB,YAAY,QAAQ;AAAA,IACtB,CAAC;AAED,SAAK,MAAM,QAAQ,KAAK;AAAA,EAC1B;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,MAAM,QAAuB;AAC3B,UAAM,KAAK,MAAM,MAAM;AAAA,EACzB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,MAAM,WAA0B;AAC9B,QAAI,KAAK,WAAY;AAErB,SAAK,aAAa;AAElB,QAAI,KAAK,YAAY;AACnB,oBAAc,KAAK,UAAU;AAC7B,WAAK,aAAa;AAAA,IACpB;AAEA,UAAM,KAAK,MAAM;AAAA,EACnB;AAAA;AAAA;AAAA;AAAA,EAKA,IAAI,YAAoB;AACtB,WAAO,KAAK,MAAM;AAAA,EACpB;AAAA;AAAA;AAAA;AAAA,EAMQ,kBAAwB;AAC9B,QAAI,KAAK,WAAY;AAErB,SAAK,aAAa,YAAY,MAAM;AAClC,WAAK,MAAM,EAAE,MAAM,CAAC,UAAU;AAC5B,YAAI,KAAK,6BAA6B,KAAK,GAAG;AAE5C;AAAA,QACF;AACA,gBAAQ,MAAM,yBAAyB,KAAK;AAAA,MAC9C,CAAC;AAAA,IACH,GAAG,KAAK,aAAa;AAGrB,QAAI,KAAK,WAAW,OAAO;AACzB,WAAK,WAAW,MAAM;AAAA,IACxB;AAAA,EACF;AAAA,EAEA,MAAc,WAAW,QAAuC;AAC9D,QAAI,OAAO,WAAW,EAAG;AACzB,QAAI,KAAK,oBAAqB;AAI9B,UAAM,UAAyB;AAAA,MAC7B,QAAQ;AAAA,MACR;AAAA;AAAA,IAEF;AAEA,QAAI;AACF,YAAM,KAAK,UAAU,KAAK,OAAO;AAAA,IACnC,SAAS,OAAO;AACd,UAAI,KAAK,6BAA6B,KAAK,GAAG;AAC5C,aAAK,0BAA0B,KAAK;AAAA,MACtC;AACA,YAAM;AAAA,IACR;AAAA,EACF;AAAA,EAEQ,oBAA0B;AAChC,QAAI,KAAK,YAAY;AACnB,YAAM,IAAI;AAAA,QACR;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAAA,EAEQ,6BAA6B,OAAyC;AAC5E,WAAO,iBAAiB,kBAAkB,MAAM,cAAc;AAAA,EAChE;AAAA,EAEQ,0BAA0B,OAA6B;AAC7D,QAAI,KAAK,oBAAqB;AAE9B,SAAK,sBAAsB;AAE3B,QAAI,KAAK,YAAY;AACnB,oBAAc,KAAK,UAAU;AAC7B,WAAK,aAAa;AAAA,IACpB;AAEA,YAAQ;AAAA,MACN;AAAA,MACA;AAAA,IACF;AAAA,EACF;AACF;","names":[]}
1
+ {"version":3,"sources":["../src/client.ts","../src/queue.ts","../src/transport.ts"],"sourcesContent":["import {\n type BillingStatus,\n buildBillingEvent,\n buildCustomEvent,\n buildIdentifyEvent,\n buildStageEvent,\n type CustomerIdentifier,\n DEFAULT_API_HOST,\n type ExplicitJourneyStage,\n type IngestPayload,\n type ServerIdentifyOptions,\n type ServerIdentity,\n type ServerTrackOptions,\n type TrackerEvent,\n validateCustomerIdentity,\n validateServerIdentity,\n} from \"@outlit/core\"\nimport { EventQueue } from \"./queue\"\nimport { HttpTransport, TransportError } from \"./transport\"\n\n// ============================================\n// STAGE OPTIONS\n// ============================================\n\n/**\n * Options for stage transition events (activate, engaged, inactive).\n * Server-side stage events require at least one identifier (fingerprint, email, or userId).\n */\nexport interface StageOptions extends ServerIdentity {\n /**\n * Optional properties for context.\n */\n properties?: Record<string, string | number | boolean | null>\n}\n\n/**\n * Options for billing status events.\n * Public billing calls should use customerId.\n */\nexport interface BillingOptions extends CustomerIdentifier {\n properties?: Record<string, string | number | boolean | null>\n}\n\n// ============================================\n// OUTLIT CLIENT\n// ============================================\n\nexport interface OutlitOptions {\n /**\n * Your Outlit public key.\n */\n publicKey: string\n\n /**\n * API host URL.\n * @default \"https://app.outlit.ai\"\n */\n apiHost?: string\n\n /**\n * How often to flush events (in milliseconds).\n * @default 10000 (10 seconds)\n */\n flushInterval?: number\n\n /**\n * Maximum number of events to batch before flushing.\n * @default 100\n */\n maxBatchSize?: number\n\n /**\n * Request timeout in milliseconds.\n * @default 10000 (10 seconds)\n */\n timeout?: number\n}\n\n/**\n * Outlit server-side tracking client.\n *\n * Supports tracking with fingerprint (device ID), email, userId, or customer attribution.\n * Use fingerprint for anonymous tracking that can be linked to users later.\n *\n * @example\n * ```typescript\n * import { Outlit } from '@outlit/node'\n *\n * const outlit = new Outlit({ publicKey: 'pk_xxx' })\n *\n * // Track with fingerprint only (anonymous, stored for later backfill)\n * outlit.track({\n * fingerprint: deviceId,\n * eventName: 'page_view',\n * properties: { page: '/pricing' }\n * })\n *\n * // Track with customer attribution only\n * outlit.track({\n * customerId: 'cust_123',\n * eventName: 'account_synced'\n * })\n * // `customerId`-only events stay attributable immediately and can later\n * // link to the email-resolved customer when identify() uses the same customerId.\n *\n * // Track with email (resolves immediately)\n * outlit.track({\n * email: 'user@example.com',\n * eventName: 'subscription_created',\n * properties: { plan: 'pro' }\n * })\n *\n * // Identify user and link fingerprint to email\n * outlit.identify({\n * email: 'user@example.com',\n * fingerprint: deviceId, // Links this device to the user\n * userId: 'usr_123',\n * traits: { name: 'John Doe' }\n * })\n *\n * // Flush before shutdown (important for serverless)\n * await outlit.flush()\n * ```\n */\nexport class Outlit {\n private transport: HttpTransport\n private queue: EventQueue\n private flushTimer: ReturnType<typeof setInterval> | null = null\n private flushInterval: number\n private isShutdown = false\n private fatalTransportError: TransportError | null = null\n\n constructor(options: OutlitOptions) {\n const apiHost = options.apiHost ?? DEFAULT_API_HOST\n this.flushInterval = options.flushInterval ?? 10000\n\n this.transport = new HttpTransport({\n apiHost,\n publicKey: options.publicKey,\n timeout: options.timeout,\n })\n\n this.queue = new EventQueue({\n maxSize: options.maxBatchSize ?? 100,\n onFlush: async (events) => {\n await this.sendEvents(events)\n },\n })\n\n // Start flush timer\n this.startFlushTimer()\n }\n\n /**\n * Track a custom event.\n *\n * Requires at least one of: `fingerprint`, `email`, `userId`, or `customerId`.\n *\n * - Use `fingerprint` for anonymous tracking (events linked later via identify)\n * - Use `email` or `userId` for user-scoped attribution\n * - Use `customerId` for customer/account/workspace-scoped attribution\n * - If you later call `identify({ email, customerId })`, Outlit can link earlier\n * `customerId`-only events to the customer resolved from email\n * - `userId` is your system-owned user/contact ID\n * - `customerId` is your system-owned customer/account/workspace ID\n *\n * @throws Error if no identity is provided\n */\n track(options: ServerTrackOptions): void {\n this.ensureNotShutdown()\n validateServerIdentity(options.fingerprint, options.email, options.userId, options.customerId)\n\n const event = buildCustomEvent({\n url: `server://${\n options.email ?? options.userId ?? options.customerId ?? options.fingerprint\n }`,\n timestamp: options.timestamp,\n eventName: options.eventName,\n email: options.email,\n userId: options.userId,\n fingerprint: options.fingerprint,\n customerId: options.customerId,\n properties: options.properties,\n })\n\n this.queue.enqueue(event)\n }\n\n /**\n * Identify or update a user.\n *\n * Requires `email` or `userId` to establish user-scoped identity.\n * Optionally include `fingerprint` and customer attribution fields to link them.\n * `userId` is your system-owned user/contact ID and `customerId` is your system-owned\n * customer/account/workspace ID. When `email` and `customerId` are sent together,\n * Outlit can link that account/workspace to the customer resolved from email.\n *\n * This is how you link anonymous fingerprint-tracked events to a real user:\n * ```typescript\n * outlit.identify({\n * email: 'user@example.com',\n * fingerprint: deviceId, // Links this device to the user\n * userId: 'usr_123', // Links this app user ID to the user\n * });\n * ```\n *\n * @throws Error if neither email nor userId is provided\n */\n identify(options: ServerIdentifyOptions): void {\n this.ensureNotShutdown()\n\n // Identify requires user-scoped identity.\n if (!options.email && !options.userId) {\n throw new Error(\n \"identify() requires email or userId to establish user-scoped identity. \" +\n \"Use customerId as the optional account/workspace identifier for linking attribution.\",\n )\n }\n\n const event = buildIdentifyEvent({\n url: `server://${options.email ?? options.userId}`,\n email: options.email,\n userId: options.userId,\n fingerprint: options.fingerprint,\n traits: options.traits,\n customerId: options.customerId,\n customerTraits: options.customerTraits,\n })\n\n this.queue.enqueue(event)\n }\n\n /**\n * User namespace methods for contact journey stages.\n */\n readonly user = {\n identify: (options: ServerIdentifyOptions) => this.identify(options),\n activate: (options: StageOptions) => this.sendStageEvent(\"activated\", options),\n engaged: (options: StageOptions) => this.sendStageEvent(\"engaged\", options),\n inactive: (options: StageOptions) => this.sendStageEvent(\"inactive\", options),\n }\n\n /**\n * Customer namespace methods for billing status.\n */\n readonly customer = {\n trialing: (options: BillingOptions) => this.sendBillingEvent(\"trialing\", options),\n paid: (options: BillingOptions) => this.sendBillingEvent(\"paid\", options),\n churned: (options: BillingOptions) => this.sendBillingEvent(\"churned\", options),\n }\n\n /**\n * Internal method to send a stage event.\n */\n private sendStageEvent(stage: ExplicitJourneyStage, options: StageOptions): void {\n this.ensureNotShutdown()\n validateServerIdentity(options.fingerprint, options.email, options.userId)\n\n const event = buildStageEvent({\n url: `server://${options.email ?? options.userId ?? options.fingerprint}`,\n stage,\n properties: {\n ...options.properties,\n __fingerprint: options.fingerprint ?? null,\n __email: options.email ?? null,\n __userId: options.userId ?? null,\n },\n })\n\n this.queue.enqueue(event)\n }\n\n private sendBillingEvent(status: BillingStatus, options: BillingOptions): void {\n this.ensureNotShutdown()\n validateCustomerIdentity(options.customerId, options.stripeCustomerId)\n\n const event = buildBillingEvent({\n url: `server://${options.customerId ?? options.stripeCustomerId}`,\n status,\n customerId: options.customerId,\n stripeCustomerId: options.stripeCustomerId,\n properties: options.properties,\n })\n\n this.queue.enqueue(event)\n }\n\n /**\n * Flush all pending events immediately.\n *\n * Important: Call this before your serverless function exits!\n */\n async flush(): Promise<void> {\n await this.queue.flush()\n }\n\n /**\n * Shutdown the client gracefully.\n *\n * Flushes remaining events and stops the flush timer.\n */\n async shutdown(): Promise<void> {\n if (this.isShutdown) return\n\n this.isShutdown = true\n\n if (this.flushTimer) {\n clearInterval(this.flushTimer)\n this.flushTimer = null\n }\n\n await this.flush()\n }\n\n /**\n * Get the number of events waiting to be sent.\n */\n get queueSize(): number {\n return this.queue.size\n }\n\n // ============================================\n // INTERNAL METHODS\n // ============================================\n\n private startFlushTimer(): void {\n if (this.flushTimer) return\n\n this.flushTimer = setInterval(() => {\n this.flush().catch((error) => {\n if (this.isNonRetryableTransportError(error)) {\n // Already handled and logged in sendEvents().\n return\n }\n console.error(\"[Outlit] Flush error:\", error)\n })\n }, this.flushInterval)\n\n // Don't block process exit\n if (this.flushTimer.unref) {\n this.flushTimer.unref()\n }\n }\n\n private async sendEvents(events: TrackerEvent[]): Promise<void> {\n if (events.length === 0) return\n if (this.fatalTransportError) return\n\n // For server events, we don't use visitorId - the API resolves identity\n // directly from the event data (email/userId)\n const payload: IngestPayload = {\n source: \"server\",\n events,\n // visitorId is intentionally omitted for server events\n }\n\n try {\n await this.transport.send(payload)\n } catch (error) {\n if (this.isNonRetryableTransportError(error)) {\n this.handleFatalTransportError(error)\n }\n throw error\n }\n }\n\n private ensureNotShutdown(): void {\n if (this.isShutdown) {\n throw new Error(\n \"[Outlit] Client has been shutdown. Create a new instance to continue tracking.\",\n )\n }\n }\n\n private isNonRetryableTransportError(error: unknown): error is TransportError {\n return error instanceof TransportError && error.retryable === false\n }\n\n private handleFatalTransportError(error: TransportError): void {\n if (this.fatalTransportError) return\n\n this.fatalTransportError = error\n\n if (this.flushTimer) {\n clearInterval(this.flushTimer)\n this.flushTimer = null\n }\n\n console.error(\n \"[Outlit] Non-retryable ingest error. Automatic flush retries disabled until client restart:\",\n error,\n )\n }\n}\n","import type { TrackerEvent } from \"@outlit/core\"\n\n// ============================================\n// EVENT QUEUE\n// ============================================\n\nfunction isNonRetryableError(error: unknown): boolean {\n if (!error || typeof error !== \"object\") return false\n if (!(\"retryable\" in error)) return false\n\n return (error as { retryable?: unknown }).retryable === false\n}\n\nexport interface QueueOptions {\n maxSize?: number\n onFlush: (events: TrackerEvent[]) => Promise<void>\n}\n\nexport class EventQueue {\n private queue: TrackerEvent[] = []\n private maxSize: number\n private onFlush: (events: TrackerEvent[]) => Promise<void>\n private isFlushing = false\n\n constructor(options: QueueOptions) {\n this.maxSize = options.maxSize ?? 100\n this.onFlush = options.onFlush\n }\n\n /**\n * Add an event to the queue.\n * Triggers flush if queue reaches max size.\n */\n async enqueue(event: TrackerEvent): Promise<void> {\n this.queue.push(event)\n\n if (this.queue.length >= this.maxSize) {\n await this.flush()\n }\n }\n\n /**\n * Flush all events in the queue.\n */\n async flush(): Promise<void> {\n if (this.isFlushing || this.queue.length === 0) return\n\n this.isFlushing = true\n const events = [...this.queue]\n this.queue = []\n\n try {\n await this.onFlush(events)\n } catch (error) {\n // Re-add events for retryable failures only.\n // Non-retryable failures (e.g. invalid config 4xx) are dropped to avoid infinite retry loops.\n if (!isNonRetryableError(error)) {\n this.queue = [...events, ...this.queue]\n }\n throw error\n } finally {\n this.isFlushing = false\n }\n }\n\n /**\n * Get the number of events in the queue.\n */\n get size(): number {\n return this.queue.length\n }\n\n /**\n * Check if the queue is currently flushing.\n */\n get flushing(): boolean {\n return this.isFlushing\n }\n}\n","import type { IngestPayload, IngestResponse } from \"@outlit/core\"\n\n// ============================================\n// HTTP TRANSPORT\n// ============================================\n\ninterface TransportErrorOptions {\n status?: number\n retryable?: boolean\n cause?: unknown\n}\n\n/**\n * Transport-level error enriched with retryability metadata.\n */\nexport class TransportError extends Error {\n readonly status?: number\n readonly retryable: boolean\n\n constructor(message: string, options?: TransportErrorOptions) {\n super(message)\n this.name = \"TransportError\"\n this.status = options?.status\n this.retryable = options?.retryable ?? true\n\n if (options?.cause !== undefined) {\n this.cause = options.cause\n }\n }\n}\n\nfunction isRetryableStatus(status: number): boolean {\n // 429 is rate limiting (transient). Most other 4xx are configuration/input errors.\n return status === 429 || status >= 500\n}\n\nexport interface TransportOptions {\n apiHost: string\n publicKey: string\n timeout?: number\n}\n\nexport class HttpTransport {\n private apiHost: string\n private publicKey: string\n private timeout: number\n\n constructor(options: TransportOptions) {\n this.apiHost = options.apiHost\n this.publicKey = options.publicKey\n this.timeout = options.timeout ?? 10000\n }\n\n /**\n * Send events to the ingest API.\n */\n async send(payload: IngestPayload): Promise<IngestResponse> {\n const url = `${this.apiHost}/api/i/v1/${this.publicKey}/events`\n\n const controller = new AbortController()\n const timeoutId = setTimeout(() => controller.abort(), this.timeout)\n\n try {\n const response = await fetch(url, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n },\n body: JSON.stringify(payload),\n signal: controller.signal,\n })\n\n if (!response.ok) {\n const errorBody = await response.text().catch(() => \"Unknown error\")\n throw new TransportError(`HTTP ${response.status}: ${errorBody}`, {\n status: response.status,\n retryable: isRetryableStatus(response.status),\n })\n }\n\n return (await response.json()) as IngestResponse\n } catch (error) {\n if (error instanceof TransportError) {\n throw error\n }\n\n if (error instanceof Error && error.name === \"AbortError\") {\n throw new TransportError(`Request timed out after ${this.timeout}ms`, {\n retryable: true,\n cause: error,\n })\n }\n\n if (error instanceof Error) {\n throw new TransportError(error.message, {\n retryable: true,\n cause: error,\n })\n }\n\n throw new TransportError(\"Unknown transport error\", {\n retryable: true,\n cause: error,\n })\n } finally {\n clearTimeout(timeoutId)\n }\n }\n}\n"],"mappings":"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","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@outlit/node",
3
- "version": "1.4.3",
3
+ "version": "1.5.0",
4
4
  "description": "Outlit server-side tracking SDK for Node.js",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Outlit AI",
@@ -13,7 +13,14 @@
13
13
  "url": "https://github.com/OutlitAI/outlit-sdk/issues"
14
14
  },
15
15
  "homepage": "https://github.com/OutlitAI/outlit-sdk#readme",
16
- "keywords": ["outlit", "analytics", "tracking", "sdk", "node", "server"],
16
+ "keywords": [
17
+ "outlit",
18
+ "analytics",
19
+ "tracking",
20
+ "sdk",
21
+ "node",
22
+ "server"
23
+ ],
17
24
  "publishConfig": {
18
25
  "access": "public"
19
26
  },
@@ -28,7 +35,9 @@
28
35
  "default": "./dist/index.mjs"
29
36
  }
30
37
  },
31
- "files": ["dist"],
38
+ "files": [
39
+ "dist"
40
+ ],
32
41
  "engines": {
33
42
  "node": ">=18.0.0"
34
43
  },
@@ -38,14 +47,16 @@
38
47
  "clean": "rm -rf dist .turbo node_modules",
39
48
  "lint": "biome check . --write",
40
49
  "format": "biome format --write .",
41
- "typecheck": "tsc --noEmit"
50
+ "typecheck": "tsc --noEmit",
51
+ "test": "vitest run"
42
52
  },
43
53
  "dependencies": {
44
- "@outlit/core": "^1.4.3"
54
+ "@outlit/core": "^1.5.0"
45
55
  },
46
56
  "devDependencies": {
47
57
  "@outlit/typescript-config": "workspace:*",
48
- "tsup": "^8.0.1",
49
- "typescript": "^5.3.3"
58
+ "tsup": "^8.5.1",
59
+ "typescript": "^5.9.3",
60
+ "vitest": "^4.0.18"
50
61
  }
51
62
  }