@outlit/node 1.0.0 → 1.1.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 +35 -13
- package/dist/index.d.ts +35 -13
- package/dist/index.js +32 -15
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +32 -15
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
package/dist/index.d.mts
CHANGED
|
@@ -3,21 +3,21 @@ export { ExplicitJourneyStage, IngestResponse, ServerIdentifyOptions, ServerTrac
|
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Options for stage transition events (activate, engaged, inactive).
|
|
6
|
-
* Server-side stage events require
|
|
6
|
+
* Server-side stage events require at least one identifier (fingerprint, email, or userId).
|
|
7
7
|
*/
|
|
8
|
-
|
|
8
|
+
interface StageOptions extends ServerIdentity {
|
|
9
9
|
/**
|
|
10
10
|
* Optional properties for context.
|
|
11
11
|
*/
|
|
12
12
|
properties?: Record<string, string | number | boolean | null>;
|
|
13
|
-
}
|
|
13
|
+
}
|
|
14
14
|
/**
|
|
15
15
|
* Options for billing status events.
|
|
16
16
|
* Requires at least one customer identifier (customerId, stripeCustomerId, or domain).
|
|
17
17
|
*/
|
|
18
|
-
|
|
18
|
+
interface BillingOptions extends CustomerIdentifier {
|
|
19
19
|
properties?: Record<string, string | number | boolean | null>;
|
|
20
|
-
}
|
|
20
|
+
}
|
|
21
21
|
interface OutlitOptions {
|
|
22
22
|
/**
|
|
23
23
|
* Your Outlit public key.
|
|
@@ -47,8 +47,8 @@ interface OutlitOptions {
|
|
|
47
47
|
/**
|
|
48
48
|
* Outlit server-side tracking client.
|
|
49
49
|
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
50
|
+
* Supports tracking with fingerprint (device ID), email, or userId.
|
|
51
|
+
* Use fingerprint for anonymous tracking that can be linked to users later.
|
|
52
52
|
*
|
|
53
53
|
* @example
|
|
54
54
|
* ```typescript
|
|
@@ -56,16 +56,24 @@ interface OutlitOptions {
|
|
|
56
56
|
*
|
|
57
57
|
* const outlit = new Outlit({ publicKey: 'pk_xxx' })
|
|
58
58
|
*
|
|
59
|
-
* // Track
|
|
59
|
+
* // Track with fingerprint only (anonymous, stored for later backfill)
|
|
60
|
+
* outlit.track({
|
|
61
|
+
* fingerprint: deviceId,
|
|
62
|
+
* eventName: 'page_view',
|
|
63
|
+
* properties: { page: '/pricing' }
|
|
64
|
+
* })
|
|
65
|
+
*
|
|
66
|
+
* // Track with email (resolves immediately)
|
|
60
67
|
* outlit.track({
|
|
61
68
|
* email: 'user@example.com',
|
|
62
69
|
* eventName: 'subscription_created',
|
|
63
70
|
* properties: { plan: 'pro' }
|
|
64
71
|
* })
|
|
65
72
|
*
|
|
66
|
-
* // Identify
|
|
73
|
+
* // Identify user and link fingerprint to email
|
|
67
74
|
* outlit.identify({
|
|
68
75
|
* email: 'user@example.com',
|
|
76
|
+
* fingerprint: deviceId, // Links this device to the user
|
|
69
77
|
* userId: 'usr_123',
|
|
70
78
|
* traits: { name: 'John Doe' }
|
|
71
79
|
* })
|
|
@@ -84,17 +92,31 @@ declare class Outlit {
|
|
|
84
92
|
/**
|
|
85
93
|
* Track a custom event.
|
|
86
94
|
*
|
|
87
|
-
* Requires
|
|
95
|
+
* Requires at least one of: `fingerprint`, `email`, or `userId`.
|
|
96
|
+
*
|
|
97
|
+
* - Use `fingerprint` for anonymous tracking (events linked later via identify)
|
|
98
|
+
* - Use `email` for immediate user resolution
|
|
99
|
+
* - Use `userId` for app-specific user identification
|
|
88
100
|
*
|
|
89
|
-
* @throws Error if
|
|
101
|
+
* @throws Error if no identity is provided
|
|
90
102
|
*/
|
|
91
103
|
track(options: ServerTrackOptions): void;
|
|
92
104
|
/**
|
|
93
105
|
* Identify or update a user.
|
|
94
106
|
*
|
|
95
|
-
* Requires
|
|
107
|
+
* Requires `email` to establish definitive identity.
|
|
108
|
+
* Optionally include `fingerprint` and/or `userId` to link them to this email.
|
|
109
|
+
*
|
|
110
|
+
* This is how you link anonymous fingerprint-tracked events to a real user:
|
|
111
|
+
* ```typescript
|
|
112
|
+
* outlit.identify({
|
|
113
|
+
* email: 'user@example.com',
|
|
114
|
+
* fingerprint: deviceId, // Links this device to the user
|
|
115
|
+
* userId: 'usr_123', // Links this app user ID to the user
|
|
116
|
+
* });
|
|
117
|
+
* ```
|
|
96
118
|
*
|
|
97
|
-
* @throws Error if
|
|
119
|
+
* @throws Error if email is not provided
|
|
98
120
|
*/
|
|
99
121
|
identify(options: ServerIdentifyOptions): void;
|
|
100
122
|
/**
|
package/dist/index.d.ts
CHANGED
|
@@ -3,21 +3,21 @@ export { ExplicitJourneyStage, IngestResponse, ServerIdentifyOptions, ServerTrac
|
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Options for stage transition events (activate, engaged, inactive).
|
|
6
|
-
* Server-side stage events require
|
|
6
|
+
* Server-side stage events require at least one identifier (fingerprint, email, or userId).
|
|
7
7
|
*/
|
|
8
|
-
|
|
8
|
+
interface StageOptions extends ServerIdentity {
|
|
9
9
|
/**
|
|
10
10
|
* Optional properties for context.
|
|
11
11
|
*/
|
|
12
12
|
properties?: Record<string, string | number | boolean | null>;
|
|
13
|
-
}
|
|
13
|
+
}
|
|
14
14
|
/**
|
|
15
15
|
* Options for billing status events.
|
|
16
16
|
* Requires at least one customer identifier (customerId, stripeCustomerId, or domain).
|
|
17
17
|
*/
|
|
18
|
-
|
|
18
|
+
interface BillingOptions extends CustomerIdentifier {
|
|
19
19
|
properties?: Record<string, string | number | boolean | null>;
|
|
20
|
-
}
|
|
20
|
+
}
|
|
21
21
|
interface OutlitOptions {
|
|
22
22
|
/**
|
|
23
23
|
* Your Outlit public key.
|
|
@@ -47,8 +47,8 @@ interface OutlitOptions {
|
|
|
47
47
|
/**
|
|
48
48
|
* Outlit server-side tracking client.
|
|
49
49
|
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
50
|
+
* Supports tracking with fingerprint (device ID), email, or userId.
|
|
51
|
+
* Use fingerprint for anonymous tracking that can be linked to users later.
|
|
52
52
|
*
|
|
53
53
|
* @example
|
|
54
54
|
* ```typescript
|
|
@@ -56,16 +56,24 @@ interface OutlitOptions {
|
|
|
56
56
|
*
|
|
57
57
|
* const outlit = new Outlit({ publicKey: 'pk_xxx' })
|
|
58
58
|
*
|
|
59
|
-
* // Track
|
|
59
|
+
* // Track with fingerprint only (anonymous, stored for later backfill)
|
|
60
|
+
* outlit.track({
|
|
61
|
+
* fingerprint: deviceId,
|
|
62
|
+
* eventName: 'page_view',
|
|
63
|
+
* properties: { page: '/pricing' }
|
|
64
|
+
* })
|
|
65
|
+
*
|
|
66
|
+
* // Track with email (resolves immediately)
|
|
60
67
|
* outlit.track({
|
|
61
68
|
* email: 'user@example.com',
|
|
62
69
|
* eventName: 'subscription_created',
|
|
63
70
|
* properties: { plan: 'pro' }
|
|
64
71
|
* })
|
|
65
72
|
*
|
|
66
|
-
* // Identify
|
|
73
|
+
* // Identify user and link fingerprint to email
|
|
67
74
|
* outlit.identify({
|
|
68
75
|
* email: 'user@example.com',
|
|
76
|
+
* fingerprint: deviceId, // Links this device to the user
|
|
69
77
|
* userId: 'usr_123',
|
|
70
78
|
* traits: { name: 'John Doe' }
|
|
71
79
|
* })
|
|
@@ -84,17 +92,31 @@ declare class Outlit {
|
|
|
84
92
|
/**
|
|
85
93
|
* Track a custom event.
|
|
86
94
|
*
|
|
87
|
-
* Requires
|
|
95
|
+
* Requires at least one of: `fingerprint`, `email`, or `userId`.
|
|
96
|
+
*
|
|
97
|
+
* - Use `fingerprint` for anonymous tracking (events linked later via identify)
|
|
98
|
+
* - Use `email` for immediate user resolution
|
|
99
|
+
* - Use `userId` for app-specific user identification
|
|
88
100
|
*
|
|
89
|
-
* @throws Error if
|
|
101
|
+
* @throws Error if no identity is provided
|
|
90
102
|
*/
|
|
91
103
|
track(options: ServerTrackOptions): void;
|
|
92
104
|
/**
|
|
93
105
|
* Identify or update a user.
|
|
94
106
|
*
|
|
95
|
-
* Requires
|
|
107
|
+
* Requires `email` to establish definitive identity.
|
|
108
|
+
* Optionally include `fingerprint` and/or `userId` to link them to this email.
|
|
109
|
+
*
|
|
110
|
+
* This is how you link anonymous fingerprint-tracked events to a real user:
|
|
111
|
+
* ```typescript
|
|
112
|
+
* outlit.identify({
|
|
113
|
+
* email: 'user@example.com',
|
|
114
|
+
* fingerprint: deviceId, // Links this device to the user
|
|
115
|
+
* userId: 'usr_123', // Links this app user ID to the user
|
|
116
|
+
* });
|
|
117
|
+
* ```
|
|
96
118
|
*
|
|
97
|
-
* @throws Error if
|
|
119
|
+
* @throws Error if email is not provided
|
|
98
120
|
*/
|
|
99
121
|
identify(options: ServerIdentifyOptions): void;
|
|
100
122
|
/**
|
package/dist/index.js
CHANGED
|
@@ -146,20 +146,25 @@ var Outlit = class {
|
|
|
146
146
|
/**
|
|
147
147
|
* Track a custom event.
|
|
148
148
|
*
|
|
149
|
-
* Requires
|
|
149
|
+
* Requires at least one of: `fingerprint`, `email`, or `userId`.
|
|
150
150
|
*
|
|
151
|
-
*
|
|
151
|
+
* - Use `fingerprint` for anonymous tracking (events linked later via identify)
|
|
152
|
+
* - Use `email` for immediate user resolution
|
|
153
|
+
* - Use `userId` for app-specific user identification
|
|
154
|
+
*
|
|
155
|
+
* @throws Error if no identity is provided
|
|
152
156
|
*/
|
|
153
157
|
track(options) {
|
|
154
158
|
this.ensureNotShutdown();
|
|
155
|
-
(0, import_core.validateServerIdentity)(options.email, options.userId);
|
|
159
|
+
(0, import_core.validateServerIdentity)(options.fingerprint, options.email, options.userId);
|
|
156
160
|
const event = (0, import_core.buildCustomEvent)({
|
|
157
|
-
url: `server://${options.email ?? options.userId}`,
|
|
161
|
+
url: `server://${options.email ?? options.userId ?? options.fingerprint}`,
|
|
158
162
|
timestamp: options.timestamp,
|
|
159
163
|
eventName: options.eventName,
|
|
160
164
|
properties: {
|
|
161
165
|
...options.properties,
|
|
162
166
|
// Include identity in properties for server-side resolution
|
|
167
|
+
__fingerprint: options.fingerprint ?? null,
|
|
163
168
|
__email: options.email ?? null,
|
|
164
169
|
__userId: options.userId ?? null
|
|
165
170
|
}
|
|
@@ -169,17 +174,32 @@ var Outlit = class {
|
|
|
169
174
|
/**
|
|
170
175
|
* Identify or update a user.
|
|
171
176
|
*
|
|
172
|
-
* Requires
|
|
177
|
+
* Requires `email` to establish definitive identity.
|
|
178
|
+
* Optionally include `fingerprint` and/or `userId` to link them to this email.
|
|
179
|
+
*
|
|
180
|
+
* This is how you link anonymous fingerprint-tracked events to a real user:
|
|
181
|
+
* ```typescript
|
|
182
|
+
* outlit.identify({
|
|
183
|
+
* email: 'user@example.com',
|
|
184
|
+
* fingerprint: deviceId, // Links this device to the user
|
|
185
|
+
* userId: 'usr_123', // Links this app user ID to the user
|
|
186
|
+
* });
|
|
187
|
+
* ```
|
|
173
188
|
*
|
|
174
|
-
* @throws Error if
|
|
189
|
+
* @throws Error if email is not provided
|
|
175
190
|
*/
|
|
176
191
|
identify(options) {
|
|
177
192
|
this.ensureNotShutdown();
|
|
178
|
-
|
|
193
|
+
if (!options.email) {
|
|
194
|
+
throw new Error(
|
|
195
|
+
"identify() requires email to establish definitive identity. Use fingerprint and/or userId as optional fields to link them to the email."
|
|
196
|
+
);
|
|
197
|
+
}
|
|
179
198
|
const event = (0, import_core.buildIdentifyEvent)({
|
|
180
|
-
url: `server://${options.email
|
|
199
|
+
url: `server://${options.email}`,
|
|
181
200
|
email: options.email,
|
|
182
201
|
userId: options.userId,
|
|
202
|
+
fingerprint: options.fingerprint,
|
|
183
203
|
traits: options.traits
|
|
184
204
|
});
|
|
185
205
|
this.queue.enqueue(event);
|
|
@@ -206,13 +226,14 @@ var Outlit = class {
|
|
|
206
226
|
*/
|
|
207
227
|
sendStageEvent(stage, options) {
|
|
208
228
|
this.ensureNotShutdown();
|
|
209
|
-
(0, import_core.validateServerIdentity)(options.email, options.userId);
|
|
229
|
+
(0, import_core.validateServerIdentity)(options.fingerprint, options.email, options.userId);
|
|
210
230
|
const event = (0, import_core.buildStageEvent)({
|
|
211
|
-
url: `server://${options.email ?? options.userId}`,
|
|
231
|
+
url: `server://${options.email ?? options.userId ?? options.fingerprint}`,
|
|
212
232
|
stage,
|
|
213
233
|
properties: {
|
|
214
234
|
...options.properties,
|
|
215
235
|
// Include identity in properties for server-side resolution
|
|
236
|
+
__fingerprint: options.fingerprint ?? null,
|
|
216
237
|
__email: options.email ?? null,
|
|
217
238
|
__userId: options.userId ?? null
|
|
218
239
|
}
|
|
@@ -221,12 +242,8 @@ var Outlit = class {
|
|
|
221
242
|
}
|
|
222
243
|
sendBillingEvent(status, options) {
|
|
223
244
|
this.ensureNotShutdown();
|
|
224
|
-
if (!options.customerId && !options.stripeCustomerId && !options.domain) {
|
|
225
|
-
throw new Error("[Outlit] customer.* requires customerId, stripeCustomerId, or domain");
|
|
226
|
-
}
|
|
227
|
-
const identityLabel = options.customerId ?? options.stripeCustomerId ?? options.domain ?? "unknown";
|
|
228
245
|
const event = (0, import_core.buildBillingEvent)({
|
|
229
|
-
url: `server://${
|
|
246
|
+
url: `server://${options.domain}`,
|
|
230
247
|
status,
|
|
231
248
|
customerId: options.customerId,
|
|
232
249
|
stripeCustomerId: options.stripeCustomerId,
|
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 } 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 user identification (email or userId).\n */\nexport type StageOptions = 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 type BillingOptions = 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 * Unlike the browser SDK, this requires identity (email or userId) for all calls.\n * Anonymous tracking is not supported server-side.\n *\n * @example\n * ```typescript\n * import { Outlit } from '@outlit/node'\n *\n * const outlit = new Outlit({ publicKey: 'pk_xxx' })\n *\n * // Track a custom event\n * outlit.track({\n * email: 'user@example.com',\n * eventName: 'subscription_created',\n * properties: { plan: 'pro' }\n * })\n *\n * // Identify/update user\n * outlit.identify({\n * email: 'user@example.com',\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\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 either `email` or `userId` to identify the user.\n *\n * @throws Error if neither email nor userId is provided\n */\n track(options: ServerTrackOptions): void {\n this.ensureNotShutdown()\n validateServerIdentity(options.email, options.userId)\n\n const event = buildCustomEvent({\n url: `server://${options.email ?? options.userId}`,\n timestamp: options.timestamp,\n eventName: options.eventName,\n properties: {\n ...options.properties,\n // Include identity in properties for server-side resolution\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 either `email` or `userId` to identify the user.\n *\n * @throws Error if neither email nor userId is provided\n */\n identify(options: ServerIdentifyOptions): void {\n this.ensureNotShutdown()\n validateServerIdentity(options.email, options.userId)\n\n const event = buildIdentifyEvent({\n url: `server://${options.email ?? options.userId}`,\n email: options.email,\n userId: options.userId,\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.email, options.userId)\n\n const event = buildStageEvent({\n url: `server://${options.email ?? options.userId}`,\n stage,\n properties: {\n ...options.properties,\n // Include identity in properties for server-side resolution\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 if (!options.customerId && !options.stripeCustomerId && !options.domain) {\n throw new Error(\"[Outlit] customer.* requires customerId, stripeCustomerId, or domain\")\n }\n\n const identityLabel =\n options.customerId ?? options.stripeCustomerId ?? options.domain ?? \"unknown\"\n\n const event = buildBillingEvent({\n url: `server://${identityLabel}`,\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 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\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 await this.transport.send(payload)\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","import type { TrackerEvent } from \"@outlit/core\"\n\n// ============================================\n// EVENT QUEUE\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 to queue on failure\n this.queue = [...events, ...this.queue]\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\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 Error(`HTTP ${response.status}: ${errorBody}`)\n }\n\n return (await response.json()) as IngestResponse\n } catch (error) {\n if (error instanceof Error && error.name === \"AbortError\") {\n throw new Error(`Request timed out after ${this.timeout}ms`)\n }\n throw error\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\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 } 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\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 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\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 await this.transport.send(payload)\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","import type { TrackerEvent } from \"@outlit/core\"\n\n// ============================================\n// EVENT QUEUE\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 to queue on failure\n this.queue = [...events, ...this.queue]\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\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 Error(`HTTP ${response.status}: ${errorBody}`)\n }\n\n return (await response.json()) as IngestResponse\n } catch (error) {\n if (error instanceof Error && error.name === \"AbortError\") {\n throw new Error(`Request timed out after ${this.timeout}ms`)\n }\n throw error\n } finally {\n clearTimeout(timeoutId)\n }\n }\n}\n"],"mappings":"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","names":[]}
|
package/dist/index.mjs
CHANGED
|
@@ -127,20 +127,25 @@ var Outlit = class {
|
|
|
127
127
|
/**
|
|
128
128
|
* Track a custom event.
|
|
129
129
|
*
|
|
130
|
-
* Requires
|
|
130
|
+
* Requires at least one of: `fingerprint`, `email`, or `userId`.
|
|
131
131
|
*
|
|
132
|
-
*
|
|
132
|
+
* - Use `fingerprint` for anonymous tracking (events linked later via identify)
|
|
133
|
+
* - Use `email` for immediate user resolution
|
|
134
|
+
* - Use `userId` for app-specific user identification
|
|
135
|
+
*
|
|
136
|
+
* @throws Error if no identity is provided
|
|
133
137
|
*/
|
|
134
138
|
track(options) {
|
|
135
139
|
this.ensureNotShutdown();
|
|
136
|
-
validateServerIdentity(options.email, options.userId);
|
|
140
|
+
validateServerIdentity(options.fingerprint, options.email, options.userId);
|
|
137
141
|
const event = buildCustomEvent({
|
|
138
|
-
url: `server://${options.email ?? options.userId}`,
|
|
142
|
+
url: `server://${options.email ?? options.userId ?? options.fingerprint}`,
|
|
139
143
|
timestamp: options.timestamp,
|
|
140
144
|
eventName: options.eventName,
|
|
141
145
|
properties: {
|
|
142
146
|
...options.properties,
|
|
143
147
|
// Include identity in properties for server-side resolution
|
|
148
|
+
__fingerprint: options.fingerprint ?? null,
|
|
144
149
|
__email: options.email ?? null,
|
|
145
150
|
__userId: options.userId ?? null
|
|
146
151
|
}
|
|
@@ -150,17 +155,32 @@ var Outlit = class {
|
|
|
150
155
|
/**
|
|
151
156
|
* Identify or update a user.
|
|
152
157
|
*
|
|
153
|
-
* Requires
|
|
158
|
+
* Requires `email` to establish definitive identity.
|
|
159
|
+
* Optionally include `fingerprint` and/or `userId` to link them to this email.
|
|
160
|
+
*
|
|
161
|
+
* This is how you link anonymous fingerprint-tracked events to a real user:
|
|
162
|
+
* ```typescript
|
|
163
|
+
* outlit.identify({
|
|
164
|
+
* email: 'user@example.com',
|
|
165
|
+
* fingerprint: deviceId, // Links this device to the user
|
|
166
|
+
* userId: 'usr_123', // Links this app user ID to the user
|
|
167
|
+
* });
|
|
168
|
+
* ```
|
|
154
169
|
*
|
|
155
|
-
* @throws Error if
|
|
170
|
+
* @throws Error if email is not provided
|
|
156
171
|
*/
|
|
157
172
|
identify(options) {
|
|
158
173
|
this.ensureNotShutdown();
|
|
159
|
-
|
|
174
|
+
if (!options.email) {
|
|
175
|
+
throw new Error(
|
|
176
|
+
"identify() requires email to establish definitive identity. Use fingerprint and/or userId as optional fields to link them to the email."
|
|
177
|
+
);
|
|
178
|
+
}
|
|
160
179
|
const event = buildIdentifyEvent({
|
|
161
|
-
url: `server://${options.email
|
|
180
|
+
url: `server://${options.email}`,
|
|
162
181
|
email: options.email,
|
|
163
182
|
userId: options.userId,
|
|
183
|
+
fingerprint: options.fingerprint,
|
|
164
184
|
traits: options.traits
|
|
165
185
|
});
|
|
166
186
|
this.queue.enqueue(event);
|
|
@@ -187,13 +207,14 @@ var Outlit = class {
|
|
|
187
207
|
*/
|
|
188
208
|
sendStageEvent(stage, options) {
|
|
189
209
|
this.ensureNotShutdown();
|
|
190
|
-
validateServerIdentity(options.email, options.userId);
|
|
210
|
+
validateServerIdentity(options.fingerprint, options.email, options.userId);
|
|
191
211
|
const event = buildStageEvent({
|
|
192
|
-
url: `server://${options.email ?? options.userId}`,
|
|
212
|
+
url: `server://${options.email ?? options.userId ?? options.fingerprint}`,
|
|
193
213
|
stage,
|
|
194
214
|
properties: {
|
|
195
215
|
...options.properties,
|
|
196
216
|
// Include identity in properties for server-side resolution
|
|
217
|
+
__fingerprint: options.fingerprint ?? null,
|
|
197
218
|
__email: options.email ?? null,
|
|
198
219
|
__userId: options.userId ?? null
|
|
199
220
|
}
|
|
@@ -202,12 +223,8 @@ var Outlit = class {
|
|
|
202
223
|
}
|
|
203
224
|
sendBillingEvent(status, options) {
|
|
204
225
|
this.ensureNotShutdown();
|
|
205
|
-
if (!options.customerId && !options.stripeCustomerId && !options.domain) {
|
|
206
|
-
throw new Error("[Outlit] customer.* requires customerId, stripeCustomerId, or domain");
|
|
207
|
-
}
|
|
208
|
-
const identityLabel = options.customerId ?? options.stripeCustomerId ?? options.domain ?? "unknown";
|
|
209
226
|
const event = buildBillingEvent({
|
|
210
|
-
url: `server://${
|
|
227
|
+
url: `server://${options.domain}`,
|
|
211
228
|
status,
|
|
212
229
|
customerId: options.customerId,
|
|
213
230
|
stripeCustomerId: options.stripeCustomerId,
|
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 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 } 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 user identification (email or userId).\n */\nexport type StageOptions = 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 type BillingOptions = 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 * Unlike the browser SDK, this requires identity (email or userId) for all calls.\n * Anonymous tracking is not supported server-side.\n *\n * @example\n * ```typescript\n * import { Outlit } from '@outlit/node'\n *\n * const outlit = new Outlit({ publicKey: 'pk_xxx' })\n *\n * // Track a custom event\n * outlit.track({\n * email: 'user@example.com',\n * eventName: 'subscription_created',\n * properties: { plan: 'pro' }\n * })\n *\n * // Identify/update user\n * outlit.identify({\n * email: 'user@example.com',\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\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 either `email` or `userId` to identify the user.\n *\n * @throws Error if neither email nor userId is provided\n */\n track(options: ServerTrackOptions): void {\n this.ensureNotShutdown()\n validateServerIdentity(options.email, options.userId)\n\n const event = buildCustomEvent({\n url: `server://${options.email ?? options.userId}`,\n timestamp: options.timestamp,\n eventName: options.eventName,\n properties: {\n ...options.properties,\n // Include identity in properties for server-side resolution\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 either `email` or `userId` to identify the user.\n *\n * @throws Error if neither email nor userId is provided\n */\n identify(options: ServerIdentifyOptions): void {\n this.ensureNotShutdown()\n validateServerIdentity(options.email, options.userId)\n\n const event = buildIdentifyEvent({\n url: `server://${options.email ?? options.userId}`,\n email: options.email,\n userId: options.userId,\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.email, options.userId)\n\n const event = buildStageEvent({\n url: `server://${options.email ?? options.userId}`,\n stage,\n properties: {\n ...options.properties,\n // Include identity in properties for server-side resolution\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 if (!options.customerId && !options.stripeCustomerId && !options.domain) {\n throw new Error(\"[Outlit] customer.* requires customerId, stripeCustomerId, or domain\")\n }\n\n const identityLabel =\n options.customerId ?? options.stripeCustomerId ?? options.domain ?? \"unknown\"\n\n const event = buildBillingEvent({\n url: `server://${identityLabel}`,\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 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\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 await this.transport.send(payload)\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","import type { TrackerEvent } from \"@outlit/core\"\n\n// ============================================\n// EVENT QUEUE\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 to queue on failure\n this.queue = [...events, ...this.queue]\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\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 Error(`HTTP ${response.status}: ${errorBody}`)\n }\n\n return (await response.json()) as IngestResponse\n } catch (error) {\n if (error instanceof Error && error.name === \"AbortError\") {\n throw new Error(`Request timed out after ${this.timeout}ms`)\n }\n throw error\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 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 } 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\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 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\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 await this.transport.send(payload)\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","import type { TrackerEvent } from \"@outlit/core\"\n\n// ============================================\n// EVENT QUEUE\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 to queue on failure\n this.queue = [...events, ...this.queue]\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\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 Error(`HTTP ${response.status}: ${errorBody}`)\n }\n\n return (await response.json()) as IngestResponse\n } catch (error) {\n if (error instanceof Error && error.name === \"AbortError\") {\n throw new Error(`Request timed out after ${this.timeout}ms`)\n }\n throw error\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.
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "Outlit server-side tracking SDK for Node.js",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "Outlit AI",
|
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
"node": ">=18.0.0"
|
|
43
43
|
},
|
|
44
44
|
"dependencies": {
|
|
45
|
-
"@outlit/core": "1.
|
|
45
|
+
"@outlit/core": "1.1.0"
|
|
46
46
|
},
|
|
47
47
|
"devDependencies": {
|
|
48
48
|
"tsup": "^8.0.1",
|