@jami-studio/core 0.92.26 → 0.92.28

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.
Files changed (66) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/core/CHANGELOG.md +23 -0
  3. package/corpus/core/package.json +2 -1
  4. package/corpus/core/src/deploy/build.ts +56 -2
  5. package/corpus/core/src/event-bus/bus.ts +138 -140
  6. package/corpus/core/src/event-bus/registry.ts +81 -79
  7. package/corpus/core/src/file-upload/registry.ts +135 -125
  8. package/corpus/core/src/notifications/registry.ts +218 -216
  9. package/corpus/core/src/observability/store.ts +1313 -1321
  10. package/corpus/core/src/private-blob/registry.ts +246 -242
  11. package/corpus/core/src/secrets/register.ts +132 -129
  12. package/corpus/core/src/shared/global-scope.ts +75 -0
  13. package/corpus/core/src/shared/init-memo.ts +94 -0
  14. package/corpus/core/src/sharing/registry.ts +193 -194
  15. package/corpus/core/src/tracking/providers.ts +425 -422
  16. package/corpus/core/src/tracking/registry.ts +102 -102
  17. package/dist/collab/routes.d.ts +1 -1
  18. package/dist/deploy/build.d.ts +23 -0
  19. package/dist/deploy/build.d.ts.map +1 -1
  20. package/dist/deploy/build.js +36 -2
  21. package/dist/deploy/build.js.map +1 -1
  22. package/dist/event-bus/bus.d.ts.map +1 -1
  23. package/dist/event-bus/bus.js +8 -6
  24. package/dist/event-bus/bus.js.map +1 -1
  25. package/dist/event-bus/registry.d.ts.map +1 -1
  26. package/dist/event-bus/registry.js +10 -6
  27. package/dist/event-bus/registry.js.map +1 -1
  28. package/dist/file-upload/actions/upload-image.d.ts +1 -1
  29. package/dist/file-upload/registry.d.ts.map +1 -1
  30. package/dist/file-upload/registry.js +28 -8
  31. package/dist/file-upload/registry.js.map +1 -1
  32. package/dist/notifications/registry.d.ts.map +1 -1
  33. package/dist/notifications/registry.js +6 -5
  34. package/dist/notifications/registry.js.map +1 -1
  35. package/dist/observability/routes.d.ts +5 -5
  36. package/dist/observability/store.d.ts +1 -1
  37. package/dist/observability/store.d.ts.map +1 -1
  38. package/dist/observability/store.js +311 -316
  39. package/dist/observability/store.js.map +1 -1
  40. package/dist/private-blob/registry.d.ts.map +1 -1
  41. package/dist/private-blob/registry.js +18 -13
  42. package/dist/private-blob/registry.js.map +1 -1
  43. package/dist/progress/routes.d.ts +1 -1
  44. package/dist/resources/handlers.d.ts +2 -2
  45. package/dist/secrets/register.d.ts.map +1 -1
  46. package/dist/secrets/register.js +11 -7
  47. package/dist/secrets/register.js.map +1 -1
  48. package/dist/secrets/routes.d.ts +9 -9
  49. package/dist/shared/global-scope.d.ts +55 -0
  50. package/dist/shared/global-scope.d.ts.map +1 -0
  51. package/dist/shared/global-scope.js +70 -0
  52. package/dist/shared/global-scope.js.map +1 -0
  53. package/dist/shared/init-memo.d.ts +34 -0
  54. package/dist/shared/init-memo.d.ts.map +1 -0
  55. package/dist/shared/init-memo.js +80 -0
  56. package/dist/shared/init-memo.js.map +1 -0
  57. package/dist/sharing/registry.d.ts.map +1 -1
  58. package/dist/sharing/registry.js +2 -16
  59. package/dist/sharing/registry.js.map +1 -1
  60. package/dist/tracking/providers.d.ts.map +1 -1
  61. package/dist/tracking/providers.js +11 -9
  62. package/dist/tracking/providers.js.map +1 -1
  63. package/dist/tracking/registry.d.ts.map +1 -1
  64. package/dist/tracking/registry.js +5 -5
  65. package/dist/tracking/registry.js.map +1 -1
  66. package/package.json +2 -1
