@od-oneapp/analytics 2026.1.1301
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +509 -0
- package/dist/ai-YMnynb-t.mjs +3347 -0
- package/dist/ai-YMnynb-t.mjs.map +1 -0
- package/dist/chunk-DQk6qfdC.mjs +18 -0
- package/dist/client-CTzJVFU5.mjs +9 -0
- package/dist/client-CTzJVFU5.mjs.map +1 -0
- package/dist/client-CcFTauAh.mjs +54 -0
- package/dist/client-CcFTauAh.mjs.map +1 -0
- package/dist/client-CeOLjbac.mjs +281 -0
- package/dist/client-CeOLjbac.mjs.map +1 -0
- package/dist/client-D339NFJS.mjs +267 -0
- package/dist/client-D339NFJS.mjs.map +1 -0
- package/dist/client-next.d.mts +62 -0
- package/dist/client-next.d.mts.map +1 -0
- package/dist/client-next.mjs +525 -0
- package/dist/client-next.mjs.map +1 -0
- package/dist/client.d.mts +30 -0
- package/dist/client.d.mts.map +1 -0
- package/dist/client.mjs +186 -0
- package/dist/client.mjs.map +1 -0
- package/dist/config-DPS6bSYo.d.mts +34 -0
- package/dist/config-DPS6bSYo.d.mts.map +1 -0
- package/dist/config-P6P5adJg.mjs +287 -0
- package/dist/config-P6P5adJg.mjs.map +1 -0
- package/dist/console-8bND3mMU.mjs +128 -0
- package/dist/console-8bND3mMU.mjs.map +1 -0
- package/dist/ecommerce-Cgu4wlux.mjs +993 -0
- package/dist/ecommerce-Cgu4wlux.mjs.map +1 -0
- package/dist/emitters-6-nKo8i-.mjs +208 -0
- package/dist/emitters-6-nKo8i-.mjs.map +1 -0
- package/dist/emitters-DldkVSPp.d.mts +12 -0
- package/dist/emitters-DldkVSPp.d.mts.map +1 -0
- package/dist/index-BfNWgfa5.d.mts +1494 -0
- package/dist/index-BfNWgfa5.d.mts.map +1 -0
- package/dist/index-BkIWe--N.d.mts +953 -0
- package/dist/index-BkIWe--N.d.mts.map +1 -0
- package/dist/index-jPzXRn52.d.mts +184 -0
- package/dist/index-jPzXRn52.d.mts.map +1 -0
- package/dist/manager-DvRRjza6.d.mts +76 -0
- package/dist/manager-DvRRjza6.d.mts.map +1 -0
- package/dist/posthog-bootstrap-CYfIy_WS.mjs +1769 -0
- package/dist/posthog-bootstrap-CYfIy_WS.mjs.map +1 -0
- package/dist/posthog-bootstrap-DWxFrxlt.d.mts +81 -0
- package/dist/posthog-bootstrap-DWxFrxlt.d.mts.map +1 -0
- package/dist/providers-http-client.d.mts +37 -0
- package/dist/providers-http-client.d.mts.map +1 -0
- package/dist/providers-http-client.mjs +320 -0
- package/dist/providers-http-client.mjs.map +1 -0
- package/dist/providers-http-server.d.mts +31 -0
- package/dist/providers-http-server.d.mts.map +1 -0
- package/dist/providers-http-server.mjs +297 -0
- package/dist/providers-http-server.mjs.map +1 -0
- package/dist/providers-http.d.mts +46 -0
- package/dist/providers-http.d.mts.map +1 -0
- package/dist/providers-http.mjs +4 -0
- package/dist/server-edge.d.mts +9 -0
- package/dist/server-edge.d.mts.map +1 -0
- package/dist/server-edge.mjs +373 -0
- package/dist/server-edge.mjs.map +1 -0
- package/dist/server-next.d.mts +67 -0
- package/dist/server-next.d.mts.map +1 -0
- package/dist/server-next.mjs +193 -0
- package/dist/server-next.mjs.map +1 -0
- package/dist/server.d.mts +10 -0
- package/dist/server.mjs +7 -0
- package/dist/service-cYtBBL8x.mjs +945 -0
- package/dist/service-cYtBBL8x.mjs.map +1 -0
- package/dist/shared.d.mts +16 -0
- package/dist/shared.d.mts.map +1 -0
- package/dist/shared.mjs +93 -0
- package/dist/shared.mjs.map +1 -0
- package/dist/types-BxBnNQ0V.d.mts +354 -0
- package/dist/types-BxBnNQ0V.d.mts.map +1 -0
- package/dist/types-CBvxUEaF.d.mts +216 -0
- package/dist/types-CBvxUEaF.d.mts.map +1 -0
- package/dist/types.d.mts +4 -0
- package/dist/types.mjs +0 -0
- package/dist/vercel-types-lwakUfoI.d.mts +102 -0
- package/dist/vercel-types-lwakUfoI.d.mts.map +1 -0
- package/package.json +129 -0
- package/src/client/index.ts +164 -0
- package/src/client/manager.ts +71 -0
- package/src/client/next/components.tsx +270 -0
- package/src/client/next/hooks.ts +217 -0
- package/src/client/next/manager.ts +141 -0
- package/src/client/next.ts +144 -0
- package/src/client-next.ts +101 -0
- package/src/client.ts +89 -0
- package/src/examples/ai-sdk-patterns.ts +583 -0
- package/src/examples/emitter-patterns.ts +476 -0
- package/src/examples/nextjs-emitter-patterns.tsx +403 -0
- package/src/next/app-router.tsx +564 -0
- package/src/next/client.ts +419 -0
- package/src/next/index.ts +84 -0
- package/src/next/middleware.ts +429 -0
- package/src/next/rsc.tsx +300 -0
- package/src/next/server.ts +253 -0
- package/src/next/types.d.ts +220 -0
- package/src/providers/base-provider.ts +419 -0
- package/src/providers/console/client.ts +10 -0
- package/src/providers/console/index.ts +152 -0
- package/src/providers/console/server.ts +6 -0
- package/src/providers/console/types.ts +15 -0
- package/src/providers/http/client.ts +464 -0
- package/src/providers/http/index.ts +30 -0
- package/src/providers/http/server.ts +396 -0
- package/src/providers/http/types.ts +135 -0
- package/src/providers/posthog/client.ts +518 -0
- package/src/providers/posthog/index.ts +11 -0
- package/src/providers/posthog/server.ts +329 -0
- package/src/providers/posthog/types.ts +104 -0
- package/src/providers/segment/client.ts +113 -0
- package/src/providers/segment/index.ts +11 -0
- package/src/providers/segment/server.ts +115 -0
- package/src/providers/segment/types.ts +51 -0
- package/src/providers/vercel/client.ts +102 -0
- package/src/providers/vercel/index.ts +11 -0
- package/src/providers/vercel/server.ts +89 -0
- package/src/providers/vercel/types.ts +27 -0
- package/src/server/index.ts +103 -0
- package/src/server/manager.ts +62 -0
- package/src/server/next.ts +210 -0
- package/src/server-edge.ts +442 -0
- package/src/server-next.ts +39 -0
- package/src/server.ts +106 -0
- package/src/shared/emitters/ai/README.md +981 -0
- package/src/shared/emitters/ai/events/agent.ts +130 -0
- package/src/shared/emitters/ai/events/artifacts.ts +167 -0
- package/src/shared/emitters/ai/events/chat.ts +126 -0
- package/src/shared/emitters/ai/events/chatbot-ecommerce.ts +133 -0
- package/src/shared/emitters/ai/events/completion.ts +103 -0
- package/src/shared/emitters/ai/events/content-generation.ts +347 -0
- package/src/shared/emitters/ai/events/conversation.ts +332 -0
- package/src/shared/emitters/ai/events/product-features.ts +1402 -0
- package/src/shared/emitters/ai/events/streaming.ts +114 -0
- package/src/shared/emitters/ai/events/tool.ts +93 -0
- package/src/shared/emitters/ai/index.ts +69 -0
- package/src/shared/emitters/ai/track-ai-sdk.ts +74 -0
- package/src/shared/emitters/ai/track-ai.ts +50 -0
- package/src/shared/emitters/ai/types.ts +1041 -0
- package/src/shared/emitters/ai/utils.ts +468 -0
- package/src/shared/emitters/ecommerce/events/cart-checkout.ts +106 -0
- package/src/shared/emitters/ecommerce/events/coupon.ts +49 -0
- package/src/shared/emitters/ecommerce/events/engagement.ts +61 -0
- package/src/shared/emitters/ecommerce/events/marketplace.ts +119 -0
- package/src/shared/emitters/ecommerce/events/order.ts +199 -0
- package/src/shared/emitters/ecommerce/events/product.ts +205 -0
- package/src/shared/emitters/ecommerce/events/registry.ts +123 -0
- package/src/shared/emitters/ecommerce/events/wishlist-sharing.ts +140 -0
- package/src/shared/emitters/ecommerce/index.ts +46 -0
- package/src/shared/emitters/ecommerce/track-ecommerce.ts +53 -0
- package/src/shared/emitters/ecommerce/types.ts +314 -0
- package/src/shared/emitters/ecommerce/utils.ts +216 -0
- package/src/shared/emitters/emitter-types.ts +974 -0
- package/src/shared/emitters/emitters.ts +292 -0
- package/src/shared/emitters/helpers.ts +419 -0
- package/src/shared/emitters/index.ts +66 -0
- package/src/shared/index.ts +142 -0
- package/src/shared/ingestion/index.ts +66 -0
- package/src/shared/ingestion/schemas.ts +386 -0
- package/src/shared/ingestion/service.ts +628 -0
- package/src/shared/node22-features.ts +848 -0
- package/src/shared/providers/console-provider.ts +160 -0
- package/src/shared/types/base-types.ts +54 -0
- package/src/shared/types/console-types.ts +19 -0
- package/src/shared/types/posthog-types.ts +131 -0
- package/src/shared/types/segment-types.ts +15 -0
- package/src/shared/types/types.ts +397 -0
- package/src/shared/types/vercel-types.ts +19 -0
- package/src/shared/utils/config-client.ts +19 -0
- package/src/shared/utils/config.ts +250 -0
- package/src/shared/utils/emitter-adapter.ts +212 -0
- package/src/shared/utils/manager.test.ts +36 -0
- package/src/shared/utils/manager.ts +1322 -0
- package/src/shared/utils/posthog-bootstrap.ts +136 -0
- package/src/shared/utils/posthog-client-utils.ts +48 -0
- package/src/shared/utils/posthog-next-utils.ts +282 -0
- package/src/shared/utils/posthog-server-utils.ts +210 -0
- package/src/shared/utils/rate-limit.ts +289 -0
- package/src/shared/utils/security.ts +545 -0
- package/src/shared/utils/validation-client.ts +161 -0
- package/src/shared/utils/validation.ts +399 -0
- package/src/shared.ts +155 -0
- package/src/types/index.ts +62 -0
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
import { logError, logInfo, logWarn } from "@od-oneapp/shared/logs";
|
|
2
|
+
|
|
3
|
+
//#region src/providers/http/client.ts
|
|
4
|
+
/**
|
|
5
|
+
* @fileoverview HTTP provider for browser/client environments
|
|
6
|
+
*
|
|
7
|
+
* Provides HTTP-based analytics event sending for client-side applications.
|
|
8
|
+
* Sends events to a remote ingestion endpoint (e.g., oneapp-api/ingest).
|
|
9
|
+
*
|
|
10
|
+
* Features:
|
|
11
|
+
* - Event batching for efficiency
|
|
12
|
+
* - Automatic flush on interval, batch size, or page unload
|
|
13
|
+
* - Retry with exponential backoff
|
|
14
|
+
* - Anonymous ID persistence in localStorage
|
|
15
|
+
* - Offline queue support (events queued when offline)
|
|
16
|
+
*
|
|
17
|
+
* @module @od-oneapp/analytics/providers/http/client
|
|
18
|
+
*/
|
|
19
|
+
/** Default configuration values */
|
|
20
|
+
const DEFAULTS = {
|
|
21
|
+
batchSize: 10,
|
|
22
|
+
flushInterval: 5e3,
|
|
23
|
+
timeout: 1e4,
|
|
24
|
+
retries: 3
|
|
25
|
+
};
|
|
26
|
+
/** Base delay for exponential backoff in ms */
|
|
27
|
+
const BACKOFF_BASE_MS = 1e3;
|
|
28
|
+
/** LocalStorage key for anonymous ID */
|
|
29
|
+
const ANON_ID_KEY = "analytics_anonymous_id";
|
|
30
|
+
/**
|
|
31
|
+
* HTTP Analytics Provider for browser environments.
|
|
32
|
+
*
|
|
33
|
+
* Sends analytics events to a remote endpoint via HTTP POST requests.
|
|
34
|
+
* Events are batched and flushed periodically for efficiency.
|
|
35
|
+
*
|
|
36
|
+
* @example
|
|
37
|
+
* ```typescript
|
|
38
|
+
* const provider = new HttpClientProvider({
|
|
39
|
+
* options: {
|
|
40
|
+
* endpoint: 'https://api.oneapp.dev/v1/ingest',
|
|
41
|
+
* apiKey: process.env.NEXT_PUBLIC_ANALYTICS_API_KEY,
|
|
42
|
+
* },
|
|
43
|
+
* });
|
|
44
|
+
* await provider.initialize();
|
|
45
|
+
* await provider.track('Button Clicked', { button: 'signup' });
|
|
46
|
+
* ```
|
|
47
|
+
*/
|
|
48
|
+
var HttpClientProvider = class {
|
|
49
|
+
name = "http";
|
|
50
|
+
config;
|
|
51
|
+
isInitialized = false;
|
|
52
|
+
queue = [];
|
|
53
|
+
flushTimer = null;
|
|
54
|
+
userId;
|
|
55
|
+
anonymousId;
|
|
56
|
+
isFlushing = false;
|
|
57
|
+
constructor(providerConfig) {
|
|
58
|
+
const options = providerConfig.options ?? {};
|
|
59
|
+
if (!options.endpoint) throw new Error("HttpProvider requires an endpoint URL");
|
|
60
|
+
this.config = {
|
|
61
|
+
...options,
|
|
62
|
+
batchSize: options.batchSize ?? DEFAULTS.batchSize,
|
|
63
|
+
flushInterval: options.flushInterval ?? DEFAULTS.flushInterval,
|
|
64
|
+
timeout: options.timeout ?? DEFAULTS.timeout,
|
|
65
|
+
retries: options.retries ?? DEFAULTS.retries
|
|
66
|
+
};
|
|
67
|
+
this.userId = options.userId;
|
|
68
|
+
this.anonymousId = options.anonymousId ?? this.getOrCreateAnonymousId();
|
|
69
|
+
}
|
|
70
|
+
async initialize() {
|
|
71
|
+
if (this.isInitialized) return;
|
|
72
|
+
if (this.config.flushInterval > 0) this.flushTimer = setInterval(() => {
|
|
73
|
+
this.flush();
|
|
74
|
+
}, this.config.flushInterval);
|
|
75
|
+
if (typeof document !== "undefined") document.addEventListener("visibilitychange", this.handleVisibilityChange);
|
|
76
|
+
if (typeof window !== "undefined") window.addEventListener("beforeunload", this.handleBeforeUnload);
|
|
77
|
+
if (this.config.debug) this.log("HTTP Analytics Provider initialized", {
|
|
78
|
+
endpoint: this.config.endpoint,
|
|
79
|
+
batchSize: this.config.batchSize,
|
|
80
|
+
flushInterval: this.config.flushInterval
|
|
81
|
+
});
|
|
82
|
+
this.isInitialized = true;
|
|
83
|
+
}
|
|
84
|
+
async track(event, properties = {}, _context) {
|
|
85
|
+
if (!this.isInitialized) {
|
|
86
|
+
this.warn("HTTP provider not initialized", {
|
|
87
|
+
operation: "track",
|
|
88
|
+
event
|
|
89
|
+
});
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
this.enqueue({
|
|
93
|
+
type: "track",
|
|
94
|
+
event,
|
|
95
|
+
properties,
|
|
96
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
97
|
+
userId: this.userId,
|
|
98
|
+
anonymousId: this.anonymousId
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
async identify(userId, traits = {}, _context) {
|
|
102
|
+
if (!this.isInitialized) {
|
|
103
|
+
this.warn("HTTP provider not initialized", {
|
|
104
|
+
operation: "identify",
|
|
105
|
+
userId
|
|
106
|
+
});
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
this.userId = userId;
|
|
110
|
+
this.enqueue({
|
|
111
|
+
type: "identify",
|
|
112
|
+
userId,
|
|
113
|
+
traits,
|
|
114
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
115
|
+
anonymousId: this.anonymousId
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
async page(name, properties = {}, _context) {
|
|
119
|
+
if (!this.isInitialized) {
|
|
120
|
+
this.warn("HTTP provider not initialized", {
|
|
121
|
+
operation: "page",
|
|
122
|
+
name
|
|
123
|
+
});
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
this.enqueue({
|
|
127
|
+
type: "page",
|
|
128
|
+
name,
|
|
129
|
+
properties,
|
|
130
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
131
|
+
userId: this.userId,
|
|
132
|
+
anonymousId: this.anonymousId
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
async group(groupId, traits = {}, _context) {
|
|
136
|
+
if (!this.isInitialized) {
|
|
137
|
+
this.warn("HTTP provider not initialized", {
|
|
138
|
+
operation: "group",
|
|
139
|
+
groupId
|
|
140
|
+
});
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
this.enqueue({
|
|
144
|
+
type: "group",
|
|
145
|
+
groupId,
|
|
146
|
+
traits,
|
|
147
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
148
|
+
userId: this.userId,
|
|
149
|
+
anonymousId: this.anonymousId
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
async alias(userId, previousId, _context) {
|
|
153
|
+
if (!this.isInitialized) {
|
|
154
|
+
this.warn("HTTP provider not initialized", {
|
|
155
|
+
operation: "alias",
|
|
156
|
+
userId,
|
|
157
|
+
previousId
|
|
158
|
+
});
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
this.enqueue({
|
|
162
|
+
type: "alias",
|
|
163
|
+
userId,
|
|
164
|
+
previousId,
|
|
165
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
166
|
+
anonymousId: this.anonymousId
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Flush all queued events to the remote endpoint.
|
|
171
|
+
* Called automatically on interval, batch size, or page events.
|
|
172
|
+
*/
|
|
173
|
+
async flush() {
|
|
174
|
+
if (this.queue.length === 0 || this.isFlushing) return;
|
|
175
|
+
this.isFlushing = true;
|
|
176
|
+
const events = [...this.queue];
|
|
177
|
+
this.queue = [];
|
|
178
|
+
try {
|
|
179
|
+
await this.sendBatch(events);
|
|
180
|
+
} catch (error) {
|
|
181
|
+
this.queue.unshift(...events);
|
|
182
|
+
this.error("Failed to flush events, re-queued for retry", {
|
|
183
|
+
eventCount: events.length,
|
|
184
|
+
error: error instanceof Error ? error.message : String(error)
|
|
185
|
+
});
|
|
186
|
+
} finally {
|
|
187
|
+
this.isFlushing = false;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* Shutdown the provider gracefully.
|
|
192
|
+
* Flushes remaining events and removes event listeners.
|
|
193
|
+
*/
|
|
194
|
+
async shutdown() {
|
|
195
|
+
if (this.flushTimer) {
|
|
196
|
+
clearInterval(this.flushTimer);
|
|
197
|
+
this.flushTimer = null;
|
|
198
|
+
}
|
|
199
|
+
if (typeof document !== "undefined") document.removeEventListener("visibilitychange", this.handleVisibilityChange);
|
|
200
|
+
if (typeof window !== "undefined") window.removeEventListener("beforeunload", this.handleBeforeUnload);
|
|
201
|
+
await this.flush();
|
|
202
|
+
this.isInitialized = false;
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* Get the current queue length (for testing/monitoring).
|
|
206
|
+
*/
|
|
207
|
+
getQueueLength() {
|
|
208
|
+
return this.queue.length;
|
|
209
|
+
}
|
|
210
|
+
handleVisibilityChange = () => {
|
|
211
|
+
if (document.visibilityState === "hidden") this.flush();
|
|
212
|
+
};
|
|
213
|
+
handleBeforeUnload = () => {
|
|
214
|
+
if (this.queue.length > 0 && typeof navigator?.sendBeacon === "function") {
|
|
215
|
+
const payload = JSON.stringify({ batch: this.queue });
|
|
216
|
+
const blob = new Blob([payload], { type: "application/json" });
|
|
217
|
+
navigator.sendBeacon(this.config.endpoint, blob);
|
|
218
|
+
this.queue = [];
|
|
219
|
+
}
|
|
220
|
+
};
|
|
221
|
+
enqueue(event) {
|
|
222
|
+
this.queue.push(event);
|
|
223
|
+
if (this.config.debug) this.log("Event queued", {
|
|
224
|
+
type: event.type,
|
|
225
|
+
queueLength: this.queue.length
|
|
226
|
+
});
|
|
227
|
+
if (this.queue.length >= this.config.batchSize) this.flush();
|
|
228
|
+
}
|
|
229
|
+
async sendBatch(events) {
|
|
230
|
+
const payload = { batch: events };
|
|
231
|
+
let lastError = null;
|
|
232
|
+
for (let attempt = 0; attempt <= this.config.retries; attempt++) try {
|
|
233
|
+
const response = await this.sendRequest(payload);
|
|
234
|
+
if (response.success) {
|
|
235
|
+
if (this.config.debug) this.log("Batch sent successfully", {
|
|
236
|
+
accepted: response.accepted,
|
|
237
|
+
rejected: response.rejected
|
|
238
|
+
});
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
this.warn("Batch partially rejected", {
|
|
242
|
+
accepted: response.accepted,
|
|
243
|
+
rejected: response.rejected,
|
|
244
|
+
error: response.error
|
|
245
|
+
});
|
|
246
|
+
return;
|
|
247
|
+
} catch (error) {
|
|
248
|
+
lastError = error instanceof Error ? error : new Error(String(error));
|
|
249
|
+
if (attempt < this.config.retries) {
|
|
250
|
+
const delay = BACKOFF_BASE_MS * Math.pow(2, attempt);
|
|
251
|
+
if (this.config.debug) this.log("Retrying batch send", {
|
|
252
|
+
attempt: attempt + 1,
|
|
253
|
+
maxRetries: this.config.retries,
|
|
254
|
+
delayMs: delay
|
|
255
|
+
});
|
|
256
|
+
await this.sleep(delay);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
throw lastError ?? /* @__PURE__ */ new Error("Failed to send batch after retries");
|
|
260
|
+
}
|
|
261
|
+
async sendRequest(payload) {
|
|
262
|
+
const headers = {
|
|
263
|
+
"Content-Type": "application/json",
|
|
264
|
+
...this.config.headers
|
|
265
|
+
};
|
|
266
|
+
if (this.config.apiKey) headers["X-API-Key"] = this.config.apiKey;
|
|
267
|
+
const controller = new AbortController();
|
|
268
|
+
const timeoutId = setTimeout(() => controller.abort(), this.config.timeout);
|
|
269
|
+
try {
|
|
270
|
+
const response = await fetch(this.config.endpoint, {
|
|
271
|
+
method: "POST",
|
|
272
|
+
headers,
|
|
273
|
+
body: JSON.stringify(payload),
|
|
274
|
+
signal: controller.signal,
|
|
275
|
+
credentials: "omit"
|
|
276
|
+
});
|
|
277
|
+
if (!response.ok) {
|
|
278
|
+
if (response.status === 429) {
|
|
279
|
+
const retryAfter = response.headers.get("Retry-After");
|
|
280
|
+
throw new Error(`Rate limited. Retry after ${retryAfter ?? "unknown"} seconds`);
|
|
281
|
+
}
|
|
282
|
+
const errorBody = await response.text();
|
|
283
|
+
throw new Error(`HTTP ${response.status}: ${errorBody}`);
|
|
284
|
+
}
|
|
285
|
+
return await response.json();
|
|
286
|
+
} finally {
|
|
287
|
+
clearTimeout(timeoutId);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
getOrCreateAnonymousId() {
|
|
291
|
+
if (typeof localStorage !== "undefined") try {
|
|
292
|
+
const stored = localStorage.getItem(ANON_ID_KEY);
|
|
293
|
+
if (stored) return stored;
|
|
294
|
+
const newId = this.generateAnonymousId();
|
|
295
|
+
localStorage.setItem(ANON_ID_KEY, newId);
|
|
296
|
+
return newId;
|
|
297
|
+
} catch {}
|
|
298
|
+
return this.generateAnonymousId();
|
|
299
|
+
}
|
|
300
|
+
generateAnonymousId() {
|
|
301
|
+
if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") return crypto.randomUUID();
|
|
302
|
+
return `anon_${Date.now().toString(36)}${Math.random().toString(36).slice(2, 9)}`;
|
|
303
|
+
}
|
|
304
|
+
sleep(ms) {
|
|
305
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
306
|
+
}
|
|
307
|
+
log(message, data) {
|
|
308
|
+
if (this.config.debug) logInfo(`[Analytics:HTTP] ${message}`, data);
|
|
309
|
+
}
|
|
310
|
+
warn(message, data) {
|
|
311
|
+
logWarn(`[Analytics:HTTP] ${message}`, data);
|
|
312
|
+
}
|
|
313
|
+
error(message, data) {
|
|
314
|
+
logError(`[Analytics:HTTP] ${message}`, data);
|
|
315
|
+
}
|
|
316
|
+
};
|
|
317
|
+
|
|
318
|
+
//#endregion
|
|
319
|
+
export { HttpClientProvider };
|
|
320
|
+
//# sourceMappingURL=providers-http-client.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"providers-http-client.mjs","names":[],"sources":["../src/providers/http/client.ts"],"sourcesContent":["/**\n * @fileoverview HTTP provider for browser/client environments\n *\n * Provides HTTP-based analytics event sending for client-side applications.\n * Sends events to a remote ingestion endpoint (e.g., oneapp-api/ingest).\n *\n * Features:\n * - Event batching for efficiency\n * - Automatic flush on interval, batch size, or page unload\n * - Retry with exponential backoff\n * - Anonymous ID persistence in localStorage\n * - Offline queue support (events queued when offline)\n *\n * @module @od-oneapp/analytics/providers/http/client\n */\n\nimport { logInfo, logWarn, logError } from '@repo/shared/logs';\n\nimport type { HttpProviderConfig, IngestionResponse, QueuedEvent } from './types';\nimport type {\n AnalyticsContext,\n AnalyticsProvider,\n GroupTraits,\n PageProperties,\n Properties,\n ProviderConfig,\n UserTraits,\n} from '../../shared/types/types';\n\n/** Default configuration values */\nconst DEFAULTS = {\n batchSize: 10,\n flushInterval: 5000,\n timeout: 10000,\n retries: 3,\n} as const;\n\n/** Base delay for exponential backoff in ms */\nconst BACKOFF_BASE_MS = 1000;\n\n/** LocalStorage key for anonymous ID */\nconst ANON_ID_KEY = 'analytics_anonymous_id';\n\n/**\n * HTTP Analytics Provider for browser environments.\n *\n * Sends analytics events to a remote endpoint via HTTP POST requests.\n * Events are batched and flushed periodically for efficiency.\n *\n * @example\n * ```typescript\n * const provider = new HttpClientProvider({\n * options: {\n * endpoint: 'https://api.oneapp.dev/v1/ingest',\n * apiKey: process.env.NEXT_PUBLIC_ANALYTICS_API_KEY,\n * },\n * });\n * await provider.initialize();\n * await provider.track('Button Clicked', { button: 'signup' });\n * ```\n */\nexport class HttpClientProvider implements AnalyticsProvider {\n readonly name = 'http';\n\n private config: Required<\n Pick<HttpProviderConfig, 'batchSize' | 'flushInterval' | 'timeout' | 'retries'>\n > &\n HttpProviderConfig;\n private isInitialized = false;\n private queue: QueuedEvent[] = [];\n private flushTimer: ReturnType<typeof setInterval> | null = null;\n private userId?: string;\n private anonymousId: string;\n private isFlushing = false;\n\n constructor(providerConfig: ProviderConfig) {\n const options = (providerConfig.options ?? {}) as unknown as HttpProviderConfig;\n\n if (!options.endpoint) {\n throw new Error('HttpProvider requires an endpoint URL');\n }\n\n this.config = {\n ...options,\n batchSize: options.batchSize ?? DEFAULTS.batchSize,\n flushInterval: options.flushInterval ?? DEFAULTS.flushInterval,\n timeout: options.timeout ?? DEFAULTS.timeout,\n retries: options.retries ?? DEFAULTS.retries,\n };\n\n this.userId = options.userId;\n this.anonymousId = options.anonymousId ?? this.getOrCreateAnonymousId();\n }\n\n async initialize(): Promise<void> {\n if (this.isInitialized) return;\n\n // Start auto-flush timer if interval > 0\n if (this.config.flushInterval > 0) {\n this.flushTimer = setInterval(() => {\n void this.flush();\n }, this.config.flushInterval);\n }\n\n // Flush on page unload/visibility change\n if (typeof document !== 'undefined') {\n document.addEventListener('visibilitychange', this.handleVisibilityChange);\n }\n\n if (typeof window !== 'undefined') {\n window.addEventListener('beforeunload', this.handleBeforeUnload);\n }\n\n if (this.config.debug) {\n this.log('HTTP Analytics Provider initialized', {\n endpoint: this.config.endpoint,\n batchSize: this.config.batchSize,\n flushInterval: this.config.flushInterval,\n });\n }\n\n this.isInitialized = true;\n }\n\n async track(\n event: string,\n properties: Properties = {},\n _context?: AnalyticsContext,\n ): Promise<void> {\n if (!this.isInitialized) {\n this.warn('HTTP provider not initialized', { operation: 'track', event });\n return;\n }\n\n this.enqueue({\n type: 'track',\n event,\n properties,\n timestamp: new Date().toISOString(),\n userId: this.userId,\n anonymousId: this.anonymousId,\n });\n }\n\n async identify(\n userId: string,\n traits: UserTraits = {},\n _context?: AnalyticsContext,\n ): Promise<void> {\n if (!this.isInitialized) {\n this.warn('HTTP provider not initialized', { operation: 'identify', userId });\n return;\n }\n\n // Update stored user ID\n this.userId = userId;\n\n this.enqueue({\n type: 'identify',\n userId,\n traits,\n timestamp: new Date().toISOString(),\n anonymousId: this.anonymousId,\n });\n }\n\n async page(\n name?: string,\n properties: PageProperties = {},\n _context?: AnalyticsContext,\n ): Promise<void> {\n if (!this.isInitialized) {\n this.warn('HTTP provider not initialized', { operation: 'page', name });\n return;\n }\n\n this.enqueue({\n type: 'page',\n name,\n properties,\n timestamp: new Date().toISOString(),\n userId: this.userId,\n anonymousId: this.anonymousId,\n });\n }\n\n async group(\n groupId: string,\n traits: GroupTraits = {},\n _context?: AnalyticsContext,\n ): Promise<void> {\n if (!this.isInitialized) {\n this.warn('HTTP provider not initialized', { operation: 'group', groupId });\n return;\n }\n\n this.enqueue({\n type: 'group',\n groupId,\n traits,\n timestamp: new Date().toISOString(),\n userId: this.userId,\n anonymousId: this.anonymousId,\n });\n }\n\n async alias(userId: string, previousId: string, _context?: AnalyticsContext): Promise<void> {\n if (!this.isInitialized) {\n this.warn('HTTP provider not initialized', { operation: 'alias', userId, previousId });\n return;\n }\n\n this.enqueue({\n type: 'alias',\n userId,\n previousId,\n timestamp: new Date().toISOString(),\n anonymousId: this.anonymousId,\n });\n }\n\n /**\n * Flush all queued events to the remote endpoint.\n * Called automatically on interval, batch size, or page events.\n */\n async flush(): Promise<void> {\n if (this.queue.length === 0 || this.isFlushing) return;\n\n this.isFlushing = true;\n\n // Take all events from queue\n const events = [...this.queue];\n this.queue = [];\n\n try {\n await this.sendBatch(events);\n } catch (error) {\n // Re-queue events on failure (they'll be retried on next flush)\n this.queue.unshift(...events);\n this.error('Failed to flush events, re-queued for retry', {\n eventCount: events.length,\n error: error instanceof Error ? error.message : String(error),\n });\n } finally {\n this.isFlushing = false;\n }\n }\n\n /**\n * Shutdown the provider gracefully.\n * Flushes remaining events and removes event listeners.\n */\n async shutdown(): Promise<void> {\n if (this.flushTimer) {\n clearInterval(this.flushTimer);\n this.flushTimer = null;\n }\n\n if (typeof document !== 'undefined') {\n document.removeEventListener('visibilitychange', this.handleVisibilityChange);\n }\n\n if (typeof window !== 'undefined') {\n window.removeEventListener('beforeunload', this.handleBeforeUnload);\n }\n\n // Final flush\n await this.flush();\n this.isInitialized = false;\n }\n\n /**\n * Get the current queue length (for testing/monitoring).\n */\n getQueueLength(): number {\n return this.queue.length;\n }\n\n // ============================================================================\n // Event Handlers\n // ============================================================================\n\n private handleVisibilityChange = (): void => {\n if (document.visibilityState === 'hidden') {\n void this.flush();\n }\n };\n\n private handleBeforeUnload = (): void => {\n // Use sendBeacon for reliable delivery on page unload\n if (this.queue.length > 0 && typeof navigator?.sendBeacon === 'function') {\n const payload = JSON.stringify({ batch: this.queue });\n const headers: Record<string, string> = {\n type: 'application/json',\n };\n\n // Note: sendBeacon doesn't support custom headers, so we include API key in payload\n // The server should accept it from either header or body\n const blob = new Blob([payload], headers);\n navigator.sendBeacon(this.config.endpoint, blob);\n this.queue = [];\n }\n };\n\n // ============================================================================\n // Private Methods\n // ============================================================================\n\n private enqueue(event: QueuedEvent): void {\n this.queue.push(event);\n\n if (this.config.debug) {\n this.log('Event queued', {\n type: event.type,\n queueLength: this.queue.length,\n });\n }\n\n // Flush if batch size reached\n if (this.queue.length >= this.config.batchSize) {\n void this.flush();\n }\n }\n\n private async sendBatch(events: QueuedEvent[]): Promise<void> {\n const payload = { batch: events };\n\n let lastError: Error | null = null;\n\n for (let attempt = 0; attempt <= this.config.retries; attempt++) {\n try {\n const response = await this.sendRequest(payload);\n\n if (response.success) {\n if (this.config.debug) {\n this.log('Batch sent successfully', {\n accepted: response.accepted,\n rejected: response.rejected,\n });\n }\n return;\n }\n\n // Request succeeded but ingestion failed\n this.warn('Batch partially rejected', {\n accepted: response.accepted,\n rejected: response.rejected,\n error: response.error,\n });\n return;\n } catch (error) {\n lastError = error instanceof Error ? error : new Error(String(error));\n\n if (attempt < this.config.retries) {\n // Exponential backoff: 1s, 2s, 4s, ...\n const delay = BACKOFF_BASE_MS * Math.pow(2, attempt);\n\n if (this.config.debug) {\n this.log('Retrying batch send', {\n attempt: attempt + 1,\n maxRetries: this.config.retries,\n delayMs: delay,\n });\n }\n\n await this.sleep(delay);\n }\n }\n }\n\n // All retries exhausted\n throw lastError ?? new Error('Failed to send batch after retries');\n }\n\n private async sendRequest(payload: { batch: QueuedEvent[] }): Promise<IngestionResponse> {\n const headers: Record<string, string> = {\n 'Content-Type': 'application/json',\n ...this.config.headers,\n };\n\n if (this.config.apiKey) {\n headers['X-API-Key'] = this.config.apiKey;\n }\n\n const controller = new AbortController();\n const timeoutId = setTimeout(() => controller.abort(), this.config.timeout);\n\n try {\n const response = await fetch(this.config.endpoint, {\n method: 'POST',\n headers,\n body: JSON.stringify(payload),\n signal: controller.signal,\n // Include credentials for cross-origin requests if needed\n credentials: 'omit',\n });\n\n if (!response.ok) {\n // Handle rate limiting\n if (response.status === 429) {\n const retryAfter = response.headers.get('Retry-After');\n throw new Error(`Rate limited. Retry after ${retryAfter ?? 'unknown'} seconds`);\n }\n\n const errorBody = await response.text();\n throw new Error(`HTTP ${response.status}: ${errorBody}`);\n }\n\n return (await response.json()) as IngestionResponse;\n } finally {\n clearTimeout(timeoutId);\n }\n }\n\n private getOrCreateAnonymousId(): string {\n // Try to get from localStorage\n if (typeof localStorage !== 'undefined') {\n try {\n const stored = localStorage.getItem(ANON_ID_KEY);\n if (stored) return stored;\n\n const newId = this.generateAnonymousId();\n localStorage.setItem(ANON_ID_KEY, newId);\n return newId;\n } catch {\n // localStorage not available or blocked\n }\n }\n\n return this.generateAnonymousId();\n }\n\n private generateAnonymousId(): string {\n // Use crypto.randomUUID if available\n if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') {\n return crypto.randomUUID();\n }\n\n // Fallback\n return `anon_${Date.now().toString(36)}${Math.random().toString(36).slice(2, 9)}`;\n }\n\n private sleep(ms: number): Promise<void> {\n return new Promise(resolve => setTimeout(resolve, ms));\n }\n\n // ============================================================================\n // Logging (browser-compatible)\n // ============================================================================\n\n private log(message: string, data?: Record<string, unknown>): void {\n if (this.config.debug) {\n logInfo(`[Analytics:HTTP] ${message}`, data);\n }\n }\n\n private warn(message: string, data?: Record<string, unknown>): void {\n logWarn(`[Analytics:HTTP] ${message}`, data);\n }\n\n private error(message: string, data?: Record<string, unknown>): void {\n logError(`[Analytics:HTTP] ${message}`, data);\n }\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { A as GroupTraits, F as UserTraits, M as Properties, c as ProviderConfig, i as AnalyticsProvider, j as PageProperties, n as AnalyticsContext } from "./types-BxBnNQ0V.mjs";
|
|
2
|
+
|
|
3
|
+
//#region src/providers/http/server.d.ts
|
|
4
|
+
declare class HttpServerProvider implements AnalyticsProvider {
|
|
5
|
+
readonly name = "http";
|
|
6
|
+
private config;
|
|
7
|
+
private isInitialized;
|
|
8
|
+
private queue;
|
|
9
|
+
private flushTimer;
|
|
10
|
+
private userId?;
|
|
11
|
+
private anonymousId;
|
|
12
|
+
private isFlushing;
|
|
13
|
+
constructor(providerConfig: ProviderConfig);
|
|
14
|
+
initialize(): Promise<void>;
|
|
15
|
+
track(event: string, properties?: Properties, _context?: AnalyticsContext): Promise<void>;
|
|
16
|
+
identify(userId: string, traits?: UserTraits, _context?: AnalyticsContext): Promise<void>;
|
|
17
|
+
page(name?: string, properties?: PageProperties, _context?: AnalyticsContext): Promise<void>;
|
|
18
|
+
group(groupId: string, traits?: GroupTraits, _context?: AnalyticsContext): Promise<void>;
|
|
19
|
+
alias(userId: string, previousId: string, _context?: AnalyticsContext): Promise<void>;
|
|
20
|
+
flush(): Promise<void>;
|
|
21
|
+
shutdown(): Promise<void>;
|
|
22
|
+
getQueueLength(): number;
|
|
23
|
+
private enqueue;
|
|
24
|
+
private sendBatch;
|
|
25
|
+
private sendRequest;
|
|
26
|
+
private generateAnonymousId;
|
|
27
|
+
private sleep;
|
|
28
|
+
}
|
|
29
|
+
//#endregion
|
|
30
|
+
export { HttpServerProvider };
|
|
31
|
+
//# sourceMappingURL=providers-http-server.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"providers-http-server.d.mts","names":[],"sources":["../src/providers/http/server.ts"],"mappings":";;;cA0Da,kBAAA,YAA8B,iBAAA;EAAA,SAChC,IAAA;EAAA,QAED,MAAA;EAAA,QAIA,aAAA;EAAA,QACA,KAAA;EAAA,QACA,UAAA;EAAA,QACA,MAAA;EAAA,QACA,WAAA;EAAA,QACA,UAAA;cAEI,cAAA,EAAgB,cAAA;EAmBtB,UAAA,CAAA,GAAc,OAAA;EA2Bd,KAAA,CACJ,KAAA,UACA,UAAA,GAAY,UAAA,EACZ,QAAA,GAAW,gBAAA,GACV,OAAA;EAgBG,QAAA,CACJ,MAAA,UACA,MAAA,GAAQ,UAAA,EACR,QAAA,GAAW,gBAAA,GACV,OAAA;EAkBG,IAAA,CACJ,IAAA,WACA,UAAA,GAAY,cAAA,EACZ,QAAA,GAAW,gBAAA,GACV,OAAA;EAgBG,KAAA,CACJ,OAAA,UACA,MAAA,GAAQ,WAAA,EACR,QAAA,GAAW,gBAAA,GACV,OAAA;EAgBG,KAAA,CAAM,MAAA,UAAgB,UAAA,UAAoB,QAAA,GAAW,gBAAA,GAAmB,OAAA;EAwBxE,KAAA,CAAA,GAAS,OAAA;EA4BT,QAAA,CAAA,GAAY,OAAA;EAclB,cAAA,CAAA;EAAA,QAQQ,OAAA;EAAA,QAiBM,SAAA;EAAA,QAqDA,WAAA;EAAA,QAsCN,mBAAA;EAAA,QAUA,KAAA;AAAA"}
|
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
import { logDebug, logError, logWarn } from "@od-oneapp/shared/logs";
|
|
2
|
+
|
|
3
|
+
//#region src/providers/http/server.ts
|
|
4
|
+
/**
|
|
5
|
+
* @fileoverview HTTP provider for server/Node.js environments
|
|
6
|
+
*
|
|
7
|
+
* Provides HTTP-based analytics event sending for server-side applications.
|
|
8
|
+
* Sends events to a remote ingestion endpoint (e.g., oneapp-api/ingest).
|
|
9
|
+
*
|
|
10
|
+
* Features:
|
|
11
|
+
* - Event batching for efficiency
|
|
12
|
+
* - Automatic flush on interval or batch size
|
|
13
|
+
* - Retry with exponential backoff
|
|
14
|
+
* - Graceful shutdown with final flush
|
|
15
|
+
*
|
|
16
|
+
* @module @od-oneapp/analytics/providers/http/server
|
|
17
|
+
*/
|
|
18
|
+
/** Default configuration values */
|
|
19
|
+
const DEFAULTS = {
|
|
20
|
+
batchSize: 10,
|
|
21
|
+
flushInterval: 5e3,
|
|
22
|
+
timeout: 1e4,
|
|
23
|
+
retries: 3
|
|
24
|
+
};
|
|
25
|
+
/** Base delay for exponential backoff in ms */
|
|
26
|
+
const BACKOFF_BASE_MS = 1e3;
|
|
27
|
+
/**
|
|
28
|
+
* HTTP Analytics Provider for server environments.
|
|
29
|
+
*
|
|
30
|
+
* Sends analytics events to a remote endpoint via HTTP POST requests.
|
|
31
|
+
* Events are batched and flushed periodically for efficiency.
|
|
32
|
+
*
|
|
33
|
+
* @example
|
|
34
|
+
* ```typescript
|
|
35
|
+
* const provider = new HttpServerProvider({
|
|
36
|
+
* options: {
|
|
37
|
+
* endpoint: 'https://api.oneapp.dev/v1/ingest',
|
|
38
|
+
* apiKey: process.env.ANALYTICS_API_KEY,
|
|
39
|
+
* },
|
|
40
|
+
* });
|
|
41
|
+
* await provider.initialize();
|
|
42
|
+
* await provider.track('Button Clicked', { button: 'signup' });
|
|
43
|
+
* await provider.flush(); // Send queued events
|
|
44
|
+
* ```
|
|
45
|
+
*/
|
|
46
|
+
var HttpServerProvider = class {
|
|
47
|
+
name = "http";
|
|
48
|
+
config;
|
|
49
|
+
isInitialized = false;
|
|
50
|
+
queue = [];
|
|
51
|
+
flushTimer = null;
|
|
52
|
+
userId;
|
|
53
|
+
anonymousId;
|
|
54
|
+
isFlushing = false;
|
|
55
|
+
constructor(providerConfig) {
|
|
56
|
+
const options = providerConfig.options ?? {};
|
|
57
|
+
if (!options.endpoint) throw new Error("HttpProvider requires an endpoint URL");
|
|
58
|
+
this.config = {
|
|
59
|
+
...options,
|
|
60
|
+
batchSize: options.batchSize ?? DEFAULTS.batchSize,
|
|
61
|
+
flushInterval: options.flushInterval ?? DEFAULTS.flushInterval,
|
|
62
|
+
timeout: options.timeout ?? DEFAULTS.timeout,
|
|
63
|
+
retries: options.retries ?? DEFAULTS.retries
|
|
64
|
+
};
|
|
65
|
+
this.userId = options.userId;
|
|
66
|
+
this.anonymousId = options.anonymousId ?? this.generateAnonymousId();
|
|
67
|
+
}
|
|
68
|
+
async initialize() {
|
|
69
|
+
if (this.isInitialized) return;
|
|
70
|
+
if (this.config.flushInterval > 0) {
|
|
71
|
+
this.flushTimer = setInterval(() => {
|
|
72
|
+
this.flush();
|
|
73
|
+
}, this.config.flushInterval);
|
|
74
|
+
if (typeof this.flushTimer.unref === "function") this.flushTimer.unref();
|
|
75
|
+
}
|
|
76
|
+
if (this.config.debug) logDebug("HTTP Analytics Provider initialized", {
|
|
77
|
+
provider: "http",
|
|
78
|
+
endpoint: this.config.endpoint,
|
|
79
|
+
batchSize: this.config.batchSize,
|
|
80
|
+
flushInterval: this.config.flushInterval
|
|
81
|
+
});
|
|
82
|
+
this.isInitialized = true;
|
|
83
|
+
}
|
|
84
|
+
async track(event, properties = {}, _context) {
|
|
85
|
+
if (!this.isInitialized) {
|
|
86
|
+
logWarn("HTTP provider not initialized", {
|
|
87
|
+
provider: "http",
|
|
88
|
+
operation: "track",
|
|
89
|
+
event
|
|
90
|
+
});
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
this.enqueue({
|
|
94
|
+
type: "track",
|
|
95
|
+
event,
|
|
96
|
+
properties,
|
|
97
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
98
|
+
userId: this.userId,
|
|
99
|
+
anonymousId: this.anonymousId
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
async identify(userId, traits = {}, _context) {
|
|
103
|
+
if (!this.isInitialized) {
|
|
104
|
+
logWarn("HTTP provider not initialized", {
|
|
105
|
+
provider: "http",
|
|
106
|
+
operation: "identify",
|
|
107
|
+
userId
|
|
108
|
+
});
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
this.userId = userId;
|
|
112
|
+
this.enqueue({
|
|
113
|
+
type: "identify",
|
|
114
|
+
userId,
|
|
115
|
+
traits,
|
|
116
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
117
|
+
anonymousId: this.anonymousId
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
async page(name, properties = {}, _context) {
|
|
121
|
+
if (!this.isInitialized) {
|
|
122
|
+
logWarn("HTTP provider not initialized", {
|
|
123
|
+
provider: "http",
|
|
124
|
+
operation: "page",
|
|
125
|
+
name
|
|
126
|
+
});
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
this.enqueue({
|
|
130
|
+
type: "page",
|
|
131
|
+
name,
|
|
132
|
+
properties,
|
|
133
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
134
|
+
userId: this.userId,
|
|
135
|
+
anonymousId: this.anonymousId
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
async group(groupId, traits = {}, _context) {
|
|
139
|
+
if (!this.isInitialized) {
|
|
140
|
+
logWarn("HTTP provider not initialized", {
|
|
141
|
+
provider: "http",
|
|
142
|
+
operation: "group",
|
|
143
|
+
groupId
|
|
144
|
+
});
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
this.enqueue({
|
|
148
|
+
type: "group",
|
|
149
|
+
groupId,
|
|
150
|
+
traits,
|
|
151
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
152
|
+
userId: this.userId,
|
|
153
|
+
anonymousId: this.anonymousId
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
async alias(userId, previousId, _context) {
|
|
157
|
+
if (!this.isInitialized) {
|
|
158
|
+
logWarn("HTTP provider not initialized", {
|
|
159
|
+
provider: "http",
|
|
160
|
+
operation: "alias",
|
|
161
|
+
userId,
|
|
162
|
+
previousId
|
|
163
|
+
});
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
this.enqueue({
|
|
167
|
+
type: "alias",
|
|
168
|
+
userId,
|
|
169
|
+
previousId,
|
|
170
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
171
|
+
anonymousId: this.anonymousId
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Flush all queued events to the remote endpoint.
|
|
176
|
+
* Called automatically on interval or when batch size is reached.
|
|
177
|
+
*/
|
|
178
|
+
async flush() {
|
|
179
|
+
if (this.queue.length === 0 || this.isFlushing) return;
|
|
180
|
+
this.isFlushing = true;
|
|
181
|
+
const events = [...this.queue];
|
|
182
|
+
this.queue = [];
|
|
183
|
+
try {
|
|
184
|
+
await this.sendBatch(events);
|
|
185
|
+
} catch (error) {
|
|
186
|
+
this.queue.unshift(...events);
|
|
187
|
+
logError("Failed to flush events, re-queued for retry", {
|
|
188
|
+
provider: "http",
|
|
189
|
+
eventCount: events.length,
|
|
190
|
+
error: error instanceof Error ? error.message : String(error)
|
|
191
|
+
});
|
|
192
|
+
} finally {
|
|
193
|
+
this.isFlushing = false;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Shutdown the provider gracefully.
|
|
198
|
+
* Flushes remaining events and stops the flush timer.
|
|
199
|
+
*/
|
|
200
|
+
async shutdown() {
|
|
201
|
+
if (this.flushTimer) {
|
|
202
|
+
clearInterval(this.flushTimer);
|
|
203
|
+
this.flushTimer = null;
|
|
204
|
+
}
|
|
205
|
+
await this.flush();
|
|
206
|
+
this.isInitialized = false;
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* Get the current queue length (for testing/monitoring).
|
|
210
|
+
*/
|
|
211
|
+
getQueueLength() {
|
|
212
|
+
return this.queue.length;
|
|
213
|
+
}
|
|
214
|
+
enqueue(event) {
|
|
215
|
+
this.queue.push(event);
|
|
216
|
+
if (this.config.debug) logDebug("Event queued", {
|
|
217
|
+
provider: "http",
|
|
218
|
+
type: event.type,
|
|
219
|
+
queueLength: this.queue.length
|
|
220
|
+
});
|
|
221
|
+
if (this.queue.length >= this.config.batchSize) this.flush();
|
|
222
|
+
}
|
|
223
|
+
async sendBatch(events) {
|
|
224
|
+
const payload = { batch: events };
|
|
225
|
+
let lastError = null;
|
|
226
|
+
for (let attempt = 0; attempt <= this.config.retries; attempt++) try {
|
|
227
|
+
const response = await this.sendRequest(payload);
|
|
228
|
+
if (response.success) {
|
|
229
|
+
if (this.config.debug) logDebug("Batch sent successfully", {
|
|
230
|
+
provider: "http",
|
|
231
|
+
accepted: response.accepted,
|
|
232
|
+
rejected: response.rejected
|
|
233
|
+
});
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
logWarn("Batch partially rejected", {
|
|
237
|
+
provider: "http",
|
|
238
|
+
accepted: response.accepted,
|
|
239
|
+
rejected: response.rejected,
|
|
240
|
+
error: response.error
|
|
241
|
+
});
|
|
242
|
+
return;
|
|
243
|
+
} catch (error) {
|
|
244
|
+
lastError = error instanceof Error ? error : new Error(String(error));
|
|
245
|
+
if (attempt < this.config.retries) {
|
|
246
|
+
const delay = BACKOFF_BASE_MS * Math.pow(2, attempt);
|
|
247
|
+
if (this.config.debug) logDebug("Retrying batch send", {
|
|
248
|
+
provider: "http",
|
|
249
|
+
attempt: attempt + 1,
|
|
250
|
+
maxRetries: this.config.retries,
|
|
251
|
+
delayMs: delay
|
|
252
|
+
});
|
|
253
|
+
await this.sleep(delay);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
throw lastError ?? /* @__PURE__ */ new Error("Failed to send batch after retries");
|
|
257
|
+
}
|
|
258
|
+
async sendRequest(payload) {
|
|
259
|
+
const headers = {
|
|
260
|
+
"Content-Type": "application/json",
|
|
261
|
+
...this.config.headers
|
|
262
|
+
};
|
|
263
|
+
if (this.config.apiKey) headers["X-API-Key"] = this.config.apiKey;
|
|
264
|
+
const controller = new AbortController();
|
|
265
|
+
const timeoutId = setTimeout(() => controller.abort(), this.config.timeout);
|
|
266
|
+
try {
|
|
267
|
+
const response = await fetch(this.config.endpoint, {
|
|
268
|
+
method: "POST",
|
|
269
|
+
headers,
|
|
270
|
+
body: JSON.stringify(payload),
|
|
271
|
+
signal: controller.signal
|
|
272
|
+
});
|
|
273
|
+
if (!response.ok) {
|
|
274
|
+
if (response.status === 429) {
|
|
275
|
+
const retryAfter = response.headers.get("Retry-After");
|
|
276
|
+
throw new Error(`Rate limited. Retry after ${retryAfter ?? "unknown"} seconds`);
|
|
277
|
+
}
|
|
278
|
+
const errorBody = await response.text();
|
|
279
|
+
throw new Error(`HTTP ${response.status}: ${errorBody}`);
|
|
280
|
+
}
|
|
281
|
+
return await response.json();
|
|
282
|
+
} finally {
|
|
283
|
+
clearTimeout(timeoutId);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
generateAnonymousId() {
|
|
287
|
+
if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") return crypto.randomUUID();
|
|
288
|
+
return `anon_${Date.now().toString(36)}${Math.random().toString(36).slice(2, 9)}`;
|
|
289
|
+
}
|
|
290
|
+
sleep(ms) {
|
|
291
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
292
|
+
}
|
|
293
|
+
};
|
|
294
|
+
|
|
295
|
+
//#endregion
|
|
296
|
+
export { HttpServerProvider };
|
|
297
|
+
//# sourceMappingURL=providers-http-server.mjs.map
|