@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,396 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview HTTP provider for server/Node.js environments
|
|
3
|
+
*
|
|
4
|
+
* Provides HTTP-based analytics event sending for server-side applications.
|
|
5
|
+
* Sends events to a remote ingestion endpoint (e.g., oneapp-api/ingest).
|
|
6
|
+
*
|
|
7
|
+
* Features:
|
|
8
|
+
* - Event batching for efficiency
|
|
9
|
+
* - Automatic flush on interval or batch size
|
|
10
|
+
* - Retry with exponential backoff
|
|
11
|
+
* - Graceful shutdown with final flush
|
|
12
|
+
*
|
|
13
|
+
* @module @od-oneapp/analytics/providers/http/server
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { logDebug, logError, logWarn } from '@repo/shared/logs';
|
|
17
|
+
|
|
18
|
+
import type { HttpProviderConfig, IngestionResponse, QueuedEvent } from './types';
|
|
19
|
+
import type {
|
|
20
|
+
AnalyticsContext,
|
|
21
|
+
AnalyticsProvider,
|
|
22
|
+
GroupTraits,
|
|
23
|
+
PageProperties,
|
|
24
|
+
Properties,
|
|
25
|
+
ProviderConfig,
|
|
26
|
+
UserTraits,
|
|
27
|
+
} from '../../shared/types/types';
|
|
28
|
+
|
|
29
|
+
/** Default configuration values */
|
|
30
|
+
const DEFAULTS = {
|
|
31
|
+
batchSize: 10,
|
|
32
|
+
flushInterval: 5000,
|
|
33
|
+
timeout: 10000,
|
|
34
|
+
retries: 3,
|
|
35
|
+
} as const;
|
|
36
|
+
|
|
37
|
+
/** Base delay for exponential backoff in ms */
|
|
38
|
+
const BACKOFF_BASE_MS = 1000;
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* HTTP Analytics Provider for server environments.
|
|
42
|
+
*
|
|
43
|
+
* Sends analytics events to a remote endpoint via HTTP POST requests.
|
|
44
|
+
* Events are batched and flushed periodically for efficiency.
|
|
45
|
+
*
|
|
46
|
+
* @example
|
|
47
|
+
* ```typescript
|
|
48
|
+
* const provider = new HttpServerProvider({
|
|
49
|
+
* options: {
|
|
50
|
+
* endpoint: 'https://api.oneapp.dev/v1/ingest',
|
|
51
|
+
* apiKey: process.env.ANALYTICS_API_KEY,
|
|
52
|
+
* },
|
|
53
|
+
* });
|
|
54
|
+
* await provider.initialize();
|
|
55
|
+
* await provider.track('Button Clicked', { button: 'signup' });
|
|
56
|
+
* await provider.flush(); // Send queued events
|
|
57
|
+
* ```
|
|
58
|
+
*/
|
|
59
|
+
export class HttpServerProvider implements AnalyticsProvider {
|
|
60
|
+
readonly name = 'http';
|
|
61
|
+
|
|
62
|
+
private config: Required<
|
|
63
|
+
Pick<HttpProviderConfig, 'batchSize' | 'flushInterval' | 'timeout' | 'retries'>
|
|
64
|
+
> &
|
|
65
|
+
HttpProviderConfig;
|
|
66
|
+
private isInitialized = false;
|
|
67
|
+
private queue: QueuedEvent[] = [];
|
|
68
|
+
private flushTimer: ReturnType<typeof setInterval> | null = null;
|
|
69
|
+
private userId?: string;
|
|
70
|
+
private anonymousId: string;
|
|
71
|
+
private isFlushing = false;
|
|
72
|
+
|
|
73
|
+
constructor(providerConfig: ProviderConfig) {
|
|
74
|
+
const options = (providerConfig.options ?? {}) as unknown as HttpProviderConfig;
|
|
75
|
+
|
|
76
|
+
if (!options.endpoint) {
|
|
77
|
+
throw new Error('HttpProvider requires an endpoint URL');
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
this.config = {
|
|
81
|
+
...options,
|
|
82
|
+
batchSize: options.batchSize ?? DEFAULTS.batchSize,
|
|
83
|
+
flushInterval: options.flushInterval ?? DEFAULTS.flushInterval,
|
|
84
|
+
timeout: options.timeout ?? DEFAULTS.timeout,
|
|
85
|
+
retries: options.retries ?? DEFAULTS.retries,
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
this.userId = options.userId;
|
|
89
|
+
this.anonymousId = options.anonymousId ?? this.generateAnonymousId();
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async initialize(): Promise<void> {
|
|
93
|
+
if (this.isInitialized) return;
|
|
94
|
+
|
|
95
|
+
// Start auto-flush timer if interval > 0
|
|
96
|
+
if (this.config.flushInterval > 0) {
|
|
97
|
+
this.flushTimer = setInterval(() => {
|
|
98
|
+
void this.flush();
|
|
99
|
+
}, this.config.flushInterval);
|
|
100
|
+
|
|
101
|
+
// Unref the timer so it doesn't prevent Node.js from exiting
|
|
102
|
+
if (typeof this.flushTimer.unref === 'function') {
|
|
103
|
+
this.flushTimer.unref();
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (this.config.debug) {
|
|
108
|
+
logDebug('HTTP Analytics Provider initialized', {
|
|
109
|
+
provider: 'http',
|
|
110
|
+
endpoint: this.config.endpoint,
|
|
111
|
+
batchSize: this.config.batchSize,
|
|
112
|
+
flushInterval: this.config.flushInterval,
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
this.isInitialized = true;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
async track(
|
|
120
|
+
event: string,
|
|
121
|
+
properties: Properties = {},
|
|
122
|
+
_context?: AnalyticsContext,
|
|
123
|
+
): Promise<void> {
|
|
124
|
+
if (!this.isInitialized) {
|
|
125
|
+
logWarn('HTTP provider not initialized', { provider: 'http', operation: 'track', event });
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
this.enqueue({
|
|
130
|
+
type: 'track',
|
|
131
|
+
event,
|
|
132
|
+
properties,
|
|
133
|
+
timestamp: new Date().toISOString(),
|
|
134
|
+
userId: this.userId,
|
|
135
|
+
anonymousId: this.anonymousId,
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
async identify(
|
|
140
|
+
userId: string,
|
|
141
|
+
traits: UserTraits = {},
|
|
142
|
+
_context?: AnalyticsContext,
|
|
143
|
+
): Promise<void> {
|
|
144
|
+
if (!this.isInitialized) {
|
|
145
|
+
logWarn('HTTP provider not initialized', { provider: 'http', operation: 'identify', userId });
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Update stored user ID
|
|
150
|
+
this.userId = userId;
|
|
151
|
+
|
|
152
|
+
this.enqueue({
|
|
153
|
+
type: 'identify',
|
|
154
|
+
userId,
|
|
155
|
+
traits,
|
|
156
|
+
timestamp: new Date().toISOString(),
|
|
157
|
+
anonymousId: this.anonymousId,
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
async page(
|
|
162
|
+
name?: string,
|
|
163
|
+
properties: PageProperties = {},
|
|
164
|
+
_context?: AnalyticsContext,
|
|
165
|
+
): Promise<void> {
|
|
166
|
+
if (!this.isInitialized) {
|
|
167
|
+
logWarn('HTTP provider not initialized', { provider: 'http', operation: 'page', name });
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
this.enqueue({
|
|
172
|
+
type: 'page',
|
|
173
|
+
name,
|
|
174
|
+
properties,
|
|
175
|
+
timestamp: new Date().toISOString(),
|
|
176
|
+
userId: this.userId,
|
|
177
|
+
anonymousId: this.anonymousId,
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
async group(
|
|
182
|
+
groupId: string,
|
|
183
|
+
traits: GroupTraits = {},
|
|
184
|
+
_context?: AnalyticsContext,
|
|
185
|
+
): Promise<void> {
|
|
186
|
+
if (!this.isInitialized) {
|
|
187
|
+
logWarn('HTTP provider not initialized', { provider: 'http', operation: 'group', groupId });
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
this.enqueue({
|
|
192
|
+
type: 'group',
|
|
193
|
+
groupId,
|
|
194
|
+
traits,
|
|
195
|
+
timestamp: new Date().toISOString(),
|
|
196
|
+
userId: this.userId,
|
|
197
|
+
anonymousId: this.anonymousId,
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
async alias(userId: string, previousId: string, _context?: AnalyticsContext): Promise<void> {
|
|
202
|
+
if (!this.isInitialized) {
|
|
203
|
+
logWarn('HTTP provider not initialized', {
|
|
204
|
+
provider: 'http',
|
|
205
|
+
operation: 'alias',
|
|
206
|
+
userId,
|
|
207
|
+
previousId,
|
|
208
|
+
});
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
this.enqueue({
|
|
213
|
+
type: 'alias',
|
|
214
|
+
userId,
|
|
215
|
+
previousId,
|
|
216
|
+
timestamp: new Date().toISOString(),
|
|
217
|
+
anonymousId: this.anonymousId,
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Flush all queued events to the remote endpoint.
|
|
223
|
+
* Called automatically on interval or when batch size is reached.
|
|
224
|
+
*/
|
|
225
|
+
async flush(): Promise<void> {
|
|
226
|
+
if (this.queue.length === 0 || this.isFlushing) return;
|
|
227
|
+
|
|
228
|
+
this.isFlushing = true;
|
|
229
|
+
|
|
230
|
+
// Take all events from queue
|
|
231
|
+
const events = [...this.queue];
|
|
232
|
+
this.queue = [];
|
|
233
|
+
|
|
234
|
+
try {
|
|
235
|
+
await this.sendBatch(events);
|
|
236
|
+
} catch (error) {
|
|
237
|
+
// Re-queue events on failure (they'll be retried on next flush)
|
|
238
|
+
this.queue.unshift(...events);
|
|
239
|
+
logError('Failed to flush events, re-queued for retry', {
|
|
240
|
+
provider: 'http',
|
|
241
|
+
eventCount: events.length,
|
|
242
|
+
error: error instanceof Error ? error.message : String(error),
|
|
243
|
+
});
|
|
244
|
+
} finally {
|
|
245
|
+
this.isFlushing = false;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Shutdown the provider gracefully.
|
|
251
|
+
* Flushes remaining events and stops the flush timer.
|
|
252
|
+
*/
|
|
253
|
+
async shutdown(): Promise<void> {
|
|
254
|
+
if (this.flushTimer) {
|
|
255
|
+
clearInterval(this.flushTimer);
|
|
256
|
+
this.flushTimer = null;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Final flush
|
|
260
|
+
await this.flush();
|
|
261
|
+
this.isInitialized = false;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Get the current queue length (for testing/monitoring).
|
|
266
|
+
*/
|
|
267
|
+
getQueueLength(): number {
|
|
268
|
+
return this.queue.length;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// ============================================================================
|
|
272
|
+
// Private Methods
|
|
273
|
+
// ============================================================================
|
|
274
|
+
|
|
275
|
+
private enqueue(event: QueuedEvent): void {
|
|
276
|
+
this.queue.push(event);
|
|
277
|
+
|
|
278
|
+
if (this.config.debug) {
|
|
279
|
+
logDebug('Event queued', {
|
|
280
|
+
provider: 'http',
|
|
281
|
+
type: event.type,
|
|
282
|
+
queueLength: this.queue.length,
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// Flush if batch size reached
|
|
287
|
+
if (this.queue.length >= this.config.batchSize) {
|
|
288
|
+
void this.flush();
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
private async sendBatch(events: QueuedEvent[]): Promise<void> {
|
|
293
|
+
const payload = { batch: events };
|
|
294
|
+
|
|
295
|
+
let lastError: Error | null = null;
|
|
296
|
+
|
|
297
|
+
for (let attempt = 0; attempt <= this.config.retries; attempt++) {
|
|
298
|
+
try {
|
|
299
|
+
const response = await this.sendRequest(payload);
|
|
300
|
+
|
|
301
|
+
if (response.success) {
|
|
302
|
+
if (this.config.debug) {
|
|
303
|
+
logDebug('Batch sent successfully', {
|
|
304
|
+
provider: 'http',
|
|
305
|
+
accepted: response.accepted,
|
|
306
|
+
rejected: response.rejected,
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// Request succeeded but ingestion failed
|
|
313
|
+
logWarn('Batch partially rejected', {
|
|
314
|
+
provider: 'http',
|
|
315
|
+
accepted: response.accepted,
|
|
316
|
+
rejected: response.rejected,
|
|
317
|
+
error: response.error,
|
|
318
|
+
});
|
|
319
|
+
return;
|
|
320
|
+
} catch (error) {
|
|
321
|
+
lastError = error instanceof Error ? error : new Error(String(error));
|
|
322
|
+
|
|
323
|
+
if (attempt < this.config.retries) {
|
|
324
|
+
// Exponential backoff: 1s, 2s, 4s, ...
|
|
325
|
+
const delay = BACKOFF_BASE_MS * Math.pow(2, attempt);
|
|
326
|
+
|
|
327
|
+
if (this.config.debug) {
|
|
328
|
+
logDebug('Retrying batch send', {
|
|
329
|
+
provider: 'http',
|
|
330
|
+
attempt: attempt + 1,
|
|
331
|
+
maxRetries: this.config.retries,
|
|
332
|
+
delayMs: delay,
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
await this.sleep(delay);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// All retries exhausted
|
|
342
|
+
throw lastError ?? new Error('Failed to send batch after retries');
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
private async sendRequest(payload: { batch: QueuedEvent[] }): Promise<IngestionResponse> {
|
|
346
|
+
const headers: Record<string, string> = {
|
|
347
|
+
'Content-Type': 'application/json',
|
|
348
|
+
...this.config.headers,
|
|
349
|
+
};
|
|
350
|
+
|
|
351
|
+
if (this.config.apiKey) {
|
|
352
|
+
headers['X-API-Key'] = this.config.apiKey;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
const controller = new AbortController();
|
|
356
|
+
const timeoutId = setTimeout(() => controller.abort(), this.config.timeout);
|
|
357
|
+
|
|
358
|
+
try {
|
|
359
|
+
const response = await fetch(this.config.endpoint, {
|
|
360
|
+
method: 'POST',
|
|
361
|
+
headers,
|
|
362
|
+
body: JSON.stringify(payload),
|
|
363
|
+
signal: controller.signal,
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
if (!response.ok) {
|
|
367
|
+
// Handle rate limiting
|
|
368
|
+
if (response.status === 429) {
|
|
369
|
+
const retryAfter = response.headers.get('Retry-After');
|
|
370
|
+
throw new Error(`Rate limited. Retry after ${retryAfter ?? 'unknown'} seconds`);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
const errorBody = await response.text();
|
|
374
|
+
throw new Error(`HTTP ${response.status}: ${errorBody}`);
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
return (await response.json()) as IngestionResponse;
|
|
378
|
+
} finally {
|
|
379
|
+
clearTimeout(timeoutId);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
private generateAnonymousId(): string {
|
|
384
|
+
// Use crypto.randomUUID if available (Node 19+), otherwise fallback
|
|
385
|
+
if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') {
|
|
386
|
+
return crypto.randomUUID();
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// Fallback for older Node versions
|
|
390
|
+
return `anon_${Date.now().toString(36)}${Math.random().toString(36).slice(2, 9)}`;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
private sleep(ms: number): Promise<void> {
|
|
394
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
395
|
+
}
|
|
396
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview HTTP provider type definitions
|
|
3
|
+
*
|
|
4
|
+
* Defines TypeScript types and interfaces for HTTP Analytics provider configuration.
|
|
5
|
+
* This provider sends events to a remote analytics endpoint (e.g., oneapp-api/ingest).
|
|
6
|
+
*
|
|
7
|
+
* @module @od-oneapp/analytics/providers/http/types
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Configuration for the HTTP analytics provider.
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* ```typescript
|
|
15
|
+
* const config: HttpProviderConfig = {
|
|
16
|
+
* endpoint: 'https://api.oneapp.dev/v1/ingest',
|
|
17
|
+
* apiKey: process.env.ANALYTICS_API_KEY,
|
|
18
|
+
* batchSize: 10,
|
|
19
|
+
* flushInterval: 5000,
|
|
20
|
+
* };
|
|
21
|
+
* ```
|
|
22
|
+
*/
|
|
23
|
+
export interface HttpProviderConfig {
|
|
24
|
+
/**
|
|
25
|
+
* The URL of the analytics ingestion endpoint.
|
|
26
|
+
* Must be a valid HTTPS URL in production.
|
|
27
|
+
*
|
|
28
|
+
* @example 'https://api.oneapp.dev/v1/ingest'
|
|
29
|
+
*/
|
|
30
|
+
endpoint: string;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* API key for authentication.
|
|
34
|
+
* Sent as X-API-Key header.
|
|
35
|
+
* Optional if the endpoint is configured for open mode.
|
|
36
|
+
*/
|
|
37
|
+
apiKey?: string;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Number of events to batch before sending.
|
|
41
|
+
* Events are queued until this threshold is reached or flushInterval expires.
|
|
42
|
+
*
|
|
43
|
+
* @default 10
|
|
44
|
+
*/
|
|
45
|
+
batchSize?: number;
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Interval in milliseconds to auto-flush queued events.
|
|
49
|
+
* Set to 0 to disable auto-flush (manual flush only).
|
|
50
|
+
*
|
|
51
|
+
* @default 5000
|
|
52
|
+
*/
|
|
53
|
+
flushInterval?: number;
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Request timeout in milliseconds.
|
|
57
|
+
*
|
|
58
|
+
* @default 10000
|
|
59
|
+
*/
|
|
60
|
+
timeout?: number;
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Number of retry attempts on failed requests.
|
|
64
|
+
* Uses exponential backoff between retries.
|
|
65
|
+
*
|
|
66
|
+
* @default 3
|
|
67
|
+
*/
|
|
68
|
+
retries?: number;
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Additional headers to include in requests.
|
|
72
|
+
* Note: X-API-Key and Content-Type are set automatically.
|
|
73
|
+
*/
|
|
74
|
+
headers?: Record<string, string>;
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Enable debug logging for HTTP requests.
|
|
78
|
+
*
|
|
79
|
+
* @default false
|
|
80
|
+
*/
|
|
81
|
+
debug?: boolean;
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* User ID to include with all events.
|
|
85
|
+
* Can be set after initialization via identify().
|
|
86
|
+
*/
|
|
87
|
+
userId?: string;
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Anonymous ID for unidentified users.
|
|
91
|
+
* Auto-generated if not provided.
|
|
92
|
+
*/
|
|
93
|
+
anonymousId?: string;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Internal event queue item structure.
|
|
98
|
+
* @internal
|
|
99
|
+
*/
|
|
100
|
+
export interface QueuedEvent {
|
|
101
|
+
type: 'track' | 'identify' | 'page' | 'screen' | 'group' | 'alias';
|
|
102
|
+
timestamp: string;
|
|
103
|
+
userId?: string;
|
|
104
|
+
anonymousId?: string;
|
|
105
|
+
// Track events
|
|
106
|
+
event?: string;
|
|
107
|
+
properties?: Record<string, unknown>;
|
|
108
|
+
// Identify events
|
|
109
|
+
traits?: Record<string, unknown>;
|
|
110
|
+
// Page/screen events
|
|
111
|
+
name?: string;
|
|
112
|
+
category?: string;
|
|
113
|
+
// Group events
|
|
114
|
+
groupId?: string;
|
|
115
|
+
// Alias events
|
|
116
|
+
previousId?: string;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Response from the ingestion endpoint.
|
|
121
|
+
*/
|
|
122
|
+
export interface IngestionResponse {
|
|
123
|
+
success: boolean;
|
|
124
|
+
accepted?: number;
|
|
125
|
+
rejected?: number;
|
|
126
|
+
results?: Array<{
|
|
127
|
+
id: string;
|
|
128
|
+
type: string;
|
|
129
|
+
status: 'accepted' | 'rejected';
|
|
130
|
+
error?: string;
|
|
131
|
+
}>;
|
|
132
|
+
receivedAt?: string;
|
|
133
|
+
error?: string;
|
|
134
|
+
code?: string;
|
|
135
|
+
}
|