@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.
Files changed (184) hide show
  1. package/README.md +509 -0
  2. package/dist/ai-YMnynb-t.mjs +3347 -0
  3. package/dist/ai-YMnynb-t.mjs.map +1 -0
  4. package/dist/chunk-DQk6qfdC.mjs +18 -0
  5. package/dist/client-CTzJVFU5.mjs +9 -0
  6. package/dist/client-CTzJVFU5.mjs.map +1 -0
  7. package/dist/client-CcFTauAh.mjs +54 -0
  8. package/dist/client-CcFTauAh.mjs.map +1 -0
  9. package/dist/client-CeOLjbac.mjs +281 -0
  10. package/dist/client-CeOLjbac.mjs.map +1 -0
  11. package/dist/client-D339NFJS.mjs +267 -0
  12. package/dist/client-D339NFJS.mjs.map +1 -0
  13. package/dist/client-next.d.mts +62 -0
  14. package/dist/client-next.d.mts.map +1 -0
  15. package/dist/client-next.mjs +525 -0
  16. package/dist/client-next.mjs.map +1 -0
  17. package/dist/client.d.mts +30 -0
  18. package/dist/client.d.mts.map +1 -0
  19. package/dist/client.mjs +186 -0
  20. package/dist/client.mjs.map +1 -0
  21. package/dist/config-DPS6bSYo.d.mts +34 -0
  22. package/dist/config-DPS6bSYo.d.mts.map +1 -0
  23. package/dist/config-P6P5adJg.mjs +287 -0
  24. package/dist/config-P6P5adJg.mjs.map +1 -0
  25. package/dist/console-8bND3mMU.mjs +128 -0
  26. package/dist/console-8bND3mMU.mjs.map +1 -0
  27. package/dist/ecommerce-Cgu4wlux.mjs +993 -0
  28. package/dist/ecommerce-Cgu4wlux.mjs.map +1 -0
  29. package/dist/emitters-6-nKo8i-.mjs +208 -0
  30. package/dist/emitters-6-nKo8i-.mjs.map +1 -0
  31. package/dist/emitters-DldkVSPp.d.mts +12 -0
  32. package/dist/emitters-DldkVSPp.d.mts.map +1 -0
  33. package/dist/index-BfNWgfa5.d.mts +1494 -0
  34. package/dist/index-BfNWgfa5.d.mts.map +1 -0
  35. package/dist/index-BkIWe--N.d.mts +953 -0
  36. package/dist/index-BkIWe--N.d.mts.map +1 -0
  37. package/dist/index-jPzXRn52.d.mts +184 -0
  38. package/dist/index-jPzXRn52.d.mts.map +1 -0
  39. package/dist/manager-DvRRjza6.d.mts +76 -0
  40. package/dist/manager-DvRRjza6.d.mts.map +1 -0
  41. package/dist/posthog-bootstrap-CYfIy_WS.mjs +1769 -0
  42. package/dist/posthog-bootstrap-CYfIy_WS.mjs.map +1 -0
  43. package/dist/posthog-bootstrap-DWxFrxlt.d.mts +81 -0
  44. package/dist/posthog-bootstrap-DWxFrxlt.d.mts.map +1 -0
  45. package/dist/providers-http-client.d.mts +37 -0
  46. package/dist/providers-http-client.d.mts.map +1 -0
  47. package/dist/providers-http-client.mjs +320 -0
  48. package/dist/providers-http-client.mjs.map +1 -0
  49. package/dist/providers-http-server.d.mts +31 -0
  50. package/dist/providers-http-server.d.mts.map +1 -0
  51. package/dist/providers-http-server.mjs +297 -0
  52. package/dist/providers-http-server.mjs.map +1 -0
  53. package/dist/providers-http.d.mts +46 -0
  54. package/dist/providers-http.d.mts.map +1 -0
  55. package/dist/providers-http.mjs +4 -0
  56. package/dist/server-edge.d.mts +9 -0
  57. package/dist/server-edge.d.mts.map +1 -0
  58. package/dist/server-edge.mjs +373 -0
  59. package/dist/server-edge.mjs.map +1 -0
  60. package/dist/server-next.d.mts +67 -0
  61. package/dist/server-next.d.mts.map +1 -0
  62. package/dist/server-next.mjs +193 -0
  63. package/dist/server-next.mjs.map +1 -0
  64. package/dist/server.d.mts +10 -0
  65. package/dist/server.mjs +7 -0
  66. package/dist/service-cYtBBL8x.mjs +945 -0
  67. package/dist/service-cYtBBL8x.mjs.map +1 -0
  68. package/dist/shared.d.mts +16 -0
  69. package/dist/shared.d.mts.map +1 -0
  70. package/dist/shared.mjs +93 -0
  71. package/dist/shared.mjs.map +1 -0
  72. package/dist/types-BxBnNQ0V.d.mts +354 -0
  73. package/dist/types-BxBnNQ0V.d.mts.map +1 -0
  74. package/dist/types-CBvxUEaF.d.mts +216 -0
  75. package/dist/types-CBvxUEaF.d.mts.map +1 -0
  76. package/dist/types.d.mts +4 -0
  77. package/dist/types.mjs +0 -0
  78. package/dist/vercel-types-lwakUfoI.d.mts +102 -0
  79. package/dist/vercel-types-lwakUfoI.d.mts.map +1 -0
  80. package/package.json +129 -0
  81. package/src/client/index.ts +164 -0
  82. package/src/client/manager.ts +71 -0
  83. package/src/client/next/components.tsx +270 -0
  84. package/src/client/next/hooks.ts +217 -0
  85. package/src/client/next/manager.ts +141 -0
  86. package/src/client/next.ts +144 -0
  87. package/src/client-next.ts +101 -0
  88. package/src/client.ts +89 -0
  89. package/src/examples/ai-sdk-patterns.ts +583 -0
  90. package/src/examples/emitter-patterns.ts +476 -0
  91. package/src/examples/nextjs-emitter-patterns.tsx +403 -0
  92. package/src/next/app-router.tsx +564 -0
  93. package/src/next/client.ts +419 -0
  94. package/src/next/index.ts +84 -0
  95. package/src/next/middleware.ts +429 -0
  96. package/src/next/rsc.tsx +300 -0
  97. package/src/next/server.ts +253 -0
  98. package/src/next/types.d.ts +220 -0
  99. package/src/providers/base-provider.ts +419 -0
  100. package/src/providers/console/client.ts +10 -0
  101. package/src/providers/console/index.ts +152 -0
  102. package/src/providers/console/server.ts +6 -0
  103. package/src/providers/console/types.ts +15 -0
  104. package/src/providers/http/client.ts +464 -0
  105. package/src/providers/http/index.ts +30 -0
  106. package/src/providers/http/server.ts +396 -0
  107. package/src/providers/http/types.ts +135 -0
  108. package/src/providers/posthog/client.ts +518 -0
  109. package/src/providers/posthog/index.ts +11 -0
  110. package/src/providers/posthog/server.ts +329 -0
  111. package/src/providers/posthog/types.ts +104 -0
  112. package/src/providers/segment/client.ts +113 -0
  113. package/src/providers/segment/index.ts +11 -0
  114. package/src/providers/segment/server.ts +115 -0
  115. package/src/providers/segment/types.ts +51 -0
  116. package/src/providers/vercel/client.ts +102 -0
  117. package/src/providers/vercel/index.ts +11 -0
  118. package/src/providers/vercel/server.ts +89 -0
  119. package/src/providers/vercel/types.ts +27 -0
  120. package/src/server/index.ts +103 -0
  121. package/src/server/manager.ts +62 -0
  122. package/src/server/next.ts +210 -0
  123. package/src/server-edge.ts +442 -0
  124. package/src/server-next.ts +39 -0
  125. package/src/server.ts +106 -0
  126. package/src/shared/emitters/ai/README.md +981 -0
  127. package/src/shared/emitters/ai/events/agent.ts +130 -0
  128. package/src/shared/emitters/ai/events/artifacts.ts +167 -0
  129. package/src/shared/emitters/ai/events/chat.ts +126 -0
  130. package/src/shared/emitters/ai/events/chatbot-ecommerce.ts +133 -0
  131. package/src/shared/emitters/ai/events/completion.ts +103 -0
  132. package/src/shared/emitters/ai/events/content-generation.ts +347 -0
  133. package/src/shared/emitters/ai/events/conversation.ts +332 -0
  134. package/src/shared/emitters/ai/events/product-features.ts +1402 -0
  135. package/src/shared/emitters/ai/events/streaming.ts +114 -0
  136. package/src/shared/emitters/ai/events/tool.ts +93 -0
  137. package/src/shared/emitters/ai/index.ts +69 -0
  138. package/src/shared/emitters/ai/track-ai-sdk.ts +74 -0
  139. package/src/shared/emitters/ai/track-ai.ts +50 -0
  140. package/src/shared/emitters/ai/types.ts +1041 -0
  141. package/src/shared/emitters/ai/utils.ts +468 -0
  142. package/src/shared/emitters/ecommerce/events/cart-checkout.ts +106 -0
  143. package/src/shared/emitters/ecommerce/events/coupon.ts +49 -0
  144. package/src/shared/emitters/ecommerce/events/engagement.ts +61 -0
  145. package/src/shared/emitters/ecommerce/events/marketplace.ts +119 -0
  146. package/src/shared/emitters/ecommerce/events/order.ts +199 -0
  147. package/src/shared/emitters/ecommerce/events/product.ts +205 -0
  148. package/src/shared/emitters/ecommerce/events/registry.ts +123 -0
  149. package/src/shared/emitters/ecommerce/events/wishlist-sharing.ts +140 -0
  150. package/src/shared/emitters/ecommerce/index.ts +46 -0
  151. package/src/shared/emitters/ecommerce/track-ecommerce.ts +53 -0
  152. package/src/shared/emitters/ecommerce/types.ts +314 -0
  153. package/src/shared/emitters/ecommerce/utils.ts +216 -0
  154. package/src/shared/emitters/emitter-types.ts +974 -0
  155. package/src/shared/emitters/emitters.ts +292 -0
  156. package/src/shared/emitters/helpers.ts +419 -0
  157. package/src/shared/emitters/index.ts +66 -0
  158. package/src/shared/index.ts +142 -0
  159. package/src/shared/ingestion/index.ts +66 -0
  160. package/src/shared/ingestion/schemas.ts +386 -0
  161. package/src/shared/ingestion/service.ts +628 -0
  162. package/src/shared/node22-features.ts +848 -0
  163. package/src/shared/providers/console-provider.ts +160 -0
  164. package/src/shared/types/base-types.ts +54 -0
  165. package/src/shared/types/console-types.ts +19 -0
  166. package/src/shared/types/posthog-types.ts +131 -0
  167. package/src/shared/types/segment-types.ts +15 -0
  168. package/src/shared/types/types.ts +397 -0
  169. package/src/shared/types/vercel-types.ts +19 -0
  170. package/src/shared/utils/config-client.ts +19 -0
  171. package/src/shared/utils/config.ts +250 -0
  172. package/src/shared/utils/emitter-adapter.ts +212 -0
  173. package/src/shared/utils/manager.test.ts +36 -0
  174. package/src/shared/utils/manager.ts +1322 -0
  175. package/src/shared/utils/posthog-bootstrap.ts +136 -0
  176. package/src/shared/utils/posthog-client-utils.ts +48 -0
  177. package/src/shared/utils/posthog-next-utils.ts +282 -0
  178. package/src/shared/utils/posthog-server-utils.ts +210 -0
  179. package/src/shared/utils/rate-limit.ts +289 -0
  180. package/src/shared/utils/security.ts +545 -0
  181. package/src/shared/utils/validation-client.ts +161 -0
  182. package/src/shared/utils/validation.ts +399 -0
  183. package/src/shared.ts +155 -0
  184. package/src/types/index.ts +62 -0
