@outlit/node 1.5.1 → 2.0.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/README.md +21 -0
- package/dist/index.d.mts +7 -41
- package/dist/index.d.ts +7 -41
- package/dist/index.js +2 -45
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +2 -48
- package/dist/index.mjs.map +1 -1
- package/package.json +6 -4
package/README.md
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# @outlit/node
|
|
2
|
+
|
|
3
|
+
Node.js tracking SDK for Outlit customer context.
|
|
4
|
+
|
|
5
|
+
Use this package to track ordinary backend events, identify users, attach customer account traits, and flush server-side event queues before shutdown. Core derives lifecycle from identity and activity, including the customer-selected ordinary activation event; billing comes from verified integrations.
|
|
6
|
+
|
|
7
|
+
Outlit is the real-time understanding of every customer, the infrastructure agents use to automate customer operations.
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npm install @outlit/node
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Canonical Docs
|
|
16
|
+
|
|
17
|
+
- Node.js SDK docs: <https://docs.outlit.ai/tracking/server/nodejs>
|
|
18
|
+
- Quick start: <https://docs.outlit.ai/tracking/quickstart>
|
|
19
|
+
- Ingest API reference: <https://docs.outlit.ai/api-reference/ingest>
|
|
20
|
+
- OpenAPI spec: <https://docs.outlit.ai/openapi.json>
|
|
21
|
+
- Agent-facing docs index: <https://docs.outlit.ai/llms.txt>
|
package/dist/index.d.mts
CHANGED
|
@@ -1,22 +1,8 @@
|
|
|
1
|
-
import {
|
|
2
|
-
export {
|
|
1
|
+
import { ServerTrackOptions, ServerIdentifyOptions } from '@outlit/core';
|
|
2
|
+
export { IngestResponse, ServerIdentifyOptions, ServerTrackOptions, TrackerConfig } from '@outlit/core';
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
* Server-side stage events require at least one identifier (fingerprint, email, or userId).
|
|
7
|
-
*/
|
|
8
|
-
interface StageOptions extends ServerIdentity {
|
|
9
|
-
/**
|
|
10
|
-
* Optional properties for context.
|
|
11
|
-
*/
|
|
12
|
-
properties?: Record<string, string | number | boolean | null>;
|
|
13
|
-
}
|
|
14
|
-
/**
|
|
15
|
-
* Options for billing status events.
|
|
16
|
-
* Public billing calls should use customerId.
|
|
17
|
-
*/
|
|
18
|
-
interface BillingOptions extends CustomerIdentifier {
|
|
19
|
-
properties?: Record<string, string | number | boolean | null>;
|
|
4
|
+
interface UserMethods {
|
|
5
|
+
identify: (options: ServerIdentifyOptions) => void;
|
|
20
6
|
}
|
|
21
7
|
interface OutlitOptions {
|
|
22
8
|
/**
|
|
@@ -135,28 +121,8 @@ declare class Outlit {
|
|
|
135
121
|
* @throws Error if neither email nor userId is provided
|
|
136
122
|
*/
|
|
137
123
|
identify(options: ServerIdentifyOptions): void;
|
|
138
|
-
/**
|
|
139
|
-
|
|
140
|
-
*/
|
|
141
|
-
readonly user: {
|
|
142
|
-
identify: (options: ServerIdentifyOptions) => void;
|
|
143
|
-
activate: (options: StageOptions) => void;
|
|
144
|
-
engaged: (options: StageOptions) => void;
|
|
145
|
-
inactive: (options: StageOptions) => void;
|
|
146
|
-
};
|
|
147
|
-
/**
|
|
148
|
-
* Customer namespace methods for billing status.
|
|
149
|
-
*/
|
|
150
|
-
readonly customer: {
|
|
151
|
-
trialing: (options: BillingOptions) => void;
|
|
152
|
-
paid: (options: BillingOptions) => void;
|
|
153
|
-
churned: (options: BillingOptions) => void;
|
|
154
|
-
};
|
|
155
|
-
/**
|
|
156
|
-
* Internal method to send a stage event.
|
|
157
|
-
*/
|
|
158
|
-
private sendStageEvent;
|
|
159
|
-
private sendBillingEvent;
|
|
124
|
+
/** User namespace method for identity. */
|
|
125
|
+
readonly user: UserMethods;
|
|
160
126
|
/**
|
|
161
127
|
* Flush all pending events immediately.
|
|
162
128
|
*
|
|
@@ -180,4 +146,4 @@ declare class Outlit {
|
|
|
180
146
|
private handleFatalTransportError;
|
|
181
147
|
}
|
|
182
148
|
|
|
183
|
-
export {
|
|
149
|
+
export { Outlit, type OutlitOptions, type UserMethods };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,22 +1,8 @@
|
|
|
1
|
-
import {
|
|
2
|
-
export {
|
|
1
|
+
import { ServerTrackOptions, ServerIdentifyOptions } from '@outlit/core';
|
|
2
|
+
export { IngestResponse, ServerIdentifyOptions, ServerTrackOptions, TrackerConfig } from '@outlit/core';
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
* Server-side stage events require at least one identifier (fingerprint, email, or userId).
|
|
7
|
-
*/
|
|
8
|
-
interface StageOptions extends ServerIdentity {
|
|
9
|
-
/**
|
|
10
|
-
* Optional properties for context.
|
|
11
|
-
*/
|
|
12
|
-
properties?: Record<string, string | number | boolean | null>;
|
|
13
|
-
}
|
|
14
|
-
/**
|
|
15
|
-
* Options for billing status events.
|
|
16
|
-
* Public billing calls should use customerId.
|
|
17
|
-
*/
|
|
18
|
-
interface BillingOptions extends CustomerIdentifier {
|
|
19
|
-
properties?: Record<string, string | number | boolean | null>;
|
|
4
|
+
interface UserMethods {
|
|
5
|
+
identify: (options: ServerIdentifyOptions) => void;
|
|
20
6
|
}
|
|
21
7
|
interface OutlitOptions {
|
|
22
8
|
/**
|
|
@@ -135,28 +121,8 @@ declare class Outlit {
|
|
|
135
121
|
* @throws Error if neither email nor userId is provided
|
|
136
122
|
*/
|
|
137
123
|
identify(options: ServerIdentifyOptions): void;
|
|
138
|
-
/**
|
|
139
|
-
|
|
140
|
-
*/
|
|
141
|
-
readonly user: {
|
|
142
|
-
identify: (options: ServerIdentifyOptions) => void;
|
|
143
|
-
activate: (options: StageOptions) => void;
|
|
144
|
-
engaged: (options: StageOptions) => void;
|
|
145
|
-
inactive: (options: StageOptions) => void;
|
|
146
|
-
};
|
|
147
|
-
/**
|
|
148
|
-
* Customer namespace methods for billing status.
|
|
149
|
-
*/
|
|
150
|
-
readonly customer: {
|
|
151
|
-
trialing: (options: BillingOptions) => void;
|
|
152
|
-
paid: (options: BillingOptions) => void;
|
|
153
|
-
churned: (options: BillingOptions) => void;
|
|
154
|
-
};
|
|
155
|
-
/**
|
|
156
|
-
* Internal method to send a stage event.
|
|
157
|
-
*/
|
|
158
|
-
private sendStageEvent;
|
|
159
|
-
private sendBillingEvent;
|
|
124
|
+
/** User namespace method for identity. */
|
|
125
|
+
readonly user: UserMethods;
|
|
160
126
|
/**
|
|
161
127
|
* Flush all pending events immediately.
|
|
162
128
|
*
|
|
@@ -180,4 +146,4 @@ declare class Outlit {
|
|
|
180
146
|
private handleFatalTransportError;
|
|
181
147
|
}
|
|
182
148
|
|
|
183
|
-
export {
|
|
149
|
+
export { Outlit, type OutlitOptions, type UserMethods };
|
package/dist/index.js
CHANGED
|
@@ -253,53 +253,10 @@ var Outlit = class {
|
|
|
253
253
|
});
|
|
254
254
|
this.queue.enqueue(event);
|
|
255
255
|
}
|
|
256
|
-
/**
|
|
257
|
-
* User namespace methods for contact journey stages.
|
|
258
|
-
*/
|
|
256
|
+
/** User namespace method for identity. */
|
|
259
257
|
user = {
|
|
260
|
-
identify: (options) => this.identify(options)
|
|
261
|
-
activate: (options) => this.sendStageEvent("activated", options),
|
|
262
|
-
engaged: (options) => this.sendStageEvent("engaged", options),
|
|
263
|
-
inactive: (options) => this.sendStageEvent("inactive", options)
|
|
264
|
-
};
|
|
265
|
-
/**
|
|
266
|
-
* Customer namespace methods for billing status.
|
|
267
|
-
*/
|
|
268
|
-
customer = {
|
|
269
|
-
trialing: (options) => this.sendBillingEvent("trialing", options),
|
|
270
|
-
paid: (options) => this.sendBillingEvent("paid", options),
|
|
271
|
-
churned: (options) => this.sendBillingEvent("churned", options)
|
|
258
|
+
identify: (options) => this.identify(options)
|
|
272
259
|
};
|
|
273
|
-
/**
|
|
274
|
-
* Internal method to send a stage event.
|
|
275
|
-
*/
|
|
276
|
-
sendStageEvent(stage, options) {
|
|
277
|
-
this.ensureNotShutdown();
|
|
278
|
-
(0, import_core.validateServerIdentity)(options.fingerprint, options.email, options.userId);
|
|
279
|
-
const event = (0, import_core.buildStageEvent)({
|
|
280
|
-
url: `server://${options.email ?? options.userId ?? options.fingerprint}`,
|
|
281
|
-
stage,
|
|
282
|
-
properties: {
|
|
283
|
-
...options.properties,
|
|
284
|
-
__fingerprint: options.fingerprint ?? null,
|
|
285
|
-
__email: options.email ?? null,
|
|
286
|
-
__userId: options.userId ?? null
|
|
287
|
-
}
|
|
288
|
-
});
|
|
289
|
-
this.queue.enqueue(event);
|
|
290
|
-
}
|
|
291
|
-
sendBillingEvent(status, options) {
|
|
292
|
-
this.ensureNotShutdown();
|
|
293
|
-
(0, import_core.validateCustomerIdentity)(options.customerId, options.stripeCustomerId);
|
|
294
|
-
const event = (0, import_core.buildBillingEvent)({
|
|
295
|
-
url: `server://${options.customerId ?? options.stripeCustomerId}`,
|
|
296
|
-
status,
|
|
297
|
-
customerId: options.customerId,
|
|
298
|
-
stripeCustomerId: options.stripeCustomerId,
|
|
299
|
-
properties: options.properties
|
|
300
|
-
});
|
|
301
|
-
this.queue.enqueue(event);
|
|
302
|
-
}
|
|
303
260
|
/**
|
|
304
261
|
* Flush all pending events immediately.
|
|
305
262
|
*
|
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\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":[]}
|
|
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 IngestResponse,\n ServerIdentifyOptions,\n ServerTrackOptions,\n TrackerConfig,\n} from \"@outlit/core\"\nexport type { OutlitOptions, UserMethods } from \"./client\"\nexport { Outlit } from \"./client\"\n","import {\n buildCustomEvent,\n buildIdentifyEvent,\n DEFAULT_API_HOST,\n type IngestPayload,\n type ServerIdentifyOptions,\n type ServerTrackOptions,\n type TrackerEvent,\n validateServerIdentity,\n} from \"@outlit/core\"\nimport { EventQueue } from \"./queue\"\nimport { HttpTransport, TransportError } from \"./transport\"\n\nexport interface UserMethods {\n identify: (options: ServerIdentifyOptions) => void\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 /** User namespace method for identity. */\n readonly user: UserMethods = {\n identify: (options: ServerIdentifyOptions) => this.identify(options),\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,11 +1,8 @@
|
|
|
1
1
|
// src/client.ts
|
|
2
2
|
import {
|
|
3
|
-
buildBillingEvent,
|
|
4
3
|
buildCustomEvent,
|
|
5
4
|
buildIdentifyEvent,
|
|
6
|
-
buildStageEvent,
|
|
7
5
|
DEFAULT_API_HOST,
|
|
8
|
-
validateCustomerIdentity,
|
|
9
6
|
validateServerIdentity
|
|
10
7
|
} from "@outlit/core";
|
|
11
8
|
|
|
@@ -235,53 +232,10 @@ var Outlit = class {
|
|
|
235
232
|
});
|
|
236
233
|
this.queue.enqueue(event);
|
|
237
234
|
}
|
|
238
|
-
/**
|
|
239
|
-
* User namespace methods for contact journey stages.
|
|
240
|
-
*/
|
|
235
|
+
/** User namespace method for identity. */
|
|
241
236
|
user = {
|
|
242
|
-
identify: (options) => this.identify(options)
|
|
243
|
-
activate: (options) => this.sendStageEvent("activated", options),
|
|
244
|
-
engaged: (options) => this.sendStageEvent("engaged", options),
|
|
245
|
-
inactive: (options) => this.sendStageEvent("inactive", options)
|
|
246
|
-
};
|
|
247
|
-
/**
|
|
248
|
-
* Customer namespace methods for billing status.
|
|
249
|
-
*/
|
|
250
|
-
customer = {
|
|
251
|
-
trialing: (options) => this.sendBillingEvent("trialing", options),
|
|
252
|
-
paid: (options) => this.sendBillingEvent("paid", options),
|
|
253
|
-
churned: (options) => this.sendBillingEvent("churned", options)
|
|
237
|
+
identify: (options) => this.identify(options)
|
|
254
238
|
};
|
|
255
|
-
/**
|
|
256
|
-
* Internal method to send a stage event.
|
|
257
|
-
*/
|
|
258
|
-
sendStageEvent(stage, options) {
|
|
259
|
-
this.ensureNotShutdown();
|
|
260
|
-
validateServerIdentity(options.fingerprint, options.email, options.userId);
|
|
261
|
-
const event = buildStageEvent({
|
|
262
|
-
url: `server://${options.email ?? options.userId ?? options.fingerprint}`,
|
|
263
|
-
stage,
|
|
264
|
-
properties: {
|
|
265
|
-
...options.properties,
|
|
266
|
-
__fingerprint: options.fingerprint ?? null,
|
|
267
|
-
__email: options.email ?? null,
|
|
268
|
-
__userId: options.userId ?? null
|
|
269
|
-
}
|
|
270
|
-
});
|
|
271
|
-
this.queue.enqueue(event);
|
|
272
|
-
}
|
|
273
|
-
sendBillingEvent(status, options) {
|
|
274
|
-
this.ensureNotShutdown();
|
|
275
|
-
validateCustomerIdentity(options.customerId, options.stripeCustomerId);
|
|
276
|
-
const event = buildBillingEvent({
|
|
277
|
-
url: `server://${options.customerId ?? options.stripeCustomerId}`,
|
|
278
|
-
status,
|
|
279
|
-
customerId: options.customerId,
|
|
280
|
-
stripeCustomerId: options.stripeCustomerId,
|
|
281
|
-
properties: options.properties
|
|
282
|
-
});
|
|
283
|
-
this.queue.enqueue(event);
|
|
284
|
-
}
|
|
285
239
|
/**
|
|
286
240
|
* Flush all pending events immediately.
|
|
287
241
|
*
|
package/dist/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
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":[]}
|
|
1
|
+
{"version":3,"sources":["../src/client.ts","../src/queue.ts","../src/transport.ts"],"sourcesContent":["import {\n buildCustomEvent,\n buildIdentifyEvent,\n DEFAULT_API_HOST,\n type IngestPayload,\n type ServerIdentifyOptions,\n type ServerTrackOptions,\n type TrackerEvent,\n validateServerIdentity,\n} from \"@outlit/core\"\nimport { EventQueue } from \"./queue\"\nimport { HttpTransport, TransportError } from \"./transport\"\n\nexport interface UserMethods {\n identify: (options: ServerIdentifyOptions) => void\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 /** User namespace method for identity. */\n readonly user: UserMethods = {\n identify: (options: ServerIdentifyOptions) => this.identify(options),\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,EACE;AAAA,EACA;AAAA,EACA;AAAA,EAKA;AAAA,OACK;;;ACHP,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;;;AFVO,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;AAAA;AAAA;AAAA;AAAA,EAiBA,MAAM,SAAmC;AACvC,SAAK,kBAAkB;AACvB,2BAAuB,QAAQ,aAAa,QAAQ,OAAO,QAAQ,QAAQ,QAAQ,UAAU;AAE7F,UAAM,QAAQ,iBAAiB;AAAA,MAC7B,KAAK,YACH,QAAQ,SAAS,QAAQ,UAAU,QAAQ,cAAc,QAAQ,WACnE;AAAA,MACA,WAAW,QAAQ;AAAA,MACnB,WAAW,QAAQ;AAAA,MACnB,OAAO,QAAQ;AAAA,MACf,QAAQ,QAAQ;AAAA,MAChB,aAAa,QAAQ;AAAA,MACrB,YAAY,QAAQ;AAAA,MACpB,YAAY,QAAQ;AAAA,IACtB,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;AAAA;AAAA;AAAA,EAsBA,SAAS,SAAsC;AAC7C,SAAK,kBAAkB;AAGvB,QAAI,CAAC,QAAQ,SAAS,CAAC,QAAQ,QAAQ;AACrC,YAAM,IAAI;AAAA,QACR;AAAA,MAEF;AAAA,IACF;AAEA,UAAM,QAAQ,mBAAmB;AAAA,MAC/B,KAAK,YAAY,QAAQ,SAAS,QAAQ,MAAM;AAAA,MAChD,OAAO,QAAQ;AAAA,MACf,QAAQ,QAAQ;AAAA,MAChB,aAAa,QAAQ;AAAA,MACrB,QAAQ,QAAQ;AAAA,MAChB,YAAY,QAAQ;AAAA,MACpB,gBAAgB,QAAQ;AAAA,IAC1B,CAAC;AAED,SAAK,MAAM,QAAQ,KAAK;AAAA,EAC1B;AAAA;AAAA,EAGS,OAAoB;AAAA,IAC3B,UAAU,CAAC,YAAmC,KAAK,SAAS,OAAO;AAAA,EACrE;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":[]}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@outlit/node",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "2.0.0",
|
|
4
|
+
"description": "Node.js tracking SDK for Outlit customer context",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "Outlit AI",
|
|
7
7
|
"repository": {
|
|
@@ -12,11 +12,13 @@
|
|
|
12
12
|
"bugs": {
|
|
13
13
|
"url": "https://github.com/OutlitAI/outlit-sdk/issues"
|
|
14
14
|
},
|
|
15
|
-
"homepage": "https://
|
|
15
|
+
"homepage": "https://docs.outlit.ai/tracking/server/nodejs",
|
|
16
16
|
"keywords": [
|
|
17
17
|
"outlit",
|
|
18
18
|
"analytics",
|
|
19
19
|
"tracking",
|
|
20
|
+
"event-tracking",
|
|
21
|
+
"customer-context",
|
|
20
22
|
"sdk",
|
|
21
23
|
"node",
|
|
22
24
|
"server"
|
|
@@ -51,7 +53,7 @@
|
|
|
51
53
|
"test": "vitest run"
|
|
52
54
|
},
|
|
53
55
|
"dependencies": {
|
|
54
|
-
"@outlit/core": "^
|
|
56
|
+
"@outlit/core": "^2.0.0"
|
|
55
57
|
},
|
|
56
58
|
"devDependencies": {
|
|
57
59
|
"@outlit/typescript-config": "workspace:*",
|