@outlit/node 0.0.0-canary-202512180440-754f4ab-20251218044038 → 0.0.0-canary-202601021633-e09ca38-20260102163321

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.mts CHANGED
@@ -1,6 +1,20 @@
1
1
  import { ServerTrackOptions, ServerIdentifyOptions } from '@outlit/core';
2
- export { IngestResponse, ServerIdentifyOptions, ServerTrackOptions, TrackerConfig } from '@outlit/core';
2
+ export { ExplicitJourneyStage, IngestResponse, ServerIdentifyOptions, ServerTrackOptions, TrackerConfig } from '@outlit/core';
3
3
 
4
+ interface StageOptions {
5
+ /**
6
+ * User's email address. Required if userId is not provided.
7
+ */
8
+ email?: string;
9
+ /**
10
+ * User's unique ID. Required if email is not provided.
11
+ */
12
+ userId?: string;
13
+ /**
14
+ * Optional properties for context.
15
+ */
16
+ properties?: Record<string, string | number | boolean | null>;
17
+ }
4
18
  interface OutlitOptions {
5
19
  /**
6
20
  * Your Outlit public key.
@@ -80,6 +94,63 @@ declare class Outlit {
80
94
  * @throws Error if neither email nor userId is provided
81
95
  */
82
96
  identify(options: ServerIdentifyOptions): void;
97
+ /**
98
+ * Mark a user as activated.
99
+ *
100
+ * Typically called after a user completes onboarding or a key activation milestone.
101
+ * Requires either `email` or `userId` to identify the user.
102
+ *
103
+ * @throws Error if neither email nor userId is provided
104
+ *
105
+ * @example
106
+ * ```typescript
107
+ * outlit.activate({
108
+ * email: 'user@example.com',
109
+ * properties: { flow: 'onboarding' }
110
+ * })
111
+ * ```
112
+ */
113
+ activate(options: StageOptions): void;
114
+ /**
115
+ * Mark a user as engaged.
116
+ *
117
+ * Typically called when a user reaches a usage milestone.
118
+ * Can also be computed automatically by the engagement cron.
119
+ * Requires either `email` or `userId` to identify the user.
120
+ *
121
+ * @throws Error if neither email nor userId is provided
122
+ *
123
+ * @example
124
+ * ```typescript
125
+ * outlit.engaged({
126
+ * userId: 'usr_123',
127
+ * properties: { milestone: 'first_project_created' }
128
+ * })
129
+ * ```
130
+ */
131
+ engaged(options: StageOptions): void;
132
+ /**
133
+ * Mark a user as paid.
134
+ *
135
+ * Typically called after a successful payment or subscription.
136
+ * Can also be triggered by Stripe integration.
137
+ * Requires either `email` or `userId` to identify the user.
138
+ *
139
+ * @throws Error if neither email nor userId is provided
140
+ *
141
+ * @example
142
+ * ```typescript
143
+ * outlit.paid({
144
+ * email: 'user@example.com',
145
+ * properties: { plan: 'pro', amount: 99 }
146
+ * })
147
+ * ```
148
+ */
149
+ paid(options: StageOptions): void;
150
+ /**
151
+ * Internal method to send a stage event.
152
+ */
153
+ private sendStageEvent;
83
154
  /**
84
155
  * Flush all pending events immediately.
85
156
  *
@@ -101,4 +172,4 @@ declare class Outlit {
101
172
  private ensureNotShutdown;
102
173
  }
103
174
 
104
- export { Outlit, type OutlitOptions };
175
+ export { Outlit, type OutlitOptions, type StageOptions };
package/dist/index.d.ts CHANGED
@@ -1,6 +1,20 @@
1
1
  import { ServerTrackOptions, ServerIdentifyOptions } from '@outlit/core';
2
- export { IngestResponse, ServerIdentifyOptions, ServerTrackOptions, TrackerConfig } from '@outlit/core';
2
+ export { ExplicitJourneyStage, IngestResponse, ServerIdentifyOptions, ServerTrackOptions, TrackerConfig } from '@outlit/core';
3
3
 
4
+ interface StageOptions {
5
+ /**
6
+ * User's email address. Required if userId is not provided.
7
+ */
8
+ email?: string;
9
+ /**
10
+ * User's unique ID. Required if email is not provided.
11
+ */
12
+ userId?: string;
13
+ /**
14
+ * Optional properties for context.
15
+ */
16
+ properties?: Record<string, string | number | boolean | null>;
17
+ }
4
18
  interface OutlitOptions {
5
19
  /**
6
20
  * Your Outlit public key.
@@ -80,6 +94,63 @@ declare class Outlit {
80
94
  * @throws Error if neither email nor userId is provided
81
95
  */
82
96
  identify(options: ServerIdentifyOptions): void;
97
+ /**
98
+ * Mark a user as activated.
99
+ *
100
+ * Typically called after a user completes onboarding or a key activation milestone.
101
+ * Requires either `email` or `userId` to identify the user.
102
+ *
103
+ * @throws Error if neither email nor userId is provided
104
+ *
105
+ * @example
106
+ * ```typescript
107
+ * outlit.activate({
108
+ * email: 'user@example.com',
109
+ * properties: { flow: 'onboarding' }
110
+ * })
111
+ * ```
112
+ */
113
+ activate(options: StageOptions): void;
114
+ /**
115
+ * Mark a user as engaged.
116
+ *
117
+ * Typically called when a user reaches a usage milestone.
118
+ * Can also be computed automatically by the engagement cron.
119
+ * Requires either `email` or `userId` to identify the user.
120
+ *
121
+ * @throws Error if neither email nor userId is provided
122
+ *
123
+ * @example
124
+ * ```typescript
125
+ * outlit.engaged({
126
+ * userId: 'usr_123',
127
+ * properties: { milestone: 'first_project_created' }
128
+ * })
129
+ * ```
130
+ */
131
+ engaged(options: StageOptions): void;
132
+ /**
133
+ * Mark a user as paid.
134
+ *
135
+ * Typically called after a successful payment or subscription.
136
+ * Can also be triggered by Stripe integration.
137
+ * Requires either `email` or `userId` to identify the user.
138
+ *
139
+ * @throws Error if neither email nor userId is provided
140
+ *
141
+ * @example
142
+ * ```typescript
143
+ * outlit.paid({
144
+ * email: 'user@example.com',
145
+ * properties: { plan: 'pro', amount: 99 }
146
+ * })
147
+ * ```
148
+ */
149
+ paid(options: StageOptions): void;
150
+ /**
151
+ * Internal method to send a stage event.
152
+ */
153
+ private sendStageEvent;
83
154
  /**
84
155
  * Flush all pending events immediately.
85
156
  *
@@ -101,4 +172,4 @@ declare class Outlit {
101
172
  private ensureNotShutdown;
102
173
  }
103
174
 
104
- export { Outlit, type OutlitOptions };
175
+ export { Outlit, type OutlitOptions, type StageOptions };
package/dist/index.js CHANGED
@@ -184,6 +184,83 @@ var Outlit = class {
184
184
  });
185
185
  this.queue.enqueue(event);
186
186
  }
187
+ /**
188
+ * Mark a user as activated.
189
+ *
190
+ * Typically called after a user completes onboarding or a key activation milestone.
191
+ * Requires either `email` or `userId` to identify the user.
192
+ *
193
+ * @throws Error if neither email nor userId is provided
194
+ *
195
+ * @example
196
+ * ```typescript
197
+ * outlit.activate({
198
+ * email: 'user@example.com',
199
+ * properties: { flow: 'onboarding' }
200
+ * })
201
+ * ```
202
+ */
203
+ activate(options) {
204
+ this.sendStageEvent("activated", options);
205
+ }
206
+ /**
207
+ * Mark a user as engaged.
208
+ *
209
+ * Typically called when a user reaches a usage milestone.
210
+ * Can also be computed automatically by the engagement cron.
211
+ * Requires either `email` or `userId` to identify the user.
212
+ *
213
+ * @throws Error if neither email nor userId is provided
214
+ *
215
+ * @example
216
+ * ```typescript
217
+ * outlit.engaged({
218
+ * userId: 'usr_123',
219
+ * properties: { milestone: 'first_project_created' }
220
+ * })
221
+ * ```
222
+ */
223
+ engaged(options) {
224
+ this.sendStageEvent("engaged", options);
225
+ }
226
+ /**
227
+ * Mark a user as paid.
228
+ *
229
+ * Typically called after a successful payment or subscription.
230
+ * Can also be triggered by Stripe integration.
231
+ * Requires either `email` or `userId` to identify the user.
232
+ *
233
+ * @throws Error if neither email nor userId is provided
234
+ *
235
+ * @example
236
+ * ```typescript
237
+ * outlit.paid({
238
+ * email: 'user@example.com',
239
+ * properties: { plan: 'pro', amount: 99 }
240
+ * })
241
+ * ```
242
+ */
243
+ paid(options) {
244
+ this.sendStageEvent("paid", options);
245
+ }
246
+ /**
247
+ * Internal method to send a stage event.
248
+ */
249
+ sendStageEvent(stage, options) {
250
+ this.ensureNotShutdown();
251
+ (0, import_core.validateServerIdentity)(options.email, options.userId);
252
+ const event = (0, import_core.buildStageEvent)({
253
+ url: `server://${options.email ?? options.userId}`,
254
+ stage,
255
+ properties: {
256
+ ...options.properties,
257
+ // Include identity in properties for server-side resolution
258
+ __email: options.email ?? null,
259
+ __userId: options.userId ?? null
260
+ }
261
+ });
262
+ this.queue.enqueue(event);
263
+ }
187
264
  /**
188
265
  * Flush all pending events immediately.
189
266
  *
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 } from \"./client\"\n\n// Re-export useful types from core\nexport type {\n ServerTrackOptions,\n ServerIdentifyOptions,\n TrackerConfig,\n IngestResponse,\n} from \"@outlit/core\"\n","import {\n DEFAULT_API_HOST,\n type IngestPayload,\n type ServerIdentifyOptions,\n type ServerTrackOptions,\n type TrackerEvent,\n buildCustomEvent,\n buildIdentifyEvent,\n validateServerIdentity,\n} from \"@outlit/core\"\nimport { EventQueue } from \"./queue\"\nimport { HttpTransport } from \"./transport\"\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/tracker-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 * 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 } 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 DEFAULT_API_HOST,\n type ExplicitJourneyStage,\n type IngestPayload,\n type ServerIdentifyOptions,\n type ServerTrackOptions,\n type TrackerEvent,\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\nexport interface StageOptions {\n /**\n * User's email address. Required if userId is not provided.\n */\n email?: string\n\n /**\n * User's unique ID. Required if email is not provided.\n */\n userId?: string\n\n /**\n * Optional properties for context.\n */\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/tracker-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 * Mark a user as activated.\n *\n * Typically called after a user completes onboarding or a key activation milestone.\n * Requires either `email` or `userId` to identify the user.\n *\n * @throws Error if neither email nor userId is provided\n *\n * @example\n * ```typescript\n * outlit.activate({\n * email: 'user@example.com',\n * properties: { flow: 'onboarding' }\n * })\n * ```\n */\n activate(options: StageOptions): void {\n this.sendStageEvent(\"activated\", options)\n }\n\n /**\n * Mark a user as engaged.\n *\n * Typically called when a user reaches a usage milestone.\n * Can also be computed automatically by the engagement cron.\n * Requires either `email` or `userId` to identify the user.\n *\n * @throws Error if neither email nor userId is provided\n *\n * @example\n * ```typescript\n * outlit.engaged({\n * userId: 'usr_123',\n * properties: { milestone: 'first_project_created' }\n * })\n * ```\n */\n engaged(options: StageOptions): void {\n this.sendStageEvent(\"engaged\", options)\n }\n\n /**\n * Mark a user as paid.\n *\n * Typically called after a successful payment or subscription.\n * Can also be triggered by Stripe integration.\n * Requires either `email` or `userId` to identify the user.\n *\n * @throws Error if neither email nor userId is provided\n *\n * @example\n * ```typescript\n * outlit.paid({\n * email: 'user@example.com',\n * properties: { plan: 'pro', amount: 99 }\n * })\n * ```\n */\n paid(options: StageOptions): void {\n this.sendStageEvent(\"paid\", 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 /**\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
@@ -3,6 +3,7 @@ import {
3
3
  DEFAULT_API_HOST,
4
4
  buildCustomEvent,
5
5
  buildIdentifyEvent,
6
+ buildStageEvent,
6
7
  validateServerIdentity
7
8
  } from "@outlit/core";
8
9
 
@@ -163,6 +164,83 @@ var Outlit = class {
163
164
  });
164
165
  this.queue.enqueue(event);
165
166
  }
167
+ /**
168
+ * Mark a user as activated.
169
+ *
170
+ * Typically called after a user completes onboarding or a key activation milestone.
171
+ * Requires either `email` or `userId` to identify the user.
172
+ *
173
+ * @throws Error if neither email nor userId is provided
174
+ *
175
+ * @example
176
+ * ```typescript
177
+ * outlit.activate({
178
+ * email: 'user@example.com',
179
+ * properties: { flow: 'onboarding' }
180
+ * })
181
+ * ```
182
+ */
183
+ activate(options) {
184
+ this.sendStageEvent("activated", options);
185
+ }
186
+ /**
187
+ * Mark a user as engaged.
188
+ *
189
+ * Typically called when a user reaches a usage milestone.
190
+ * Can also be computed automatically by the engagement cron.
191
+ * Requires either `email` or `userId` to identify the user.
192
+ *
193
+ * @throws Error if neither email nor userId is provided
194
+ *
195
+ * @example
196
+ * ```typescript
197
+ * outlit.engaged({
198
+ * userId: 'usr_123',
199
+ * properties: { milestone: 'first_project_created' }
200
+ * })
201
+ * ```
202
+ */
203
+ engaged(options) {
204
+ this.sendStageEvent("engaged", options);
205
+ }
206
+ /**
207
+ * Mark a user as paid.
208
+ *
209
+ * Typically called after a successful payment or subscription.
210
+ * Can also be triggered by Stripe integration.
211
+ * Requires either `email` or `userId` to identify the user.
212
+ *
213
+ * @throws Error if neither email nor userId is provided
214
+ *
215
+ * @example
216
+ * ```typescript
217
+ * outlit.paid({
218
+ * email: 'user@example.com',
219
+ * properties: { plan: 'pro', amount: 99 }
220
+ * })
221
+ * ```
222
+ */
223
+ paid(options) {
224
+ this.sendStageEvent("paid", options);
225
+ }
226
+ /**
227
+ * Internal method to send a stage event.
228
+ */
229
+ sendStageEvent(stage, options) {
230
+ this.ensureNotShutdown();
231
+ validateServerIdentity(options.email, options.userId);
232
+ const event = buildStageEvent({
233
+ url: `server://${options.email ?? options.userId}`,
234
+ stage,
235
+ properties: {
236
+ ...options.properties,
237
+ // Include identity in properties for server-side resolution
238
+ __email: options.email ?? null,
239
+ __userId: options.userId ?? null
240
+ }
241
+ });
242
+ this.queue.enqueue(event);
243
+ }
166
244
  /**
167
245
  * Flush all pending events immediately.
168
246
  *
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/client.ts","../src/queue.ts","../src/transport.ts"],"sourcesContent":["import {\n DEFAULT_API_HOST,\n type IngestPayload,\n type ServerIdentifyOptions,\n type ServerTrackOptions,\n type TrackerEvent,\n buildCustomEvent,\n buildIdentifyEvent,\n validateServerIdentity,\n} from \"@outlit/core\"\nimport { EventQueue } from \"./queue\"\nimport { HttpTransport } from \"./transport\"\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/tracker-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 * 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 DEFAULT_API_HOST,\n type ExplicitJourneyStage,\n type IngestPayload,\n type ServerIdentifyOptions,\n type ServerTrackOptions,\n type TrackerEvent,\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\nexport interface StageOptions {\n /**\n * User's email address. Required if userId is not provided.\n */\n email?: string\n\n /**\n * User's unique ID. Required if email is not provided.\n */\n userId?: string\n\n /**\n * Optional properties for context.\n */\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/tracker-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 * Mark a user as activated.\n *\n * Typically called after a user completes onboarding or a key activation milestone.\n * Requires either `email` or `userId` to identify the user.\n *\n * @throws Error if neither email nor userId is provided\n *\n * @example\n * ```typescript\n * outlit.activate({\n * email: 'user@example.com',\n * properties: { flow: 'onboarding' }\n * })\n * ```\n */\n activate(options: StageOptions): void {\n this.sendStageEvent(\"activated\", options)\n }\n\n /**\n * Mark a user as engaged.\n *\n * Typically called when a user reaches a usage milestone.\n * Can also be computed automatically by the engagement cron.\n * Requires either `email` or `userId` to identify the user.\n *\n * @throws Error if neither email nor userId is provided\n *\n * @example\n * ```typescript\n * outlit.engaged({\n * userId: 'usr_123',\n * properties: { milestone: 'first_project_created' }\n * })\n * ```\n */\n engaged(options: StageOptions): void {\n this.sendStageEvent(\"engaged\", options)\n }\n\n /**\n * Mark a user as paid.\n *\n * Typically called after a successful payment or subscription.\n * Can also be triggered by Stripe integration.\n * Requires either `email` or `userId` to identify the user.\n *\n * @throws Error if neither email nor userId is provided\n *\n * @example\n * ```typescript\n * outlit.paid({\n * email: 'user@example.com',\n * properties: { plan: 'pro', amount: 99 }\n * })\n * ```\n */\n paid(options: StageOptions): void {\n this.sendStageEvent(\"paid\", 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 /**\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": "0.0.0-canary-202512180440-754f4ab-20251218044038",
3
+ "version": "0.0.0-canary-202601021633-e09ca38-20260102163321",
4
4
  "description": "Outlit server-side tracking SDK for Node.js",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Outlit AI",
@@ -42,12 +42,12 @@
42
42
  "node": ">=18.0.0"
43
43
  },
44
44
  "dependencies": {
45
- "@outlit/core": "0.0.0-canary-202512180440-754f4ab-20251218044038"
45
+ "@outlit/core": "0.0.0-canary-202601021633-e09ca38-20260102163321"
46
46
  },
47
47
  "devDependencies": {
48
48
  "tsup": "^8.0.1",
49
49
  "typescript": "^5.3.3",
50
- "@outlit/typescript-config": "0.0.0-canary-202512180440-754f4ab-20251218044038"
50
+ "@outlit/typescript-config": "0.0.1"
51
51
  },
52
52
  "scripts": {
53
53
  "build": "tsup",