@@ -0,0 +1,152 @@
1
+ /**
2
+ * @fileoverview Console provider - works in both client and server environments
3
+ *
4
+ * Provides console-based analytics logging for development and debugging.
5
+ * Useful for testing analytics events without sending data to external services.
6
+ *
7
+ * @remarks
8
+ * This provider:
9
+ * - Logs analytics events to console
10
+ * - Works in both browser and Node.js environments
11
+ * - Useful for development and debugging
12
+ * - Integrates with observability package for structured logging
13
+ *
14
+ * @module @od-oneapp/analytics/providers/console
15
+ */
16
+
17
+ import { logDebug, logWarn } from '@repo/shared/logger';
18
+
19
+ import type { ConsoleConfig } from './types';
20
+ import type { AnalyticsProvider, ProviderConfig } from '../../shared/types/types';
21
+
22
+ export class ConsoleProvider implements AnalyticsProvider {
23
+ readonly name = 'console';
24
+ private config: ConsoleConfig;
25
+ private isInitialized = false;
26
+
27
+ constructor(config: ProviderConfig) {
28
+ const options = config.options as ConsoleConfig | undefined;
29
+ this.config = {
30
+ prefix: '[Analytics]',
31
+ logLevel: 'info',
32
+ colorize: false,
33
+ ...options,
34
+ };
35
+ }
36
+
37
+ async initialize(): Promise<void> {
38
+ if (this.isInitialized) return;
39
+
40
+ // Reduce noisy logs: only log in development or explicit debug
41
+ const isDev = process.env.NODE_ENV !== 'production';
42
+ const isDebug = process.env.NEXT_PUBLIC_OBSERVABILITY_DEBUG === 'true';
43
+ if (isDev || isDebug) {
44
+ this.log('Console Analytics Provider initialized', {
45
+ config: this.config,
46
+ });
47
+ }
48
+
49
+ this.isInitialized = true;
50
+ }
51
+
52
+ async track(event: string, properties: any = {}, _context?: any): Promise<void> {
53
+ if (!this.isInitialized) {
54
+ logWarn('Console provider not initialized', {
55
+ provider: 'console',
56
+ operation: 'track',
57
+ event,
58
+ });
59
+ return;
60
+ }
61
+
62
+ this.log('TRACK', {
63
+ event,
64
+ properties,
65
+ timestamp: new Date().toISOString(),
66
+ });
67
+ }
68
+
69
+ async identify(userId: string, traits: any = {}, _context?: any): Promise<void> {
70
+ if (!this.isInitialized) {
71
+ logWarn('Console provider not initialized', {
72
+ provider: 'console',
73
+ operation: 'identify',
74
+ userId,
75
+ });
76
+ return;
77
+ }
78
+
79
+ this.log('IDENTIFY', {
80
+ timestamp: new Date().toISOString(),
81
+ traits,
82
+ userId,
83
+ });
84
+ }
85
+
86
+ async page(name?: string, properties: any = {}, _context?: any): Promise<void> {
87
+ if (!this.isInitialized) {
88
+ logWarn('Console provider not initialized', {
89
+ provider: 'console',
90
+ operation: 'page',
91
+ metadata: { name },
92
+ });
93
+ return;
94
+ }
95
+
96
+ this.log('PAGE', {
97
+ name,
98
+ properties,
99
+ timestamp: new Date().toISOString(),
100
+ });
101
+ }
102
+
103
+ async group(groupId: string, traits: any = {}, _context?: any): Promise<void> {
104
+ if (!this.isInitialized) {
105
+ logWarn('Console provider not initialized', {
106
+ provider: 'console',
107
+ operation: 'group',
108
+ metadata: { groupId },
109
+ });
110
+ return;
111
+ }
112
+
113
+ this.log('GROUP', {
114
+ groupId,
115
+ timestamp: new Date().toISOString(),
116
+ traits,
117
+ });
118
+ }
119
+
120
+ async alias(userId: string, previousId: string, _context?: any): Promise<void> {
121
+ if (!this.isInitialized) {
122
+ logWarn('Console provider not initialized', {
123
+ provider: 'console',
124
+ operation: 'alias',
125
+ userId,
126
+ metadata: { previousId },
127
+ });
128
+ return;
129
+ }
130
+
131
+ this.log('ALIAS', {
132
+ previousId,
133
+ timestamp: new Date().toISOString(),
134
+ userId,
135
+ });
136
+ }
137
+
138
+ private log(action: string, data: any): void {
139
+ const { logLevel, prefix } = this.config;
140
+
141
+ if (logLevel === 'error') return; // Don't log analytics in error-only mode
142
+
143
+ const message = `${prefix} ${action}`;
144
+
145
+ // Use analytics logger instead of console directly
146
+ logDebug(message, {
147
+ provider: 'console',
148
+ operation: action.toLowerCase(),
149
+ metadata: data,
150
+ });
151
+ }
152
+ }
@@ -0,0 +1,6 @@
1
+ /**
2
+ * @fileoverview Console provider for server/Node.js environments
3
+ * Console provider for server/Node.js environments
4
+ */
5
+
6
+ export { ConsoleProvider } from '.';
@@ -0,0 +1,15 @@
1
+ /**
2
+ * @fileoverview Console provider type definitions
3
+ *
4
+ * Defines TypeScript types and interfaces for Console analytics provider configuration.
5
+ *
6
+ * @module @od-oneapp/analytics/providers/console/types
7
+ */
8
+
9
+ export interface ConsoleConfig {
10
+ // Console provider doesn't need configuration
11
+ // All options are optional
12
+ prefix?: string;
13
+ logLevel?: 'debug' | 'info' | 'warn' | 'error';
14
+ colorize?: boolean;
15
+ }
@@ -0,0 +1,464 @@
1
+ /**
2
+ * @fileoverview HTTP provider for browser/client environments
3
+ *
4
+ * Provides HTTP-based analytics event sending for client-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, batch size, or page unload
10
+ * - Retry with exponential backoff
11
+ * - Anonymous ID persistence in localStorage
12
+ * - Offline queue support (events queued when offline)
13
+ *
14
+ * @module @od-oneapp/analytics/providers/http/client
15
+ */
16
+
17
+ import { logInfo, logWarn, logError } from '@repo/shared/logs';
18
+
19
+ import type { HttpProviderConfig, IngestionResponse, QueuedEvent } from './types';
20
+ import type {
21
+ AnalyticsContext,
22
+ AnalyticsProvider,
23
+ GroupTraits,
24
+ PageProperties,
25
+ Properties,
26
+ ProviderConfig,
27
+ UserTraits,
28
+ } from '../../shared/types/types';
29
+
30
+ /** Default configuration values */
31
+ const DEFAULTS = {
32
+ batchSize: 10,
33
+ flushInterval: 5000,
34
+ timeout: 10000,
35
+ retries: 3,
36
+ } as const;
37
+
38
+ /** Base delay for exponential backoff in ms */
39
+ const BACKOFF_BASE_MS = 1000;
40
+
41
+ /** LocalStorage key for anonymous ID */
42
+ const ANON_ID_KEY = 'analytics_anonymous_id';
43
+
44
+ /**
45
+ * HTTP Analytics Provider for browser environments.
46
+ *
47
+ * Sends analytics events to a remote endpoint via HTTP POST requests.
48
+ * Events are batched and flushed periodically for efficiency.
49
+ *
50
+ * @example
51
+ * ```typescript
52
+ * const provider = new HttpClientProvider({
53
+ * options: {
54
+ * endpoint: 'https://api.oneapp.dev/v1/ingest',
55
+ * apiKey: process.env.NEXT_PUBLIC_ANALYTICS_API_KEY,
56
+ * },
57
+ * });
58
+ * await provider.initialize();
59
+ * await provider.track('Button Clicked', { button: 'signup' });
60
+ * ```
61
+ */
62
+ export class HttpClientProvider implements AnalyticsProvider {
63
+ readonly name = 'http';
64
+
65
+ private config: Required<
66
+ Pick<HttpProviderConfig, 'batchSize' | 'flushInterval' | 'timeout' | 'retries'>
67
+ > &
68
+ HttpProviderConfig;
69
+ private isInitialized = false;
70
+ private queue: QueuedEvent[] = [];
71
+ private flushTimer: ReturnType<typeof setInterval> | null = null;
72
+ private userId?: string;
73
+ private anonymousId: string;
74
+ private isFlushing = false;
75
+
76
+ constructor(providerConfig: ProviderConfig) {
77
+ const options = (providerConfig.options ?? {}) as unknown as HttpProviderConfig;
78
+
79
+ if (!options.endpoint) {
80
+ throw new Error('HttpProvider requires an endpoint URL');
81
+ }
82
+
83
+ this.config = {
84
+ ...options,
85
+ batchSize: options.batchSize ?? DEFAULTS.batchSize,
86
+ flushInterval: options.flushInterval ?? DEFAULTS.flushInterval,
87
+ timeout: options.timeout ?? DEFAULTS.timeout,
88
+ retries: options.retries ?? DEFAULTS.retries,
89
+ };
90
+
91
+ this.userId = options.userId;
92
+ this.anonymousId = options.anonymousId ?? this.getOrCreateAnonymousId();
93
+ }
94
+
95
+ async initialize(): Promise<void> {
96
+ if (this.isInitialized) return;
97
+
98
+ // Start auto-flush timer if interval > 0
99
+ if (this.config.flushInterval > 0) {
100
+ this.flushTimer = setInterval(() => {
101
+ void this.flush();
102
+ }, this.config.flushInterval);
103
+ }
104
+
105
+ // Flush on page unload/visibility change
106
+ if (typeof document !== 'undefined') {
107
+ document.addEventListener('visibilitychange', this.handleVisibilityChange);
108
+ }
109
+
110
+ if (typeof window !== 'undefined') {
111
+ window.addEventListener('beforeunload', this.handleBeforeUnload);
112
+ }
113
+
114
+ if (this.config.debug) {
115
+ this.log('HTTP Analytics Provider initialized', {
116
+ endpoint: this.config.endpoint,
117
+ batchSize: this.config.batchSize,
118
+ flushInterval: this.config.flushInterval,
119
+ });
120
+ }
121
+
122
+ this.isInitialized = true;
123
+ }
124
+
125
+ async track(
126
+ event: string,
127
+ properties: Properties = {},
128
+ _context?: AnalyticsContext,
129
+ ): Promise<void> {
130
+ if (!this.isInitialized) {
131
+ this.warn('HTTP provider not initialized', { operation: 'track', event });
132
+ return;
133
+ }
134
+
135
+ this.enqueue({
136
+ type: 'track',
137
+ event,
138
+ properties,
139
+ timestamp: new Date().toISOString(),
140
+ userId: this.userId,
141
+ anonymousId: this.anonymousId,
142
+ });
143
+ }
144
+
145
+ async identify(
146
+ userId: string,
147
+ traits: UserTraits = {},
148
+ _context?: AnalyticsContext,
149
+ ): Promise<void> {
150
+ if (!this.isInitialized) {
151
+ this.warn('HTTP provider not initialized', { operation: 'identify', userId });
152
+ return;
153
+ }
154
+
155
+ // Update stored user ID
156
+ this.userId = userId;
157
+
158
+ this.enqueue({
159
+ type: 'identify',
160
+ userId,
161
+ traits,
162
+ timestamp: new Date().toISOString(),
163
+ anonymousId: this.anonymousId,
164
+ });
165
+ }
166
+
167
+ async page(
168
+ name?: string,
169
+ properties: PageProperties = {},
170
+ _context?: AnalyticsContext,
171
+ ): Promise<void> {
172
+ if (!this.isInitialized) {
173
+ this.warn('HTTP provider not initialized', { operation: 'page', name });
174
+ return;
175
+ }
176
+
177
+ this.enqueue({
178
+ type: 'page',
179
+ name,
180
+ properties,
181
+ timestamp: new Date().toISOString(),
182
+ userId: this.userId,
183
+ anonymousId: this.anonymousId,
184
+ });
185
+ }
186
+
187
+ async group(
188
+ groupId: string,
189
+ traits: GroupTraits = {},
190
+ _context?: AnalyticsContext,
191
+ ): Promise<void> {
192
+ if (!this.isInitialized) {
193
+ this.warn('HTTP provider not initialized', { operation: 'group', groupId });
194
+ return;
195
+ }
196
+
197
+ this.enqueue({
198
+ type: 'group',
199
+ groupId,
200
+ traits,
201
+ timestamp: new Date().toISOString(),
202
+ userId: this.userId,
203
+ anonymousId: this.anonymousId,
204
+ });
205
+ }
206
+
207
+ async alias(userId: string, previousId: string, _context?: AnalyticsContext): Promise<void> {
208
+ if (!this.isInitialized) {
209
+ this.warn('HTTP provider not initialized', { operation: 'alias', userId, previousId });
210
+ return;
211
+ }
212
+
213
+ this.enqueue({
214
+ type: 'alias',
215
+ userId,
216
+ previousId,
217
+ timestamp: new Date().toISOString(),
218
+ anonymousId: this.anonymousId,
219
+ });
220
+ }
221
+
222
+ /**
223
+ * Flush all queued events to the remote endpoint.
224
+ * Called automatically on interval, batch size, or page events.
225
+ */
226
+ async flush(): Promise<void> {
227
+ if (this.queue.length === 0 || this.isFlushing) return;
228
+
229
+ this.isFlushing = true;
230
+
231
+ // Take all events from queue
232
+ const events = [...this.queue];
233
+ this.queue = [];
234
+
235
+ try {
236
+ await this.sendBatch(events);
237
+ } catch (error) {
238
+ // Re-queue events on failure (they'll be retried on next flush)
239
+ this.queue.unshift(...events);
240
+ this.error('Failed to flush events, re-queued for retry', {
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 removes event listeners.
252
+ */
253
+ async shutdown(): Promise<void> {
254
+ if (this.flushTimer) {
255
+ clearInterval(this.flushTimer);
256
+ this.flushTimer = null;
257
+ }
258
+
259
+ if (typeof document !== 'undefined') {
260
+ document.removeEventListener('visibilitychange', this.handleVisibilityChange);
261
+ }
262
+
263
+ if (typeof window !== 'undefined') {
264
+ window.removeEventListener('beforeunload', this.handleBeforeUnload);
265
+ }
266
+
267
+ // Final flush
268
+ await this.flush();
269
+ this.isInitialized = false;
270
+ }
271
+
272
+ /**
273
+ * Get the current queue length (for testing/monitoring).
274
+ */
275
+ getQueueLength(): number {
276
+ return this.queue.length;
277
+ }
278
+
279
+ // ============================================================================
280
+ // Event Handlers
281
+ // ============================================================================
282
+
283
+ private handleVisibilityChange = (): void => {
284
+ if (document.visibilityState === 'hidden') {
285
+ void this.flush();
286
+ }
287
+ };
288
+
289
+ private handleBeforeUnload = (): void => {
290
+ // Use sendBeacon for reliable delivery on page unload
291
+ if (this.queue.length > 0 && typeof navigator?.sendBeacon === 'function') {
292
+ const payload = JSON.stringify({ batch: this.queue });
293
+ const headers: Record<string, string> = {
294
+ type: 'application/json',
295
+ };
296
+
297
+ // Note: sendBeacon doesn't support custom headers, so we include API key in payload
298
+ // The server should accept it from either header or body
299
+ const blob = new Blob([payload], headers);
300
+ navigator.sendBeacon(this.config.endpoint, blob);
301
+ this.queue = [];
302
+ }
303
+ };
304
+
305
+ // ============================================================================
306
+ // Private Methods
307
+ // ============================================================================
308
+
309
+ private enqueue(event: QueuedEvent): void {
310
+ this.queue.push(event);
311
+
312
+ if (this.config.debug) {
313
+ this.log('Event queued', {
314
+ type: event.type,
315
+ queueLength: this.queue.length,
316
+ });
317
+ }
318
+
319
+ // Flush if batch size reached
320
+ if (this.queue.length >= this.config.batchSize) {
321
+ void this.flush();
322
+ }
323
+ }
324
+
325
+ private async sendBatch(events: QueuedEvent[]): Promise<void> {
326
+ const payload = { batch: events };
327
+
328
+ let lastError: Error | null = null;
329
+
330
+ for (let attempt = 0; attempt <= this.config.retries; attempt++) {
331
+ try {
332
+ const response = await this.sendRequest(payload);
333
+
334
+ if (response.success) {
335
+ if (this.config.debug) {
336
+ this.log('Batch sent successfully', {
337
+ accepted: response.accepted,
338
+ rejected: response.rejected,
339
+ });
340
+ }
341
+ return;
342
+ }
343
+
344
+ // Request succeeded but ingestion failed
345
+ this.warn('Batch partially rejected', {
346
+ accepted: response.accepted,
347
+ rejected: response.rejected,
348
+ error: response.error,
349
+ });
350
+ return;
351
+ } catch (error) {
352
+ lastError = error instanceof Error ? error : new Error(String(error));
353
+
354
+ if (attempt < this.config.retries) {
355
+ // Exponential backoff: 1s, 2s, 4s, ...
356
+ const delay = BACKOFF_BASE_MS * Math.pow(2, attempt);
357
+
358
+ if (this.config.debug) {
359
+ this.log('Retrying batch send', {
360
+ attempt: attempt + 1,
361
+ maxRetries: this.config.retries,
362
+ delayMs: delay,
363
+ });
364
+ }
365
+
366
+ await this.sleep(delay);
367
+ }
368
+ }
369
+ }
370
+
371
+ // All retries exhausted
372
+ throw lastError ?? new Error('Failed to send batch after retries');
373
+ }
374
+
375
+ private async sendRequest(payload: { batch: QueuedEvent[] }): Promise<IngestionResponse> {
376
+ const headers: Record<string, string> = {
377
+ 'Content-Type': 'application/json',
378
+ ...this.config.headers,
379
+ };
380
+
381
+ if (this.config.apiKey) {
382
+ headers['X-API-Key'] = this.config.apiKey;
383
+ }
384
+
385
+ const controller = new AbortController();
386
+ const timeoutId = setTimeout(() => controller.abort(), this.config.timeout);
387
+
388
+ try {
389
+ const response = await fetch(this.config.endpoint, {
390
+ method: 'POST',
391
+ headers,
392
+ body: JSON.stringify(payload),
393
+ signal: controller.signal,
394
+ // Include credentials for cross-origin requests if needed
395
+ credentials: 'omit',
396
+ });
397
+
398
+ if (!response.ok) {
399
+ // Handle rate limiting
400
+ if (response.status === 429) {
401
+ const retryAfter = response.headers.get('Retry-After');
402
+ throw new Error(`Rate limited. Retry after ${retryAfter ?? 'unknown'} seconds`);
403
+ }
404
+
405
+ const errorBody = await response.text();
406
+ throw new Error(`HTTP ${response.status}: ${errorBody}`);
407
+ }
408
+
409
+ return (await response.json()) as IngestionResponse;
410
+ } finally {
411
+ clearTimeout(timeoutId);
412
+ }
413
+ }
414
+
415
+ private getOrCreateAnonymousId(): string {
416
+ // Try to get from localStorage
417
+ if (typeof localStorage !== 'undefined') {
418
+ try {
419
+ const stored = localStorage.getItem(ANON_ID_KEY);
420
+ if (stored) return stored;
421
+
422
+ const newId = this.generateAnonymousId();
423
+ localStorage.setItem(ANON_ID_KEY, newId);
424
+ return newId;
425
+ } catch {
426
+ // localStorage not available or blocked
427
+ }
428
+ }
429
+
430
+ return this.generateAnonymousId();
431
+ }
432
+
433
+ private generateAnonymousId(): string {
434
+ // Use crypto.randomUUID if available
435
+ if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') {
436
+ return crypto.randomUUID();
437
+ }
438
+
439
+ // Fallback
440
+ return `anon_${Date.now().toString(36)}${Math.random().toString(36).slice(2, 9)}`;
441
+ }
442
+
443
+ private sleep(ms: number): Promise<void> {
444
+ return new Promise(resolve => setTimeout(resolve, ms));
445
+ }
446
+
447
+ // ============================================================================
448
+ // Logging (browser-compatible)
449
+ // ============================================================================
450
+
451
+ private log(message: string, data?: Record<string, unknown>): void {
452
+ if (this.config.debug) {
453
+ logInfo(`[Analytics:HTTP] ${message}`, data);
454
+ }
455
+ }
456
+
457
+ private warn(message: string, data?: Record<string, unknown>): void {
458
+ logWarn(`[Analytics:HTTP] ${message}`, data);
459
+ }
460
+
461
+ private error(message: string, data?: Record<string, unknown>): void {
462
+ logError(`[Analytics:HTTP] ${message}`, data);
463
+ }
464
+ }
@@ -0,0 +1,30 @@
1
+ /**
2
+ * @fileoverview HTTP Analytics Provider
3
+ *
4
+ * Exports HTTP-based analytics providers for sending events to a remote
5
+ * ingestion endpoint (e.g., oneapp-api's /api/v1/ingest).
6
+ *
7
+ * Use `HttpClientProvider` for browser environments and `HttpServerProvider`
8
+ * for Node.js/server environments.
9
+ *
10
+ * @example
11
+ * ```typescript
12
+ * // Client-side (browser)
13
+ * import { HttpClientProvider } from '@od-oneapp/analytics/providers/http/client';
14
+ *
15
+ * // Server-side (Node.js)
16
+ * import { HttpServerProvider } from '@od-oneapp/analytics/providers/http/server';
17
+ *
18
+ * // Or import types
19
+ * import type { HttpProviderConfig } from '@od-oneapp/analytics/providers/http';
20
+ * ```
21
+ *
22
+ * @module @od-oneapp/analytics/providers/http
23
+ */
24
+
25
+ export type { HttpProviderConfig, IngestionResponse, QueuedEvent } from './types';
26
+ export { HttpClientProvider } from './client';
27
+ export { HttpServerProvider } from './server';
28
+
29
+ // Edge-compatible alias - use HttpServerProvider which works in edge environments
30
+ export { HttpServerProvider as HttpProvider } from './server';