@@ -1,422 +1,425 @@
1
- /**
2
- * Built-in tracking providers that auto-register from env vars.
3
- *
4
- * No SDK dependencies — uses raw HTTP to keep core lightweight.
5
- * Set the env var and tracking starts automatically.
6
- *
7
- * POSTHOG_API_KEY + POSTHOG_HOST → PostHog
8
- * MIXPANEL_TOKEN → Mixpanel
9
- * AMPLITUDE_API_KEY → Amplitude
10
- * AGENT_NATIVE_ANALYTICS_PUBLIC_KEY → Agent Native Analytics
11
- *
12
- * Call `registerBuiltinProviders()` at server startup (done
13
- * automatically by the core-routes plugin).
14
- */
15
-
16
- import { registerTrackingProvider } from "./registry.js";
17
- import type { TrackingProvider, TrackingEvent } from "./types.js";
18
-
19
- const POSTHOG_DEFAULT_HOST = "https://us.i.posthog.com";
20
- const AGENT_NATIVE_ANALYTICS_DEFAULT_ENDPOINT =
21
- "https://analytics.jami.studio/track";
22
- const BATCH_INTERVAL_MS = 10_000;
23
- const MAX_BATCH_SIZE = 50;
24
-
25
- // ─── Batched sender ────────────────────────────────────────────────────────
26
-
27
- interface QueuedEvent {
28
- url: string;
29
- body: string;
30
- headers?: Record<string, string>;
31
- }
32
-
33
- interface EnqueueOptions {
34
- flushImmediately?: boolean;
35
- }
36
-
37
- // Use globalThis so multiple ESM graph instances (Vite dev + Nitro symlinks)
38
- // share one queue, matching the same pattern as the tracking registry.
39
- const QUEUE_KEY = Symbol.for("@agent-native/core/tracking.queue");
40
- const TIMER_KEY = Symbol.for("@agent-native/core/tracking.timer");
41
-
42
- interface GlobalWithQueue {
43
- [QUEUE_KEY]?: QueuedEvent[];
44
- [TIMER_KEY]?: ReturnType<typeof setTimeout> | null;
45
- }
46
-
47
- function getQueue(): QueuedEvent[] {
48
- const g = globalThis as unknown as GlobalWithQueue;
49
- if (!g[QUEUE_KEY]) g[QUEUE_KEY] = [];
50
- return g[QUEUE_KEY]!;
51
- }
52
-
53
- function getTimer(): ReturnType<typeof setTimeout> | null {
54
- const g = globalThis as unknown as GlobalWithQueue;
55
- return g[TIMER_KEY] ?? null;
56
- }
57
-
58
- function setTimer(t: ReturnType<typeof setTimeout> | null): void {
59
- (globalThis as unknown as GlobalWithQueue)[TIMER_KEY] = t;
60
- }
61
-
62
- function enqueue(
63
- url: string,
64
- body: string,
65
- headers?: Record<string, string>,
66
- options?: EnqueueOptions,
67
- ): void {
68
- const queue = getQueue();
69
- queue.push({ url, body, headers });
70
- if (options?.flushImmediately || queue.length >= MAX_BATCH_SIZE) {
71
- void drainQueue();
72
- } else if (!getTimer()) {
73
- const timer = setTimeout(() => {
74
- void drainQueue();
75
- }, BATCH_INTERVAL_MS);
76
- if (timer.unref) timer.unref();
77
- setTimer(timer);
78
- }
79
- }
80
-
81
- function drainQueue(): Promise<void[]> {
82
- const t = getTimer();
83
- if (t) {
84
- clearTimeout(t);
85
- setTimer(null);
86
- }
87
- const queue = getQueue();
88
- const batch = queue.splice(0, queue.length);
89
- return Promise.all(
90
- batch.map((item) =>
91
- fetch(item.url, {
92
- method: "POST",
93
- headers: { "Content-Type": "application/json", ...item.headers },
94
- body: item.body,
95
- }).then(
96
- () => undefined,
97
- () => undefined,
98
- ),
99
- ),
100
- );
101
- }
102
-
103
- function isLocalhostUrl(value: string | undefined): boolean {
104
- if (!value || !value.trim()) return false;
105
- const raw = value.trim();
106
- const withProtocol = /^[a-z][a-z0-9+.-]*:\/\//i.test(raw)
107
- ? raw
108
- : `https://${raw}`;
109
- try {
110
- const { hostname } = new URL(withProtocol);
111
- const h = hostname.toLowerCase();
112
- return (
113
- h === "localhost" ||
114
- h === "127.0.0.1" ||
115
- h === "::1" ||
116
- h === "[::1]" ||
117
- h.endsWith(".localhost") ||
118
- h.endsWith(".local")
119
- );
120
- } catch {
121
- return false;
122
- }
123
- }
124
-
125
- function shouldSkipAgentNativeAnalyticsForLocalhost(): boolean {
126
- if (process.env.AGENT_NATIVE_ANALYTICS_ALLOW_LOCALHOST === "true") {
127
- return false;
128
- }
129
- if (process.env.NODE_ENV === "development") return true;
130
- return [
131
- process.env.APP_URL,
132
- process.env.BETTER_AUTH_URL,
133
- process.env.URL,
134
- process.env.DEPLOY_URL,
135
- process.env.VERCEL_PROJECT_PRODUCTION_URL,
136
- process.env.VERCEL_URL,
137
- ].some(isLocalhostUrl);
138
- }
139
-
140
- function isServerlessRuntime(): boolean {
141
- return Boolean(
142
- process.env.NETLIFY ||
143
- process.env.VERCEL ||
144
- process.env.AWS_LAMBDA_FUNCTION_NAME ||
145
- process.env.AWS_EXECUTION_ENV ||
146
- process.env.LAMBDA_TASK_ROOT ||
147
- process.env.FUNCTION_NAME,
148
- );
149
- }
150
-
151
- function agentNativeAnalyticsFlushesImmediately(): boolean {
152
- const mode =
153
- process.env.AGENT_NATIVE_ANALYTICS_FLUSH_MODE?.trim().toLowerCase();
154
- if (mode === "batch") return false;
155
- if (mode === "immediate") return true;
156
- return isServerlessRuntime();
157
- }
158
-
159
- // ─── PostHog ───────────────────────────────────────────────────────────────
160
-
161
- function isPostHogAiObservabilityEvent(eventName: string): boolean {
162
- return eventName.startsWith("$ai_");
163
- }
164
-
165
- function createPostHogProvider(apiKey: string, host: string): TrackingProvider {
166
- return {
167
- name: "posthog",
168
- track(event: TrackingEvent) {
169
- const distinctId = event.userId || "anonymous";
170
- if (isPostHogAiObservabilityEvent(event.name)) {
171
- enqueue(
172
- `${host}/i/v0/e/`,
173
- JSON.stringify({
174
- api_key: apiKey,
175
- event: event.name,
176
- properties: {
177
- distinct_id: distinctId,
178
- ...event.properties,
179
- timestamp: event.timestamp,
180
- },
181
- }),
182
- );
183
- return;
184
- }
185
-
186
- enqueue(
187
- `${host}/capture/`,
188
- JSON.stringify({
189
- api_key: apiKey,
190
- event: event.name,
191
- distinct_id: distinctId,
192
- properties: {
193
- ...event.properties,
194
- timestamp: event.timestamp,
195
- },
196
- }),
197
- );
198
- },
199
- identify(userId, traits) {
200
- enqueue(
201
- `${host}/capture/`,
202
- JSON.stringify({
203
- api_key: apiKey,
204
- event: "$identify",
205
- distinct_id: userId,
206
- properties: { $set: traits },
207
- }),
208
- );
209
- },
210
- flush: () => {
211
- return drainQueue().then(() => undefined);
212
- },
213
- };
214
- }
215
-
216
- // ─── Mixpanel ──────────────────────────────────────────────────────────────
217
-
218
- function createMixpanelProvider(token: string): TrackingProvider {
219
- return {
220
- name: "mixpanel",
221
- track(event: TrackingEvent) {
222
- const data = {
223
- event: event.name,
224
- properties: {
225
- token,
226
- distinct_id: event.userId || "anonymous",
227
- time: event.timestamp
228
- ? new Date(event.timestamp).getTime() / 1000
229
- : undefined,
230
- ...event.properties,
231
- },
232
- };
233
- enqueue("https://api.mixpanel.com/track", JSON.stringify([data]));
234
- },
235
- identify(userId, traits) {
236
- const data = {
237
- $token: token,
238
- $distinct_id: userId,
239
- $set: traits,
240
- };
241
- enqueue("https://api.mixpanel.com/engage", JSON.stringify([data]));
242
- },
243
- flush: () => {
244
- return drainQueue().then(() => undefined);
245
- },
246
- };
247
- }
248
-
249
- // ─── Amplitude ─────────────────────────────────────────────────────────────
250
-
251
- function createAmplitudeProvider(apiKey: string): TrackingProvider {
252
- return {
253
- name: "amplitude",
254
- track(event: TrackingEvent) {
255
- const data = {
256
- api_key: apiKey,
257
- events: [
258
- {
259
- event_type: event.name,
260
- user_id: event.userId || "anonymous",
261
- event_properties: event.properties,
262
- time: event.timestamp
263
- ? new Date(event.timestamp).getTime()
264
- : undefined,
265
- },
266
- ],
267
- };
268
- enqueue("https://api2.amplitude.com/2/httpapi", JSON.stringify(data));
269
- },
270
- identify(userId, traits) {
271
- const data = {
272
- api_key: apiKey,
273
- events: [
274
- {
275
- event_type: "$identify",
276
- user_id: userId,
277
- user_properties: { $set: traits },
278
- },
279
- ],
280
- };
281
- enqueue("https://api2.amplitude.com/2/httpapi", JSON.stringify(data));
282
- },
283
- flush: () => {
284
- return drainQueue().then(() => undefined);
285
- },
286
- };
287
- }
288
-
289
- // ─── Webhook (custom HTTP endpoint) ───────────────────────────────────────
290
-
291
- function createWebhookProvider(
292
- url: string,
293
- authHeader?: string,
294
- ): TrackingProvider {
295
- const extra = authHeader ? { Authorization: authHeader } : undefined;
296
- return {
297
- name: "webhook",
298
- track(event: TrackingEvent) {
299
- enqueue(
300
- url,
301
- JSON.stringify({
302
- event: event.name,
303
- properties: event.properties,
304
- userId: event.userId,
305
- timestamp: event.timestamp,
306
- }),
307
- extra,
308
- );
309
- },
310
- identify(userId, traits) {
311
- enqueue(
312
- url,
313
- JSON.stringify({
314
- event: "$identify",
315
- userId,
316
- traits,
317
- timestamp: new Date().toISOString(),
318
- }),
319
- extra,
320
- );
321
- },
322
- flush: () => {
323
- return drainQueue().then(() => undefined);
324
- },
325
- };
326
- }
327
-
328
- // ─── Agent Native Analytics ───────────────────────────────────────────────
329
-
330
- function createAgentNativeAnalyticsProvider(
331
- publicKey: string,
332
- endpoint: string,
333
- ): TrackingProvider {
334
- const flushImmediately = agentNativeAnalyticsFlushesImmediately();
335
- return {
336
- name: "agent-native-analytics",
337
- track(event: TrackingEvent) {
338
- enqueue(
339
- endpoint,
340
- JSON.stringify({
341
- publicKey,
342
- event: event.name,
343
- properties: event.properties ?? {},
344
- userId: event.userId,
345
- timestamp: event.timestamp,
346
- }),
347
- undefined,
348
- { flushImmediately },
349
- );
350
- },
351
- identify(userId, traits) {
352
- enqueue(
353
- endpoint,
354
- JSON.stringify({
355
- publicKey,
356
- event: "$identify",
357
- userId,
358
- properties: traits ?? {},
359
- timestamp: new Date().toISOString(),
360
- }),
361
- undefined,
362
- { flushImmediately },
363
- );
364
- },
365
- flush: () => {
366
- return drainQueue().then(() => undefined);
367
- },
368
- };
369
- }
370
-
371
- // ─── Auto-registration ────────────────────────────────────────────────────
372
-
373
- let _registered = false;
374
-
375
- export function registerBuiltinProviders(): void {
376
- if (_registered) return;
377
- _registered = true;
378
-
379
- const posthogKey = process.env.POSTHOG_API_KEY;
380
- if (posthogKey) {
381
- const host = (process.env.POSTHOG_HOST || POSTHOG_DEFAULT_HOST).replace(
382
- /\/+$/,
383
- "",
384
- );
385
- registerTrackingProvider(createPostHogProvider(posthogKey, host));
386
- }
387
-
388
- const mixpanelToken = process.env.MIXPANEL_TOKEN;
389
- if (mixpanelToken) {
390
- registerTrackingProvider(createMixpanelProvider(mixpanelToken));
391
- }
392
-
393
- const amplitudeKey = process.env.AMPLITUDE_API_KEY;
394
- if (amplitudeKey) {
395
- registerTrackingProvider(createAmplitudeProvider(amplitudeKey));
396
- }
397
-
398
- const agentNativeAnalyticsKey =
399
- process.env.AGENT_NATIVE_ANALYTICS_PUBLIC_KEY ||
400
- process.env.VITE_AGENT_NATIVE_ANALYTICS_PUBLIC_KEY;
401
- if (
402
- agentNativeAnalyticsKey &&
403
- !shouldSkipAgentNativeAnalyticsForLocalhost()
404
- ) {
405
- registerTrackingProvider(
406
- createAgentNativeAnalyticsProvider(
407
- agentNativeAnalyticsKey,
408
- (
409
- process.env.AGENT_NATIVE_ANALYTICS_ENDPOINT ||
410
- AGENT_NATIVE_ANALYTICS_DEFAULT_ENDPOINT
411
- ).replace(/\/+$/, ""),
412
- ),
413
- );
414
- }
415
-
416
- const webhookUrl = process.env.TRACKING_WEBHOOK_URL;
417
- if (webhookUrl) {
418
- registerTrackingProvider(
419
- createWebhookProvider(webhookUrl, process.env.TRACKING_WEBHOOK_AUTH),
420
- );
421
- }
422
- }
1
+ /**
2
+ * Built-in tracking providers that auto-register from env vars.
3
+ *
4
+ * No SDK dependencies — uses raw HTTP to keep core lightweight.
5
+ * Set the env var and tracking starts automatically.
6
+ *
7
+ * POSTHOG_API_KEY + POSTHOG_HOST → PostHog
8
+ * MIXPANEL_TOKEN → Mixpanel
9
+ * AMPLITUDE_API_KEY → Amplitude
10
+ * AGENT_NATIVE_ANALYTICS_PUBLIC_KEY → Agent Native Analytics
11
+ *
12
+ * Call `registerBuiltinProviders()` at server startup (done
13
+ * automatically by the core-routes plugin).
14
+ */
15
+
16
+ import { getScopedGlobal } from "../shared/global-scope.js";
17
+ import { registerTrackingProvider } from "./registry.js";
18
+ import type { TrackingProvider, TrackingEvent } from "./types.js";
19
+
20
+ const POSTHOG_DEFAULT_HOST = "https://us.i.posthog.com";
21
+ const AGENT_NATIVE_ANALYTICS_DEFAULT_ENDPOINT =
22
+ "https://analytics.jami.studio/track";
23
+ const BATCH_INTERVAL_MS = 10_000;
24
+ const MAX_BATCH_SIZE = 50;
25
+
26
+ // ─── Batched sender ────────────────────────────────────────────────────────
27
+
28
+ interface QueuedEvent {
29
+ url: string;
30
+ body: string;
31
+ headers?: Record<string, string>;
32
+ }
33
+
34
+ interface EnqueueOptions {
35
+ flushImmediately?: boolean;
36
+ }
37
+
38
+ // Use globalThis so multiple ESM graph instances (Vite dev + Nitro symlinks)
39
+ // share one queue, matching the same pattern as the tracking registry.
40
+ // Scope-aware + lazily resolved so unified workspace deployments keep
41
+ // per-app queues/timers. See shared/global-scope.
42
+ const QUEUE_BASE_KEY = "agent-native.tracking.queue";
43
+ const TIMER_BASE_KEY = "agent-native.tracking.timer";
44
+
45
+ interface TimerBox {
46
+ timer: ReturnType<typeof setTimeout> | null;
47
+ }
48
+
49
+ function getQueue(): QueuedEvent[] {
50
+ return getScopedGlobal(QUEUE_BASE_KEY, () => [] as QueuedEvent[]);
51
+ }
52
+
53
+ function getTimerBox(): TimerBox {
54
+ return getScopedGlobal(TIMER_BASE_KEY, () => ({ timer: null }) as TimerBox);
55
+ }
56
+
57
+ function getTimer(): ReturnType<typeof setTimeout> | null {
58
+ return getTimerBox().timer;
59
+ }
60
+
61
+ function setTimer(t: ReturnType<typeof setTimeout> | null): void {
62
+ getTimerBox().timer = t;
63
+ }
64
+
65
+ function enqueue(
66
+ url: string,
67
+ body: string,
68
+ headers?: Record<string, string>,
69
+ options?: EnqueueOptions,
70
+ ): void {
71
+ const queue = getQueue();
72
+ queue.push({ url, body, headers });
73
+ if (options?.flushImmediately || queue.length >= MAX_BATCH_SIZE) {
74
+ void drainQueue();
75
+ } else if (!getTimer()) {
76
+ const timer = setTimeout(() => {
77
+ void drainQueue();
78
+ }, BATCH_INTERVAL_MS);
79
+ if (timer.unref) timer.unref();
80
+ setTimer(timer);
81
+ }
82
+ }
83
+
84
+ function drainQueue(): Promise<void[]> {
85
+ const t = getTimer();
86
+ if (t) {
87
+ clearTimeout(t);
88
+ setTimer(null);
89
+ }
90
+ const queue = getQueue();
91
+ const batch = queue.splice(0, queue.length);
92
+ return Promise.all(
93
+ batch.map((item) =>
94
+ fetch(item.url, {
95
+ method: "POST",
96
+ headers: { "Content-Type": "application/json", ...item.headers },
97
+ body: item.body,
98
+ }).then(
99
+ () => undefined,
100
+ () => undefined,
101
+ ),
102
+ ),
103
+ );
104
+ }
105
+
106
+ function isLocalhostUrl(value: string | undefined): boolean {
107
+ if (!value || !value.trim()) return false;
108
+ const raw = value.trim();
109
+ const withProtocol = /^[a-z][a-z0-9+.-]*:\/\//i.test(raw)
110
+ ? raw
111
+ : `https://${raw}`;
112
+ try {
113
+ const { hostname } = new URL(withProtocol);
114
+ const h = hostname.toLowerCase();
115
+ return (
116
+ h === "localhost" ||
117
+ h === "127.0.0.1" ||
118
+ h === "::1" ||
119
+ h === "[::1]" ||
120
+ h.endsWith(".localhost") ||
121
+ h.endsWith(".local")
122
+ );
123
+ } catch {
124
+ return false;
125
+ }
126
+ }
127
+
128
+ function shouldSkipAgentNativeAnalyticsForLocalhost(): boolean {
129
+ if (process.env.AGENT_NATIVE_ANALYTICS_ALLOW_LOCALHOST === "true") {
130
+ return false;
131
+ }
132
+ if (process.env.NODE_ENV === "development") return true;
133
+ return [
134
+ process.env.APP_URL,
135
+ process.env.BETTER_AUTH_URL,
136
+ process.env.URL,
137
+ process.env.DEPLOY_URL,
138
+ process.env.VERCEL_PROJECT_PRODUCTION_URL,
139
+ process.env.VERCEL_URL,
140
+ ].some(isLocalhostUrl);
141
+ }
142
+
143
+ function isServerlessRuntime(): boolean {
144
+ return Boolean(
145
+ process.env.NETLIFY ||
146
+ process.env.VERCEL ||
147
+ process.env.AWS_LAMBDA_FUNCTION_NAME ||
148
+ process.env.AWS_EXECUTION_ENV ||
149
+ process.env.LAMBDA_TASK_ROOT ||
150
+ process.env.FUNCTION_NAME,
151
+ );
152
+ }
153
+
154
+ function agentNativeAnalyticsFlushesImmediately(): boolean {
155
+ const mode =
156
+ process.env.AGENT_NATIVE_ANALYTICS_FLUSH_MODE?.trim().toLowerCase();
157
+ if (mode === "batch") return false;
158
+ if (mode === "immediate") return true;
159
+ return isServerlessRuntime();
160
+ }
161
+
162
+ // ─── PostHog ───────────────────────────────────────────────────────────────
163
+
164
+ function isPostHogAiObservabilityEvent(eventName: string): boolean {
165
+ return eventName.startsWith("$ai_");
166
+ }
167
+
168
+ function createPostHogProvider(apiKey: string, host: string): TrackingProvider {
169
+ return {
170
+ name: "posthog",
171
+ track(event: TrackingEvent) {
172
+ const distinctId = event.userId || "anonymous";
173
+ if (isPostHogAiObservabilityEvent(event.name)) {
174
+ enqueue(
175
+ `${host}/i/v0/e/`,
176
+ JSON.stringify({
177
+ api_key: apiKey,
178
+ event: event.name,
179
+ properties: {
180
+ distinct_id: distinctId,
181
+ ...event.properties,
182
+ timestamp: event.timestamp,
183
+ },
184
+ }),
185
+ );
186
+ return;
187
+ }
188
+
189
+ enqueue(
190
+ `${host}/capture/`,
191
+ JSON.stringify({
192
+ api_key: apiKey,
193
+ event: event.name,
194
+ distinct_id: distinctId,
195
+ properties: {
196
+ ...event.properties,
197
+ timestamp: event.timestamp,
198
+ },
199
+ }),
200
+ );
201
+ },
202
+ identify(userId, traits) {
203
+ enqueue(
204
+ `${host}/capture/`,
205
+ JSON.stringify({
206
+ api_key: apiKey,
207
+ event: "$identify",
208
+ distinct_id: userId,
209
+ properties: { $set: traits },
210
+ }),
211
+ );
212
+ },
213
+ flush: () => {
214
+ return drainQueue().then(() => undefined);
215
+ },
216
+ };
217
+ }
218
+
219
+ // ─── Mixpanel ──────────────────────────────────────────────────────────────
220
+
221
+ function createMixpanelProvider(token: string): TrackingProvider {
222
+ return {
223
+ name: "mixpanel",
224
+ track(event: TrackingEvent) {
225
+ const data = {
226
+ event: event.name,
227
+ properties: {
228
+ token,
229
+ distinct_id: event.userId || "anonymous",
230
+ time: event.timestamp
231
+ ? new Date(event.timestamp).getTime() / 1000
232
+ : undefined,
233
+ ...event.properties,
234
+ },
235
+ };
236
+ enqueue("https://api.mixpanel.com/track", JSON.stringify([data]));
237
+ },
238
+ identify(userId, traits) {
239
+ const data = {
240
+ $token: token,
241
+ $distinct_id: userId,
242
+ $set: traits,
243
+ };
244
+ enqueue("https://api.mixpanel.com/engage", JSON.stringify([data]));
245
+ },
246
+ flush: () => {
247
+ return drainQueue().then(() => undefined);
248
+ },
249
+ };
250
+ }
251
+
252
+ // ─── Amplitude ─────────────────────────────────────────────────────────────
253
+
254
+ function createAmplitudeProvider(apiKey: string): TrackingProvider {
255
+ return {
256
+ name: "amplitude",
257
+ track(event: TrackingEvent) {
258
+ const data = {
259
+ api_key: apiKey,
260
+ events: [
261
+ {
262
+ event_type: event.name,
263
+ user_id: event.userId || "anonymous",
264
+ event_properties: event.properties,
265
+ time: event.timestamp
266
+ ? new Date(event.timestamp).getTime()
267
+ : undefined,
268
+ },
269
+ ],
270
+ };
271
+ enqueue("https://api2.amplitude.com/2/httpapi", JSON.stringify(data));
272
+ },
273
+ identify(userId, traits) {
274
+ const data = {
275
+ api_key: apiKey,
276
+ events: [
277
+ {
278
+ event_type: "$identify",
279
+ user_id: userId,
280
+ user_properties: { $set: traits },
281
+ },
282
+ ],
283
+ };
284
+ enqueue("https://api2.amplitude.com/2/httpapi", JSON.stringify(data));
285
+ },
286
+ flush: () => {
287
+ return drainQueue().then(() => undefined);
288
+ },
289
+ };
290
+ }
291
+
292
+ // ─── Webhook (custom HTTP endpoint) ───────────────────────────────────────
293
+
294
+ function createWebhookProvider(
295
+ url: string,
296
+ authHeader?: string,
297
+ ): TrackingProvider {
298
+ const extra = authHeader ? { Authorization: authHeader } : undefined;
299
+ return {
300
+ name: "webhook",
301
+ track(event: TrackingEvent) {
302
+ enqueue(
303
+ url,
304
+ JSON.stringify({
305
+ event: event.name,
306
+ properties: event.properties,
307
+ userId: event.userId,
308
+ timestamp: event.timestamp,
309
+ }),
310
+ extra,
311
+ );
312
+ },
313
+ identify(userId, traits) {
314
+ enqueue(
315
+ url,
316
+ JSON.stringify({
317
+ event: "$identify",
318
+ userId,
319
+ traits,
320
+ timestamp: new Date().toISOString(),
321
+ }),
322
+ extra,
323
+ );
324
+ },
325
+ flush: () => {
326
+ return drainQueue().then(() => undefined);
327
+ },
328
+ };
329
+ }
330
+
331
+ // ─── Agent Native Analytics ───────────────────────────────────────────────
332
+
333
+ function createAgentNativeAnalyticsProvider(
334
+ publicKey: string,
335
+ endpoint: string,
336
+ ): TrackingProvider {
337
+ const flushImmediately = agentNativeAnalyticsFlushesImmediately();
338
+ return {
339
+ name: "agent-native-analytics",
340
+ track(event: TrackingEvent) {
341
+ enqueue(
342
+ endpoint,
343
+ JSON.stringify({
344
+ publicKey,
345
+ event: event.name,
346
+ properties: event.properties ?? {},
347
+ userId: event.userId,
348
+ timestamp: event.timestamp,
349
+ }),
350
+ undefined,
351
+ { flushImmediately },
352
+ );
353
+ },
354
+ identify(userId, traits) {
355
+ enqueue(
356
+ endpoint,
357
+ JSON.stringify({
358
+ publicKey,
359
+ event: "$identify",
360
+ userId,
361
+ properties: traits ?? {},
362
+ timestamp: new Date().toISOString(),
363
+ }),
364
+ undefined,
365
+ { flushImmediately },
366
+ );
367
+ },
368
+ flush: () => {
369
+ return drainQueue().then(() => undefined);
370
+ },
371
+ };
372
+ }
373
+
374
+ // ─── Auto-registration ────────────────────────────────────────────────────
375
+
376
+ let _registered = false;
377
+
378
+ export function registerBuiltinProviders(): void {
379
+ if (_registered) return;
380
+ _registered = true;
381
+
382
+ const posthogKey = process.env.POSTHOG_API_KEY;
383
+ if (posthogKey) {
384
+ const host = (process.env.POSTHOG_HOST || POSTHOG_DEFAULT_HOST).replace(
385
+ /\/+$/,
386
+ "",
387
+ );
388
+ registerTrackingProvider(createPostHogProvider(posthogKey, host));
389
+ }
390
+
391
+ const mixpanelToken = process.env.MIXPANEL_TOKEN;
392
+ if (mixpanelToken) {
393
+ registerTrackingProvider(createMixpanelProvider(mixpanelToken));
394
+ }
395
+
396
+ const amplitudeKey = process.env.AMPLITUDE_API_KEY;
397
+ if (amplitudeKey) {
398
+ registerTrackingProvider(createAmplitudeProvider(amplitudeKey));
399
+ }
400
+
401
+ const agentNativeAnalyticsKey =
402
+ process.env.AGENT_NATIVE_ANALYTICS_PUBLIC_KEY ||
403
+ process.env.VITE_AGENT_NATIVE_ANALYTICS_PUBLIC_KEY;
404
+ if (
405
+ agentNativeAnalyticsKey &&
406
+ !shouldSkipAgentNativeAnalyticsForLocalhost()
407
+ ) {
408
+ registerTrackingProvider(
409
+ createAgentNativeAnalyticsProvider(
410
+ agentNativeAnalyticsKey,
411
+ (
412
+ process.env.AGENT_NATIVE_ANALYTICS_ENDPOINT ||
413
+ AGENT_NATIVE_ANALYTICS_DEFAULT_ENDPOINT
414
+ ).replace(/\/+$/, ""),
415
+ ),
416
+ );
417
+ }
418
+
419
+ const webhookUrl = process.env.TRACKING_WEBHOOK_URL;
420
+ if (webhookUrl) {
421
+ registerTrackingProvider(
422
+ createWebhookProvider(webhookUrl, process.env.TRACKING_WEBHOOK_AUTH),
423
+ );
424
+ }
425
+ }