@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,1769 @@
1
+ import { i as page, n as group, o as track, r as identify, t as alias } from "./emitters-6-nKo8i-.mjs";
2
+ import { logWarn } from "@od-oneapp/shared/logger";
3
+
4
+ //#region src/shared/utils/rate-limit.ts
5
+ /**
6
+ * @fileoverview Rate limiting utilities for analytics calls
7
+ * Rate limiting utilities for analytics calls
8
+ *
9
+ * Prevents overwhelming provider APIs, hitting rate limits, and excessive costs.
10
+ * Uses token bucket algorithm for smooth rate limiting with bursts.
11
+ *
12
+ * @module rate-limit
13
+ */
14
+ /**
15
+ * Rate limiter for analytics calls
16
+ *
17
+ * Implements token bucket algorithm with optional call queuing.
18
+ *
19
+ * @example
20
+ * ```typescript
21
+ * const limiter = new RateLimiter({
22
+ * maxCalls: 100,
23
+ * windowMs: 1000, // 100 calls per second
24
+ * maxConcurrent: 10,
25
+ * });
26
+ *
27
+ * // Check if call is allowed
28
+ * if (await limiter.tryAcquire()) {
29
+ * await analytics.track('Event');
30
+ * limiter.release();
31
+ * }
32
+ *
33
+ * // Or use wrapper
34
+ * await limiter.execute(() => analytics.track('Event'));
35
+ * ```
36
+ */
37
+ var RateLimiter = class {
38
+ callCount = 0;
39
+ concurrentCount = 0;
40
+ windowStart = Date.now();
41
+ queue = [];
42
+ config;
43
+ constructor(config) {
44
+ this.config = {
45
+ maxCalls: config.maxCalls,
46
+ windowMs: config.windowMs,
47
+ maxConcurrent: config.maxConcurrent ?? Infinity,
48
+ queueExcess: config.queueExcess ?? false,
49
+ maxQueueSize: config.maxQueueSize ?? 1e3
50
+ };
51
+ }
52
+ /**
53
+ * Try to acquire a rate limit token
54
+ *
55
+ * @returns Promise resolving to whether the call is allowed
56
+ */
57
+ async tryAcquire() {
58
+ const now = Date.now();
59
+ if (now - this.windowStart >= this.config.windowMs) {
60
+ this.callCount = 0;
61
+ this.windowStart = now;
62
+ }
63
+ if (this.callCount >= this.config.maxCalls) {
64
+ if (this.config.queueExcess && this.queue.length < this.config.maxQueueSize) return new Promise((resolve) => {
65
+ this.queue.push(() => {
66
+ const now = Date.now();
67
+ if (now - this.windowStart >= this.config.windowMs) {
68
+ this.callCount = 0;
69
+ this.windowStart = now;
70
+ }
71
+ if (this.callCount < this.config.maxCalls) {
72
+ this.callCount++;
73
+ resolve(true);
74
+ } else resolve(false);
75
+ });
76
+ });
77
+ logWarn("Rate limit exceeded", {
78
+ maxCalls: this.config.maxCalls,
79
+ windowMs: this.config.windowMs,
80
+ currentCount: this.callCount
81
+ });
82
+ return false;
83
+ }
84
+ if (this.concurrentCount >= this.config.maxConcurrent) {
85
+ if (this.config.queueExcess && this.queue.length < this.config.maxQueueSize) return new Promise((resolve) => {
86
+ this.queue.push(() => resolve(true));
87
+ });
88
+ return false;
89
+ }
90
+ this.callCount++;
91
+ this.concurrentCount++;
92
+ return true;
93
+ }
94
+ /**
95
+ * Release a rate limit token
96
+ */
97
+ release() {
98
+ this.concurrentCount = Math.max(0, this.concurrentCount - 1);
99
+ if (this.queue.length > 0 && this.concurrentCount < this.config.maxConcurrent) {
100
+ const now = Date.now();
101
+ if (now - this.windowStart >= this.config.windowMs) {
102
+ this.callCount = 0;
103
+ this.windowStart = now;
104
+ }
105
+ if (this.callCount < this.config.maxCalls) {
106
+ const next = this.queue.shift();
107
+ if (next) {
108
+ this.concurrentCount++;
109
+ next();
110
+ }
111
+ }
112
+ }
113
+ }
114
+ /**
115
+ * Execute a function with rate limiting
116
+ *
117
+ * @param fn - Function to execute
118
+ * @returns Promise resolving to function result
119
+ */
120
+ async execute(fn) {
121
+ if (!await this.tryAcquire()) return null;
122
+ try {
123
+ return await fn();
124
+ } finally {
125
+ this.release();
126
+ }
127
+ }
128
+ /**
129
+ * Get current rate limiter stats
130
+ */
131
+ getStats() {
132
+ return {
133
+ callCount: this.callCount,
134
+ concurrentCount: this.concurrentCount,
135
+ queueSize: this.queue.length,
136
+ remainingCalls: Math.max(0, this.config.maxCalls - this.callCount),
137
+ windowStart: this.windowStart
138
+ };
139
+ }
140
+ /**
141
+ * Reset the rate limiter
142
+ */
143
+ reset() {
144
+ this.callCount = 0;
145
+ this.concurrentCount = 0;
146
+ this.windowStart = Date.now();
147
+ this.queue = [];
148
+ }
149
+ };
150
+
151
+ //#endregion
152
+ //#region src/shared/utils/security.ts
153
+ /**
154
+ * @fileoverview Security and sanitization utilities for analytics data
155
+ *
156
+ * This module provides comprehensive security utilities for analytics data:
157
+ *
158
+ * - **PII Detection**: Detects personally identifiable information (emails, phones, SSNs, credit cards, IPs)
159
+ * - **PII Redaction**: Redacts PII from strings before sending to analytics
160
+ * - **XSS Protection**: Strips HTML and script tags to prevent XSS attacks
161
+ * - **Property Validation**: Validates property keys to prevent prototype pollution
162
+ * - **Payload Sanitization**: Comprehensive sanitization with size limits and depth checks
163
+ * - **Size Limits**: Enforces maximum property and payload sizes
164
+ *
165
+ * **Security Features**:
166
+ * - Maximum property value size: 100KB
167
+ * - Maximum payload size: 1MB
168
+ * - Maximum nesting depth: 10 levels
169
+ * - Dangerous key filtering (prevents prototype pollution)
170
+ * - HTML/script tag removal
171
+ *
172
+ * @module @od-oneapp/analytics/shared/utils/security
173
+ */
174
+ /**
175
+ * Maximum allowed property value size (100KB).
176
+ *
177
+ * Prevents sending excessively large property values to analytics providers.
178
+ *
179
+ * @internal
180
+ */
181
+ const MAX_PROPERTY_VALUE_SIZE = 100 * 1024;
182
+ /**
183
+ * Maximum allowed total payload size (1MB).
184
+ *
185
+ * Prevents sending excessively large payloads to analytics providers.
186
+ *
187
+ * @internal
188
+ */
189
+ const MAX_PAYLOAD_SIZE = 1024 * 1024;
190
+ /**
191
+ * Maximum allowed object nesting depth.
192
+ *
193
+ * Prevents deeply nested objects that could cause performance issues or stack overflows.
194
+ *
195
+ * @internal
196
+ */
197
+ const MAX_DEPTH = 10;
198
+ /**
199
+ * PII patterns for detection and filtering
200
+ * Note: These regexes are intentionally complex for accurate PII detection.
201
+ * They are used only on bounded, sanitized input with size limits.
202
+ */
203
+ const PII_PATTERNS = {
204
+ email: /\b[\w%+.-]+@[\d.A-Za-z-]+\.[A-Za-z|]{2,}\b/g,
205
+ phone: /\b(?:\+?1[.-]?)?\(?(\d{3})\)?[.-]?(\d{3})[.-]?(\d{4})\b/g,
206
+ ssn: /\b\d{3}-?\d{2}-?\d{4}\b/g,
207
+ creditCard: /\b(?:\d{4}[ -]?){3}\d{4}\b/g,
208
+ ipv4: /\b(?:\d{1,3}\.){3}\d{1,3}\b/g,
209
+ ipv6: /\b(?:[\da-f]{1,4}:){7}[\da-f]{1,4}\b/gi
210
+ };
211
+ /**
212
+ * Dangerous property keys that should never be allowed
213
+ */
214
+ const DANGEROUS_KEYS = new Set([
215
+ "__proto__",
216
+ "constructor",
217
+ "prototype",
218
+ "__defineGetter__",
219
+ "__defineSetter__",
220
+ "__lookupGetter__",
221
+ "__lookupSetter__"
222
+ ]);
223
+ /**
224
+ * HTML/Script patterns for XSS protection
225
+ * Note: These regexes are intentionally complex for accurate XSS detection.
226
+ * They are used only on bounded, sanitized input with size limits.
227
+ */
228
+ const XSS_PATTERNS = {
229
+ scriptTag: /<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi,
230
+ onEvent: /\bon\w+\s*=\s*["'][^"']*["']/gi,
231
+ javascript: /javascript:/gi,
232
+ dataUri: /data:text\/html/gi
233
+ };
234
+ /**
235
+ * Check if a value contains PII (Personally Identifiable Information).
236
+ *
237
+ * Detects common PII patterns including:
238
+ * - Email addresses
239
+ * - Phone numbers
240
+ * - Social Security Numbers
241
+ * - Credit card numbers
242
+ * - IPv4 and IPv6 addresses
243
+ *
244
+ * @param {string} value - String value to check for PII
245
+ * @returns {boolean} `true` if PII is detected, `false` otherwise
246
+ *
247
+ * @example
248
+ * ```typescript
249
+ * if (containsPII(userInput)) {
250
+ * const sanitized = redactPII(userInput);
251
+ * // Use sanitized value
252
+ * }
253
+ * ```
254
+ */
255
+ function containsPII(value) {
256
+ return Object.values(PII_PATTERNS).some((pattern) => pattern.test(value));
257
+ }
258
+ /**
259
+ * Redact PII from a string.
260
+ *
261
+ * Replaces detected PII patterns with `[REDACTED_TYPE]` placeholders.
262
+ * Useful for logging or analytics where PII should not be stored.
263
+ *
264
+ * @param {string} value - String value to redact PII from
265
+ * @returns {string} String with PII redacted
266
+ *
267
+ * @example
268
+ * ```typescript
269
+ * const sanitized = redactPII('Contact john@example.com at 555-1234');
270
+ * // Returns: 'Contact [REDACTED_EMAIL] at [REDACTED_PHONE]'
271
+ * ```
272
+ */
273
+ function redactPII(value) {
274
+ let redacted = value;
275
+ for (const [type, pattern] of Object.entries(PII_PATTERNS)) redacted = redacted.replace(pattern, `[REDACTED_${type.toUpperCase()}]`);
276
+ return redacted;
277
+ }
278
+ /**
279
+ * Strip HTML and script tags from a string.
280
+ *
281
+ * Removes HTML tags, script tags, event handlers, and other potentially dangerous
282
+ * content to prevent XSS attacks. Also removes `data:text/html` URIs.
283
+ *
284
+ * @param {string} value - String value to strip HTML from
285
+ * @returns {string} String with HTML and scripts removed
286
+ *
287
+ * @example
288
+ * ```typescript
289
+ * const sanitized = stripHTML('<script>alert("xss")<\/script>Hello');
290
+ * // Returns: 'Hello'
291
+ * ```
292
+ */
293
+ function stripHTML(value) {
294
+ let stripped = value;
295
+ for (const pattern of Object.values(XSS_PATTERNS)) stripped = stripped.replace(pattern, "");
296
+ stripped = stripped.replaceAll(/<[^>]*>/g, "");
297
+ return stripped;
298
+ }
299
+ /**
300
+ * Validate that a property key is safe.
301
+ *
302
+ * Checks for dangerous keys that could cause prototype pollution or other
303
+ * security issues. Also validates against allowed key patterns if provided.
304
+ *
305
+ * @param {string} key - Property key to validate
306
+ * @param {SanitizationOptions} [options] - Sanitization options
307
+ * @returns {{ valid: boolean; reason?: string }} Validation result with reason if invalid
308
+ *
309
+ * @example
310
+ * ```typescript
311
+ * const result = isValidPropertyKey('__proto__');
312
+ * if (!result.valid) {
313
+ * console.warn('Unsafe key:', result.reason);
314
+ * }
315
+ * ```
316
+ */
317
+ function isValidPropertyKey(key, options = {}) {
318
+ if (!options.allowDangerousKeys && DANGEROUS_KEYS.has(key)) return {
319
+ valid: false,
320
+ reason: "Dangerous key that could cause prototype pollution"
321
+ };
322
+ if (options.allowedKeyPattern && !options.allowedKeyPattern.test(key)) return {
323
+ valid: false,
324
+ reason: "Key does not match allowed pattern"
325
+ };
326
+ if (/[<>[\\\]{}]/.test(key)) return {
327
+ valid: false,
328
+ reason: "Key contains potentially dangerous characters"
329
+ };
330
+ return { valid: true };
331
+ }
332
+ /**
333
+ * Get the size of a value in bytes
334
+ */
335
+ function getValueSize(value) {
336
+ if (typeof value === "string") return new Blob([value]).size;
337
+ try {
338
+ return new Blob([JSON.stringify(value)]).size;
339
+ } catch {
340
+ return 0;
341
+ }
342
+ }
343
+ /**
344
+ * Sanitize a single property value
345
+ */
346
+ function sanitizeValue(value, options, warnings) {
347
+ if (value === null || value === void 0) return {
348
+ value,
349
+ modified: false
350
+ };
351
+ let modified = false;
352
+ if (typeof value === "string") {
353
+ let sanitized = value;
354
+ const size = getValueSize(value);
355
+ const maxSize = options.maxPropertySize ?? MAX_PROPERTY_VALUE_SIZE;
356
+ if (size > maxSize) {
357
+ sanitized = value.slice(0, Math.max(0, maxSize));
358
+ warnings.push(`String value truncated from ${size} to ${maxSize} bytes`);
359
+ modified = true;
360
+ }
361
+ if (options.stripPII && containsPII(sanitized)) {
362
+ sanitized = redactPII(sanitized);
363
+ warnings.push("PII detected and redacted from value");
364
+ modified = true;
365
+ }
366
+ if (options.stripHTML) {
367
+ const stripped = stripHTML(sanitized);
368
+ if (stripped !== sanitized) {
369
+ sanitized = stripped;
370
+ warnings.push("HTML/scripts stripped from value");
371
+ modified = true;
372
+ }
373
+ }
374
+ return {
375
+ value: sanitized,
376
+ modified
377
+ };
378
+ }
379
+ if (typeof value !== "object") return {
380
+ value,
381
+ modified: false
382
+ };
383
+ return {
384
+ value,
385
+ modified: false
386
+ };
387
+ }
388
+ /**
389
+ * Sanitize analytics properties recursively.
390
+ *
391
+ * Comprehensive sanitization of analytics properties including:
392
+ * - Size limit enforcement
393
+ * - Depth limit enforcement
394
+ * - PII detection and redaction
395
+ * - HTML/script tag removal
396
+ * - Dangerous key filtering
397
+ * - Circular reference detection
398
+ *
399
+ * @param {T} properties - Properties to sanitize
400
+ * @param {SanitizationOptions} [options] - Sanitization options
401
+ * @returns {SanitizationResult<T>} Sanitization result with sanitized data and warnings
402
+ *
403
+ * @throws {Error} If properties contain circular references or exceed payload size limit
404
+ *
405
+ * @example
406
+ * ```typescript
407
+ * const result = sanitizeProperties({
408
+ * user_email: 'test@example.com',
409
+ * phone: '555-1234',
410
+ * description: '<script>alert("xss")<\/script>Hello',
411
+ * }, {
412
+ * stripPII: true,
413
+ * stripHTML: true,
414
+ * });
415
+ *
416
+ * console.log(result.data);
417
+ * // {
418
+ * // user_email: '[REDACTED_EMAIL]',
419
+ * // phone: '[REDACTED_PHONE]',
420
+ * // description: 'Hello'
421
+ * // }
422
+ * ```
423
+ */
424
+ function sanitizeProperties(properties, options = {}) {
425
+ const warnings = [];
426
+ let modified = false;
427
+ try {
428
+ JSON.stringify(properties);
429
+ } catch {
430
+ throw new Error("Properties contain circular references");
431
+ }
432
+ const totalSize = getValueSize(properties);
433
+ const maxPayloadSize = options.maxPayloadSize ?? MAX_PAYLOAD_SIZE;
434
+ if (totalSize > maxPayloadSize) throw new Error(`Payload size ${totalSize} bytes exceeds maximum ${maxPayloadSize} bytes`);
435
+ /**
436
+ * Recursive sanitization helper
437
+ */
438
+ function sanitizeRecursive(obj, depth = 0) {
439
+ const maxDepth = options.maxDepth ?? MAX_DEPTH;
440
+ if (depth > maxDepth) {
441
+ warnings.push(`Maximum nesting depth ${maxDepth} exceeded, truncating`);
442
+ modified = true;
443
+ return {};
444
+ }
445
+ const sanitized = {};
446
+ for (const [key, value] of Object.entries(obj)) {
447
+ const keyValidation = isValidPropertyKey(key, options);
448
+ if (!keyValidation.valid) {
449
+ warnings.push(`Skipping dangerous key "${key}": ${keyValidation.reason}`);
450
+ modified = true;
451
+ continue;
452
+ }
453
+ if (value === null || value === void 0) {
454
+ sanitized[key] = value;
455
+ continue;
456
+ }
457
+ if (Array.isArray(value)) {
458
+ sanitized[key] = value.map((item) => {
459
+ if (typeof item === "object" && item !== null) return sanitizeRecursive(item, depth + 1);
460
+ const result = sanitizeValue(item, options, warnings);
461
+ if (result.modified) modified = true;
462
+ return result.value;
463
+ });
464
+ continue;
465
+ }
466
+ if (typeof value === "object") {
467
+ sanitized[key] = sanitizeRecursive(value, depth + 1);
468
+ continue;
469
+ }
470
+ const result = sanitizeValue(value, options, warnings);
471
+ if (result.modified) modified = true;
472
+ sanitized[key] = result.value;
473
+ }
474
+ return sanitized;
475
+ }
476
+ const sanitized = sanitizeRecursive(properties);
477
+ if (warnings.length > 0 && true) logWarn("Analytics properties sanitization warnings", { warnings });
478
+ return {
479
+ data: sanitized,
480
+ modified,
481
+ warnings
482
+ };
483
+ }
484
+ /**
485
+ * Validate event name.
486
+ *
487
+ * Checks that an event name is safe and follows best practices:
488
+ * - Non-empty string
489
+ * - Not exceeding 255 characters
490
+ * - No XSS patterns
491
+ *
492
+ * @param {string} event - Event name to validate
493
+ * @returns {{ valid: boolean; reason?: string }} Validation result with reason if invalid
494
+ *
495
+ * @example
496
+ * ```typescript
497
+ * const result = validateEventName('Button Clicked');
498
+ * if (!result.valid) {
499
+ * console.error('Invalid event name:', result.reason);
500
+ * }
501
+ * ```
502
+ */
503
+ function validateEventName(event) {
504
+ if (!event || typeof event !== "string") return {
505
+ valid: false,
506
+ reason: "Event name must be a non-empty string"
507
+ };
508
+ if (event.trim() === "") return {
509
+ valid: false,
510
+ reason: "Event name cannot be empty or whitespace only"
511
+ };
512
+ if (event.length > 255) return {
513
+ valid: false,
514
+ reason: "Event name cannot exceed 255 characters"
515
+ };
516
+ if (Object.values(XSS_PATTERNS).some((pattern) => pattern.test(event))) return {
517
+ valid: false,
518
+ reason: "Event name contains potentially malicious content"
519
+ };
520
+ return { valid: true };
521
+ }
522
+
523
+ //#endregion
524
+ //#region src/shared/utils/manager.ts
525
+ /**
526
+ * @fileoverview Analytics Manager - Core orchestration for multi-provider analytics
527
+ *
528
+ * This module provides the core AnalyticsManager class that orchestrates multiple
529
+ * analytics providers. It includes:
530
+ *
531
+ * - **Multi-Provider Support**: Manages multiple providers simultaneously
532
+ * - **Event Deduplication**: LRU cache prevents duplicate events
533
+ * - **Rate Limiting**: Per-provider rate limiting (100 calls/second default)
534
+ * - **Batch Processing**: Concurrent batch processing with configurable concurrency
535
+ * - **Performance Metrics**: Tracks provider performance and reliability
536
+ * - **Error Handling**: Comprehensive error handling with graceful degradation
537
+ *
538
+ * **Node.js 22+ Features Used**:
539
+ * - `Promise.withResolvers()`: External promise control for complex workflows
540
+ * - `AbortSignal.timeout()`: Context-aware timeouts for provider operations
541
+ * - High-resolution timing: Precise performance measurement (`process.hrtime.bigint()`)
542
+ * - `Object.hasOwn()`: Safer property existence checks
543
+ *
544
+ * **Optimizations**:
545
+ * - Spread operator instead of `structuredClone()` for better performance
546
+ * - LRU cache for event deduplication (prevents memory leaks)
547
+ * - Concurrent batch processing with configurable concurrency
548
+ * - Comprehensive input sanitization and validation
549
+ * - Per-provider rate limiting
550
+ *
551
+ * @module @od-oneapp/analytics/shared/utils/manager
552
+ */
553
+ /**
554
+ * LRU Cache for event deduplication.
555
+ *
556
+ * Prevents memory leaks by limiting cache size. Uses least-recently-used eviction
557
+ * policy to maintain bounded memory usage.
558
+ *
559
+ * @template K - Cache key type
560
+ * @template V - Cache value type
561
+ *
562
+ * @internal
563
+ */
564
+ var LRUCache = class {
565
+ cache = /* @__PURE__ */ new Map();
566
+ maxSize;
567
+ constructor(maxSize = 1e3) {
568
+ this.maxSize = maxSize;
569
+ }
570
+ get(key) {
571
+ const value = this.cache.get(key);
572
+ if (value !== void 0) {
573
+ this.cache.delete(key);
574
+ this.cache.set(key, value);
575
+ }
576
+ return value;
577
+ }
578
+ set(key, value) {
579
+ if (this.cache.has(key)) this.cache.delete(key);
580
+ this.cache.set(key, value);
581
+ if (this.cache.size > this.maxSize) {
582
+ const firstKey = this.cache.keys().next().value;
583
+ if (firstKey !== void 0) this.cache.delete(firstKey);
584
+ }
585
+ }
586
+ has(key) {
587
+ return this.cache.get(key) !== void 0;
588
+ }
589
+ clear() {
590
+ this.cache.clear();
591
+ }
592
+ get size() {
593
+ return this.cache.size;
594
+ }
595
+ };
596
+ /**
597
+ * Analytics Manager - Main entry point for analytics tracking.
598
+ *
599
+ * Orchestrates multiple analytics providers, handles event deduplication,
600
+ * rate limiting, batch processing, and error handling.
601
+ *
602
+ * **Key Features**:
603
+ * - Multi-provider support (PostHog, Segment, Vercel, Console)
604
+ * - Event deduplication via LRU cache
605
+ * - Rate limiting (100 calls/second per provider)
606
+ * - Batch processing with concurrency control
607
+ * - Performance metrics tracking
608
+ * - Graceful error handling
609
+ *
610
+ * **Usage**:
611
+ * ```typescript
612
+ * const manager = new AnalyticsManager(config, providerRegistry);
613
+ * await manager.initialize();
614
+ * await manager.emit(track('Event Name', { property: 'value' }));
615
+ * ```
616
+ */
617
+ var AnalyticsManager = class {
618
+ providers = /* @__PURE__ */ new Map();
619
+ context = {};
620
+ isInitialized = false;
621
+ providerMetrics = /* @__PURE__ */ new Map();
622
+ eventCache = new LRUCache(1e3);
623
+ rateLimiter = new RateLimiter({
624
+ maxCalls: 100,
625
+ windowMs: 1e3,
626
+ maxConcurrent: 10,
627
+ queueExcess: false
628
+ });
629
+ constructor(config, availableProviders) {
630
+ this.config = config;
631
+ this.availableProviders = availableProviders;
632
+ }
633
+ /**
634
+ * Initialize all configured providers
635
+ * Throws error if all providers fail to initialize
636
+ */
637
+ async initialize() {
638
+ if (this.isInitialized) return;
639
+ const initPromises = [];
640
+ const initStartTime = process.hrtime.bigint();
641
+ for (const [providerName, providerConfig] of Object.entries(this.config.providers)) {
642
+ const providerFactory = this.availableProviders[providerName];
643
+ if (providerFactory) try {
644
+ const provider = providerFactory(providerConfig);
645
+ this.providers.set(providerName, provider);
646
+ initPromises.push((async () => {
647
+ const providerInitStart = typeof process.hrtime?.bigint === "function" ? process.hrtime.bigint() : BigInt(Date.now() * 1e6);
648
+ try {
649
+ const timeoutSignal = AbortSignal.timeout(1e4);
650
+ const initPromise = provider.initialize(providerConfig);
651
+ await Promise.race([initPromise, new Promise((_resolve, reject) => {
652
+ timeoutSignal.addEventListener("abort", () => reject(/* @__PURE__ */ new Error(`Provider ${providerName} initialization timed out`)));
653
+ })]);
654
+ const now = typeof process.hrtime?.bigint === "function" ? process.hrtime.bigint() : BigInt(Date.now() * 1e6);
655
+ this.providerMetrics.set(providerName, {
656
+ initTime: now - providerInitStart,
657
+ callCount: 0,
658
+ errorCount: 0,
659
+ lastUsed: now
660
+ });
661
+ } catch (error) {
662
+ if (this.config.onError) this.config.onError(error, {
663
+ provider: providerName,
664
+ method: "initialize"
665
+ });
666
+ this.providers.delete(providerName);
667
+ throw error;
668
+ }
669
+ })());
670
+ } catch (error) {
671
+ if (this.config.onError) this.config.onError(error, {
672
+ provider: providerName,
673
+ method: "create"
674
+ });
675
+ }
676
+ else if (this.config.debug && this.config.onInfo) this.config.onInfo(`Provider ${providerName} not available in this environment`);
677
+ }
678
+ const results = await Promise.allSettled(initPromises);
679
+ const initEndTime = process.hrtime.bigint();
680
+ const successCount = results.filter((r) => r.status === "fulfilled").length;
681
+ const failureCount = results.filter((r) => r.status === "rejected").length;
682
+ if (successCount === 0 && initPromises.length > 0) throw new Error(`Analytics initialization failed: All ${initPromises.length} providers failed to initialize`);
683
+ this.isInitialized = true;
684
+ if (this.config.debug && this.config.onInfo) {
685
+ const initTimeMs = Number(initEndTime - initStartTime) / 1e6;
686
+ this.config.onInfo(`Analytics initialized in ${initTimeMs.toFixed(2)}ms with ${successCount} providers (${failureCount} failed): ${[...this.providers.keys()].join(", ")}`);
687
+ }
688
+ }
689
+ /**
690
+ * Set global analytics context
691
+ * FIX #13: Use spread operator instead of structuredClone for better performance
692
+ */
693
+ setContext(context) {
694
+ this.context = {
695
+ ...this.context,
696
+ ...context
697
+ };
698
+ for (const [providerName, provider] of this.providers) if (provider.setContext) try {
699
+ provider.setContext({ ...this.context });
700
+ const metrics = this.providerMetrics.get(providerName);
701
+ if (metrics) metrics.lastUsed = process.hrtime.bigint();
702
+ } catch (error) {
703
+ if (this.config.onError) this.config.onError(error, {
704
+ provider: providerName,
705
+ method: "setContext",
706
+ context: Object.keys(context).join(", ")
707
+ });
708
+ }
709
+ }
710
+ /**
711
+ * Get current analytics context
712
+ * FIX #13: Use spread operator instead of structuredClone
713
+ */
714
+ getContext() {
715
+ return { ...this.context };
716
+ }
717
+ async track(eventOrPayload, properties, options) {
718
+ if (typeof eventOrPayload === "object") {
719
+ const payload = eventOrPayload;
720
+ return this.track(payload.event, payload.properties, {
721
+ ...options,
722
+ context: {
723
+ ...this.context,
724
+ ...payload.context
725
+ }
726
+ });
727
+ }
728
+ const event = eventOrPayload;
729
+ if (!this.isInitialized) {
730
+ if (this.config.onError) this.config.onError(/* @__PURE__ */ new Error("Analytics not initialized"), {
731
+ provider: "analytics",
732
+ event,
733
+ method: "track"
734
+ });
735
+ return;
736
+ }
737
+ const eventValidation = validateEventName(event);
738
+ if (!eventValidation.valid) {
739
+ if (this.config.onError) this.config.onError(/* @__PURE__ */ new Error(`Invalid event name: ${eventValidation.reason}`), {
740
+ provider: "analytics",
741
+ event,
742
+ method: "track"
743
+ });
744
+ return;
745
+ }
746
+ const sanitized = sanitizeProperties(properties ?? {}, {
747
+ stripPII: false,
748
+ stripHTML: true,
749
+ allowDangerousKeys: false
750
+ });
751
+ const targetProviders = this.getTargetProviders(options);
752
+ const enhancedProperties = {
753
+ ...this.context,
754
+ ...sanitized.data
755
+ };
756
+ const promises = [...targetProviders.entries()].map(async ([name, provider]) => {
757
+ if (!await this.rateLimiter.tryAcquire()) {
758
+ if (this.config.debug && this.config.onInfo) this.config.onInfo(`Rate limit exceeded for provider ${name}`);
759
+ if (this.config.onError) this.config.onError(/* @__PURE__ */ new Error(`Rate limit exceeded for provider ${name}`), {
760
+ provider: name,
761
+ event,
762
+ method: "track"
763
+ });
764
+ return;
765
+ }
766
+ try {
767
+ await provider.track(event, enhancedProperties, this.context);
768
+ const metrics = this.providerMetrics.get(name);
769
+ if (metrics) {
770
+ metrics.callCount++;
771
+ metrics.lastUsed = process.hrtime.bigint();
772
+ }
773
+ } catch (error) {
774
+ const metrics = this.providerMetrics.get(name);
775
+ if (metrics) metrics.errorCount++;
776
+ if (this.config.onError) this.config.onError(error, {
777
+ provider: name,
778
+ event,
779
+ method: "track"
780
+ });
781
+ } finally {
782
+ this.rateLimiter.release();
783
+ }
784
+ });
785
+ await Promise.allSettled(promises);
786
+ }
787
+ async identify(userIdOrPayload, traits, options) {
788
+ if (typeof userIdOrPayload === "object") {
789
+ const payload = userIdOrPayload;
790
+ return this.identify(payload.userId, payload.traits, {
791
+ ...options,
792
+ context: {
793
+ ...this.context,
794
+ ...payload.context
795
+ }
796
+ });
797
+ }
798
+ const userId = userIdOrPayload;
799
+ if (!this.isInitialized) {
800
+ if (this.config.onError) this.config.onError(/* @__PURE__ */ new Error("Analytics not initialized"), {
801
+ provider: "analytics",
802
+ method: "identify",
803
+ userId
804
+ });
805
+ return;
806
+ }
807
+ const sanitized = sanitizeProperties(traits ?? {}, {
808
+ stripPII: false,
809
+ stripHTML: true,
810
+ allowDangerousKeys: false
811
+ });
812
+ this.setContext({
813
+ userId,
814
+ ...sanitized.data
815
+ });
816
+ const targetProviders = this.getTargetProviders(options);
817
+ const enhancedTraits = { ...sanitized.data };
818
+ const promises = [...targetProviders.entries()].map(async ([name, provider]) => {
819
+ if (provider.identify) {
820
+ if (!await this.rateLimiter.tryAcquire()) {
821
+ if (this.config.debug && this.config.onInfo) this.config.onInfo(`Rate limit exceeded for provider ${name}`);
822
+ if (this.config.onError) this.config.onError(/* @__PURE__ */ new Error(`Rate limit exceeded for provider ${name}`), {
823
+ provider: name,
824
+ userId,
825
+ method: "identify"
826
+ });
827
+ return;
828
+ }
829
+ try {
830
+ await provider.identify(userId, enhancedTraits, {
831
+ ...options,
832
+ context: this.context
833
+ });
834
+ const metrics = this.providerMetrics.get(name);
835
+ if (metrics) {
836
+ metrics.callCount++;
837
+ metrics.lastUsed = process.hrtime.bigint();
838
+ }
839
+ } catch (error) {
840
+ const metrics = this.providerMetrics.get(name);
841
+ if (metrics) metrics.errorCount++;
842
+ if (this.config.onError) this.config.onError(error, {
843
+ provider: name,
844
+ method: "identify",
845
+ userId
846
+ });
847
+ } finally {
848
+ this.rateLimiter.release();
849
+ }
850
+ }
851
+ });
852
+ await Promise.allSettled(promises);
853
+ }
854
+ async page(nameOrPayload, properties, options) {
855
+ if (typeof nameOrPayload === "object") {
856
+ const payload = nameOrPayload;
857
+ return this.page(payload.name, payload.properties, {
858
+ ...options,
859
+ context: {
860
+ ...this.context,
861
+ ...payload.context
862
+ }
863
+ });
864
+ }
865
+ const name = nameOrPayload;
866
+ if (!this.isInitialized) {
867
+ if (this.config.onError) this.config.onError(/* @__PURE__ */ new Error("Analytics not initialized"), {
868
+ provider: "analytics",
869
+ name,
870
+ method: "page"
871
+ });
872
+ return;
873
+ }
874
+ const sanitized = sanitizeProperties(properties ?? {}, {
875
+ stripPII: false,
876
+ stripHTML: true,
877
+ allowDangerousKeys: false
878
+ });
879
+ const targetProviders = this.getTargetProviders(options);
880
+ const enhancedProperties = { ...sanitized.data };
881
+ const promises = [...targetProviders.entries()].map(async ([providerName, provider]) => {
882
+ if (provider.page) {
883
+ if (!await this.rateLimiter.tryAcquire()) {
884
+ if (this.config.debug && this.config.onInfo) this.config.onInfo(`Rate limit exceeded for provider ${providerName}`);
885
+ if (this.config.onError) this.config.onError(/* @__PURE__ */ new Error(`Rate limit exceeded for provider ${providerName}`), {
886
+ provider: providerName,
887
+ name,
888
+ method: "page"
889
+ });
890
+ return;
891
+ }
892
+ try {
893
+ await provider.page(name ?? "", enhancedProperties, {
894
+ ...options,
895
+ context: this.context
896
+ });
897
+ const metrics = this.providerMetrics.get(providerName);
898
+ if (metrics) {
899
+ metrics.callCount++;
900
+ metrics.lastUsed = process.hrtime.bigint();
901
+ }
902
+ } catch (error) {
903
+ const metrics = this.providerMetrics.get(providerName);
904
+ if (metrics) metrics.errorCount++;
905
+ if (this.config.onError) this.config.onError(error, {
906
+ provider: providerName,
907
+ name: name ?? "",
908
+ method: "page"
909
+ });
910
+ } finally {
911
+ this.rateLimiter.release();
912
+ }
913
+ }
914
+ });
915
+ await Promise.allSettled(promises);
916
+ }
917
+ async screen(nameOrPayload, properties, options) {
918
+ if (typeof nameOrPayload === "object") {
919
+ const payload = nameOrPayload;
920
+ return this.screen(payload.name, payload.properties, {
921
+ ...options,
922
+ context: {
923
+ ...this.context,
924
+ ...payload.context
925
+ }
926
+ });
927
+ }
928
+ const name = nameOrPayload;
929
+ if (!this.isInitialized) {
930
+ if (this.config.onError) this.config.onError(/* @__PURE__ */ new Error("Analytics not initialized"), {
931
+ provider: "analytics",
932
+ name,
933
+ method: "screen"
934
+ });
935
+ return;
936
+ }
937
+ const sanitized = sanitizeProperties(properties ?? {}, {
938
+ stripPII: false,
939
+ stripHTML: true,
940
+ allowDangerousKeys: false
941
+ });
942
+ const targetProviders = this.getTargetProviders(options);
943
+ const enhancedProperties = { ...sanitized.data };
944
+ const promises = [...targetProviders.entries()].map(async ([providerName, provider]) => {
945
+ if (provider.screen) {
946
+ if (!await this.rateLimiter.tryAcquire()) {
947
+ if (this.config.debug && this.config.onInfo) this.config.onInfo(`Rate limit exceeded for provider ${providerName}`);
948
+ if (this.config.onError) this.config.onError(/* @__PURE__ */ new Error(`Rate limit exceeded for provider ${providerName}`), {
949
+ provider: providerName,
950
+ name,
951
+ method: "screen"
952
+ });
953
+ return;
954
+ }
955
+ try {
956
+ await provider.screen(name ?? "", enhancedProperties, {
957
+ ...options,
958
+ context: this.context
959
+ });
960
+ const metrics = this.providerMetrics.get(providerName);
961
+ if (metrics) {
962
+ metrics.callCount++;
963
+ metrics.lastUsed = process.hrtime.bigint();
964
+ }
965
+ } catch (error) {
966
+ const metrics = this.providerMetrics.get(providerName);
967
+ if (metrics) metrics.errorCount++;
968
+ if (this.config.onError) this.config.onError(error, {
969
+ provider: providerName,
970
+ name: name ?? "",
971
+ method: "screen"
972
+ });
973
+ } finally {
974
+ this.rateLimiter.release();
975
+ }
976
+ }
977
+ });
978
+ await Promise.allSettled(promises);
979
+ }
980
+ async group(groupIdOrPayload, traits, options) {
981
+ if (typeof groupIdOrPayload === "object") {
982
+ const payload = groupIdOrPayload;
983
+ return this.group(payload.groupId, payload.traits, {
984
+ ...options,
985
+ context: {
986
+ ...this.context,
987
+ ...payload.context
988
+ }
989
+ });
990
+ }
991
+ const groupId = groupIdOrPayload;
992
+ if (!this.isInitialized) {
993
+ if (this.config.onError) this.config.onError(/* @__PURE__ */ new Error("Analytics not initialized"), {
994
+ provider: "analytics",
995
+ groupId,
996
+ method: "group"
997
+ });
998
+ return;
999
+ }
1000
+ const sanitized = sanitizeProperties(traits ?? {}, {
1001
+ stripPII: false,
1002
+ stripHTML: true,
1003
+ allowDangerousKeys: false
1004
+ });
1005
+ this.setContext({
1006
+ organizationId: groupId,
1007
+ ...sanitized.data
1008
+ });
1009
+ const targetProviders = this.getTargetProviders(options);
1010
+ const enhancedTraits = { ...sanitized.data };
1011
+ const promises = [...targetProviders.entries()].map(async ([providerName, provider]) => {
1012
+ if (provider.group) {
1013
+ if (!await this.rateLimiter.tryAcquire()) {
1014
+ if (this.config.debug && this.config.onInfo) this.config.onInfo(`Rate limit exceeded for provider ${providerName}`);
1015
+ if (this.config.onError) this.config.onError(/* @__PURE__ */ new Error(`Rate limit exceeded for provider ${providerName}`), {
1016
+ provider: providerName,
1017
+ groupId,
1018
+ method: "group"
1019
+ });
1020
+ return;
1021
+ }
1022
+ try {
1023
+ await provider.group(groupId, enhancedTraits, {
1024
+ ...options,
1025
+ context: this.context
1026
+ });
1027
+ const metrics = this.providerMetrics.get(providerName);
1028
+ if (metrics) {
1029
+ metrics.callCount++;
1030
+ metrics.lastUsed = process.hrtime.bigint();
1031
+ }
1032
+ } catch (error) {
1033
+ const metrics = this.providerMetrics.get(providerName);
1034
+ if (metrics) metrics.errorCount++;
1035
+ if (this.config.onError) this.config.onError(error, {
1036
+ provider: providerName,
1037
+ groupId,
1038
+ method: "group"
1039
+ });
1040
+ } finally {
1041
+ this.rateLimiter.release();
1042
+ }
1043
+ }
1044
+ });
1045
+ await Promise.allSettled(promises);
1046
+ }
1047
+ async alias(userIdOrPayload, previousId, options) {
1048
+ if (typeof userIdOrPayload === "object") {
1049
+ const payload = userIdOrPayload;
1050
+ return this.alias(payload.userId, payload.previousId, {
1051
+ ...options,
1052
+ context: {
1053
+ ...this.context,
1054
+ ...payload.context
1055
+ }
1056
+ });
1057
+ }
1058
+ const userId = userIdOrPayload;
1059
+ const prevId = previousId;
1060
+ if (!this.isInitialized) {
1061
+ if (this.config.onError) this.config.onError(/* @__PURE__ */ new Error("Analytics not initialized"), {
1062
+ provider: "analytics",
1063
+ method: "alias",
1064
+ previousId: prevId,
1065
+ userId
1066
+ });
1067
+ return;
1068
+ }
1069
+ const promises = [...this.getTargetProviders(options).entries()].map(async ([providerName, provider]) => {
1070
+ if (provider.alias) {
1071
+ if (!await this.rateLimiter.tryAcquire()) {
1072
+ if (this.config.debug && this.config.onInfo) this.config.onInfo(`Rate limit exceeded for provider ${providerName}`);
1073
+ if (this.config.onError) this.config.onError(/* @__PURE__ */ new Error(`Rate limit exceeded for provider ${providerName}`), {
1074
+ provider: providerName,
1075
+ userId,
1076
+ previousId,
1077
+ method: "alias"
1078
+ });
1079
+ return;
1080
+ }
1081
+ try {
1082
+ await provider.alias(userId, prevId, this.context);
1083
+ const metrics = this.providerMetrics.get(providerName);
1084
+ if (metrics) {
1085
+ metrics.callCount++;
1086
+ metrics.lastUsed = process.hrtime.bigint();
1087
+ }
1088
+ } catch (error) {
1089
+ const metrics = this.providerMetrics.get(providerName);
1090
+ if (metrics) metrics.errorCount++;
1091
+ if (this.config.onError) this.config.onError(error, {
1092
+ provider: providerName,
1093
+ method: "alias",
1094
+ previousId: prevId,
1095
+ userId
1096
+ });
1097
+ } finally {
1098
+ this.rateLimiter.release();
1099
+ }
1100
+ }
1101
+ });
1102
+ await Promise.allSettled(promises);
1103
+ }
1104
+ /**
1105
+ * Get list of active provider names
1106
+ */
1107
+ getActiveProviders() {
1108
+ return [...this.providers.keys()];
1109
+ }
1110
+ /**
1111
+ * Get a specific provider instance
1112
+ */
1113
+ getProvider(name) {
1114
+ return this.providers.get(name);
1115
+ }
1116
+ /**
1117
+ * Reset analytics context and identity
1118
+ */
1119
+ reset() {
1120
+ this.context = {};
1121
+ this.eventCache.clear();
1122
+ }
1123
+ /**
1124
+ * Shutdown all providers and cleanup resources
1125
+ */
1126
+ async shutdown() {
1127
+ const shutdownPromises = [...this.providers.entries()].map(async ([name, provider]) => {
1128
+ try {
1129
+ if (typeof provider.destroy === "function") await provider.destroy();
1130
+ } catch (error) {
1131
+ if (this.config.onError) this.config.onError(error, {
1132
+ provider: name,
1133
+ method: "shutdown"
1134
+ });
1135
+ }
1136
+ });
1137
+ await Promise.allSettled(shutdownPromises);
1138
+ this.providers.clear();
1139
+ this.isInitialized = false;
1140
+ }
1141
+ /**
1142
+ * Process any emitter payload with Node 22+ optimizations
1143
+ * FIX #12: Use LRU cache for event deduplication (prevents memory leaks)
1144
+ */
1145
+ async emit(payload, options) {
1146
+ const emitStartTime = process.hrtime.bigint();
1147
+ const createCacheKey = (p) => {
1148
+ const sorted = (obj) => {
1149
+ if (obj === null || typeof obj !== "object" || obj instanceof Array) return obj;
1150
+ const sortedObj = {};
1151
+ for (const key of Object.keys(obj).sort()) sortedObj[key] = sorted(obj[key]);
1152
+ return sortedObj;
1153
+ };
1154
+ return JSON.stringify(sorted(p));
1155
+ };
1156
+ const cacheKey = createCacheKey(payload);
1157
+ if (this.eventCache.has(cacheKey)) {
1158
+ if (this.eventCache.get(cacheKey)?.processed) {
1159
+ if (this.config.debug && this.config.onInfo) this.config.onInfo("Skipping duplicate event processing");
1160
+ return;
1161
+ }
1162
+ }
1163
+ this.eventCache.set(cacheKey, {
1164
+ timestamp: emitStartTime,
1165
+ processed: true
1166
+ });
1167
+ try {
1168
+ if (options?.timeout) {
1169
+ const timeoutSignal = AbortSignal.timeout(options.timeout);
1170
+ await Promise.race([this.processPayloadByType(payload), new Promise((_resolve, reject) => {
1171
+ timeoutSignal.addEventListener("abort", () => reject(/* @__PURE__ */ new Error(`Event processing timed out after ${options.timeout}ms`)));
1172
+ })]);
1173
+ } else await this.processPayloadByType(payload);
1174
+ if (this.config.debug && this.config.onInfo) {
1175
+ const processingTime = Number(process.hrtime.bigint() - emitStartTime) / 1e6;
1176
+ this.config.onInfo(`Event processed in ${processingTime.toFixed(2)}ms`);
1177
+ }
1178
+ } catch (error) {
1179
+ this.eventCache.set(cacheKey, {
1180
+ timestamp: emitStartTime,
1181
+ processed: false
1182
+ });
1183
+ throw error;
1184
+ }
1185
+ }
1186
+ /**
1187
+ * Route payload to appropriate method based on type
1188
+ */
1189
+ async processPayloadByType(payload) {
1190
+ switch (payload.type) {
1191
+ case "track": return this.track(payload);
1192
+ case "identify": return this.identify(payload);
1193
+ case "page": return this.page(payload);
1194
+ case "screen": return this.screen(payload);
1195
+ case "group": return this.group(payload);
1196
+ case "alias": return this.alias(payload);
1197
+ }
1198
+ }
1199
+ /**
1200
+ * Process an emitter payload (legacy method - use emit() instead)
1201
+ * @deprecated Use emit() for better type safety
1202
+ */
1203
+ async processEmitterPayload(payload) {
1204
+ return this.emit(payload);
1205
+ }
1206
+ /**
1207
+ * Batch emit multiple payloads with Node 22+ optimizations
1208
+ * FIX #14: Process chunks concurrently instead of sequentially
1209
+ * FIX #13: Use spread operator instead of structuredClone
1210
+ */
1211
+ async emitBatch(payloads, options) {
1212
+ const batchStartTime = process.hrtime.bigint();
1213
+ const concurrency = options?.concurrency ?? 10;
1214
+ if (payloads.length === 0) return;
1215
+ const clonedPayloads = payloads.map((p) => structuredClone(p));
1216
+ const chunks = [];
1217
+ for (let i = 0; i < clonedPayloads.length; i += concurrency) chunks.push(clonedPayloads.slice(i, i + concurrency));
1218
+ const processChunk = async (chunk) => {
1219
+ const chunkPromises = chunk.map((payload) => this.emit(payload, options?.timeout !== void 0 ? { timeout: options.timeout } : void 0));
1220
+ if (options?.failFast) await Promise.all(chunkPromises);
1221
+ else await Promise.allSettled(chunkPromises);
1222
+ };
1223
+ const chunkPromises = chunks.map((chunk) => processChunk(chunk));
1224
+ await Promise.all(chunkPromises);
1225
+ if (this.config.debug && this.config.onInfo) {
1226
+ const batchTime = Number(process.hrtime.bigint() - batchStartTime) / 1e6;
1227
+ this.config.onInfo(`Batch of ${payloads.length} events processed in ${batchTime.toFixed(2)}ms (concurrency: ${concurrency})`);
1228
+ }
1229
+ }
1230
+ /**
1231
+ * Create a bound emitter function for convenience
1232
+ */
1233
+ createEmitter() {
1234
+ return this.emit.bind(this);
1235
+ }
1236
+ /**
1237
+ * Track an ecommerce event specification
1238
+ * @deprecated Use emit(ecommerce.EVENT_NAME(...)) instead
1239
+ */
1240
+ async trackEcommerce(eventSpec) {
1241
+ return this.track(eventSpec.name, eventSpec.properties);
1242
+ }
1243
+ /**
1244
+ * Get target providers based on tracking options
1245
+ */
1246
+ getTargetProviders(options) {
1247
+ let targetProviders = new Map(this.providers);
1248
+ if (options) {
1249
+ if (options.providers && Object.hasOwn(options, "providers")) for (const [name, config] of Object.entries(options.providers)) {
1250
+ const factory = this.availableProviders[name];
1251
+ if (factory) try {
1252
+ const provider = factory(config);
1253
+ targetProviders.set(name, provider);
1254
+ this.providerMetrics.set(name, {
1255
+ initTime: process.hrtime.bigint(),
1256
+ callCount: 0,
1257
+ errorCount: 0,
1258
+ lastUsed: process.hrtime.bigint()
1259
+ });
1260
+ } catch (error) {
1261
+ if (this.config.onError) this.config.onError(error, {
1262
+ provider: name,
1263
+ method: "runtime-create"
1264
+ });
1265
+ }
1266
+ }
1267
+ if (options.only && Object.hasOwn(options, "only")) {
1268
+ const onlyProviders = /* @__PURE__ */ new Map();
1269
+ for (const name of options.only) if (targetProviders.has(name)) onlyProviders.set(name, targetProviders.get(name));
1270
+ targetProviders = onlyProviders;
1271
+ }
1272
+ if (options.exclude && Object.hasOwn(options, "exclude")) for (const name of options.exclude) targetProviders.delete(name);
1273
+ }
1274
+ return targetProviders;
1275
+ }
1276
+ /**
1277
+ * Get analytics performance metrics using Node 22+ features
1278
+ */
1279
+ getAnalyticsMetrics() {
1280
+ const now = process.hrtime.bigint();
1281
+ const providers = {};
1282
+ for (const [providerName, metrics] of this.providerMetrics) providers[providerName] = {
1283
+ initTimeMs: Number(metrics.initTime) / 1e6,
1284
+ callCount: metrics.callCount,
1285
+ errorCount: metrics.errorCount,
1286
+ lastUsedMs: Number(now - metrics.lastUsed) / 1e6,
1287
+ successRate: metrics.callCount > 0 ? 1 - metrics.errorCount / metrics.callCount : 1
1288
+ };
1289
+ return {
1290
+ providers,
1291
+ events: {
1292
+ totalProcessed: this.eventCache.size,
1293
+ cacheSize: this.eventCache.size,
1294
+ memoryUsage: process.memoryUsage()
1295
+ }
1296
+ };
1297
+ }
1298
+ /**
1299
+ * Health check for analytics system using Node 22+ timing
1300
+ * FIX #16: Send actual test events instead of just setContext
1301
+ */
1302
+ async healthCheck(timeout = 5e3) {
1303
+ const checkStartTime = process.hrtime.bigint();
1304
+ const providerHealth = {};
1305
+ const timeoutSignal = AbortSignal.timeout(timeout);
1306
+ try {
1307
+ const healthPromises = [...this.providers.entries()].map(async ([name, provider]) => {
1308
+ try {
1309
+ await Promise.race([provider.track("__health_check__", {
1310
+ timestamp: Date.now(),
1311
+ provider: name
1312
+ }), new Promise((_resolve, reject) => {
1313
+ timeoutSignal.addEventListener("abort", () => reject(/* @__PURE__ */ new Error("Health check timeout")));
1314
+ })]);
1315
+ providerHealth[name] = true;
1316
+ } catch {
1317
+ providerHealth[name] = false;
1318
+ }
1319
+ });
1320
+ await Promise.allSettled(healthPromises);
1321
+ const checkEndTime = process.hrtime.bigint();
1322
+ const totalCheckTime = Number(checkEndTime - checkStartTime) / 1e6;
1323
+ return {
1324
+ healthy: Object.values(providerHealth).filter(Boolean).length > 0 && this.isInitialized,
1325
+ providers: providerHealth,
1326
+ metrics: this.getAnalyticsMetrics(),
1327
+ totalCheckTime
1328
+ };
1329
+ } catch {
1330
+ return {
1331
+ healthy: false,
1332
+ providers: providerHealth,
1333
+ metrics: this.getAnalyticsMetrics(),
1334
+ totalCheckTime: Number(process.hrtime.bigint() - checkStartTime) / 1e6
1335
+ };
1336
+ }
1337
+ }
1338
+ };
1339
+ /**
1340
+ * Factory function to create analytics manager
1341
+ */
1342
+ function createAnalyticsManager(config, availableProviders) {
1343
+ return new AnalyticsManager(config, availableProviders);
1344
+ }
1345
+
1346
+ //#endregion
1347
+ //#region src/shared/emitters/helpers.ts
1348
+ /**
1349
+ * @fileoverview Helper Functions for Analytics Emitters
1350
+ *
1351
+ * These utilities make it easier to use the emitter-first approach for analytics.
1352
+ * Provides builders, batch processing, session management, and convenience
1353
+ * functions for common tracking patterns.
1354
+ *
1355
+ * **Builders**:
1356
+ * - `ContextBuilder`: Build consistent context across events
1357
+ * - `PayloadBuilder`: Chain emitter creation with shared options
1358
+ * - `EventBatch`: Batch related events with shared context
1359
+ *
1360
+ * **Session Management**:
1361
+ * - `createUserSession()`: Create a user session tracking flow
1362
+ * - `createAnonymousSession()`: Create an anonymous user tracking flow
1363
+ *
1364
+ * **Convenience Functions**:
1365
+ * - `withMetadata()`: Add consistent metadata to events
1366
+ * - `withUTM()`: Add UTM parameters to events
1367
+ * - Type guards: `isTrackPayload()`, `isIdentifyPayload()`, etc.
1368
+ *
1369
+ * @module @od-oneapp/analytics/shared/emitters/helpers
1370
+ */
1371
+ /**
1372
+ * Builder for creating consistent context across analytics events.
1373
+ *
1374
+ * @remarks
1375
+ * Use this builder to create a reusable context object that can be applied
1376
+ * to multiple events. This ensures consistency and reduces repetition.
1377
+ *
1378
+ * @example
1379
+ * ```typescript
1380
+ * const context = new ContextBuilder()
1381
+ * .setUser('user-123', { plan: 'pro' })
1382
+ * .setOrganization('org-456')
1383
+ * .setPage({ path: '/dashboard', title: 'Dashboard' })
1384
+ * .build();
1385
+ *
1386
+ * await analytics.emit(track('Button Clicked', {}, { context }));
1387
+ * ```
1388
+ */
1389
+ var ContextBuilder = class {
1390
+ context = {};
1391
+ constructor(initialContext) {
1392
+ if (initialContext) this.context = { ...initialContext };
1393
+ }
1394
+ setUser(_userId, traits) {
1395
+ this.context.traits = {
1396
+ ...this.context.traits,
1397
+ ...traits
1398
+ };
1399
+ return this;
1400
+ }
1401
+ setOrganization(groupId) {
1402
+ this.context.groupId = groupId;
1403
+ return this;
1404
+ }
1405
+ setPage(pageInfo) {
1406
+ this.context.page = {
1407
+ ...this.context.page,
1408
+ ...pageInfo
1409
+ };
1410
+ return this;
1411
+ }
1412
+ setCampaign(utmParams) {
1413
+ this.context.campaign = {
1414
+ ...this.context.campaign,
1415
+ ...utmParams
1416
+ };
1417
+ return this;
1418
+ }
1419
+ setDevice(deviceInfo) {
1420
+ this.context.device = {
1421
+ ...this.context.device,
1422
+ ...deviceInfo
1423
+ };
1424
+ return this;
1425
+ }
1426
+ build() {
1427
+ return { ...this.context };
1428
+ }
1429
+ };
1430
+ /**
1431
+ * Builder for chaining emitter creation with shared options.
1432
+ *
1433
+ * @remarks
1434
+ * Use this builder to create multiple events with shared options like
1435
+ * timestamp, anonymousId, or integrations. This is useful when tracking
1436
+ * multiple related events in a single flow.
1437
+ *
1438
+ * @example
1439
+ * ```typescript
1440
+ * const builder = new PayloadBuilder(context)
1441
+ * .withTimestamp(new Date())
1442
+ * .withAnonymousId('anon-123');
1443
+ *
1444
+ * await analytics.emit(builder.track('Event 1', {}));
1445
+ * await analytics.emit(builder.track('Event 2', {}));
1446
+ * ```
1447
+ */
1448
+ var PayloadBuilder = class {
1449
+ options = {};
1450
+ constructor(context) {
1451
+ if (context) this.options.context = context;
1452
+ }
1453
+ withTimestamp(timestamp) {
1454
+ this.options.timestamp = timestamp;
1455
+ return this;
1456
+ }
1457
+ withAnonymousId(anonymousId) {
1458
+ this.options.anonymousId = anonymousId;
1459
+ return this;
1460
+ }
1461
+ withIntegrations(integrations) {
1462
+ this.options.integrations = integrations;
1463
+ return this;
1464
+ }
1465
+ track(event, properties) {
1466
+ return track(event, properties, this.options);
1467
+ }
1468
+ identify(userId, traits) {
1469
+ return identify(userId, traits, this.options);
1470
+ }
1471
+ page(name, properties) {
1472
+ return page(void 0, name, properties, this.options);
1473
+ }
1474
+ group(groupId, traits) {
1475
+ return group(groupId, traits, this.options);
1476
+ }
1477
+ alias(userId, previousId) {
1478
+ return alias(userId, previousId, this.options);
1479
+ }
1480
+ };
1481
+ /**
1482
+ * Batch processor for related events with shared context.
1483
+ *
1484
+ * @remarks
1485
+ * Use this class to collect multiple events and emit them together with
1486
+ * a shared context. This is useful for tracking user flows or multi-step
1487
+ * processes where events should be grouped together.
1488
+ *
1489
+ * @example
1490
+ * ```typescript
1491
+ * const batch = new EventBatch(context)
1492
+ * .addTrack('Step 1 Completed', { step: 1 })
1493
+ * .addTrack('Step 2 Completed', { step: 2 })
1494
+ * .addTrack('Flow Completed', { totalSteps: 2 });
1495
+ *
1496
+ * for (const event of batch.getEvents()) {
1497
+ * await analytics.emit(event);
1498
+ * }
1499
+ * ```
1500
+ */
1501
+ var EventBatch = class {
1502
+ events = [];
1503
+ sharedContext;
1504
+ constructor(context) {
1505
+ this.sharedContext = context ?? {};
1506
+ }
1507
+ add(payload) {
1508
+ const enrichedPayload = {
1509
+ ...payload,
1510
+ context: {
1511
+ ...this.sharedContext,
1512
+ ...payload.context
1513
+ }
1514
+ };
1515
+ this.events.push(enrichedPayload);
1516
+ return this;
1517
+ }
1518
+ addTrack(event, properties) {
1519
+ return this.add(track(event, properties, { context: this.sharedContext }));
1520
+ }
1521
+ addIdentify(userId, traits) {
1522
+ return this.add(identify(userId, traits, { context: this.sharedContext }));
1523
+ }
1524
+ addPage(name, properties) {
1525
+ return this.add(page(void 0, name, properties, { context: this.sharedContext }));
1526
+ }
1527
+ addGroup(groupId, traits) {
1528
+ return this.add(group(groupId, traits, { context: this.sharedContext }));
1529
+ }
1530
+ getEvents() {
1531
+ return [...this.events];
1532
+ }
1533
+ clear() {
1534
+ this.events = [];
1535
+ }
1536
+ };
1537
+ /**
1538
+ * Creates a user session tracking flow with pre-configured context.
1539
+ *
1540
+ * @remarks
1541
+ * This helper creates a session object with methods for tracking events
1542
+ * within a specific user session. All events will include the sessionId
1543
+ * and user context automatically.
1544
+ *
1545
+ * @param userId - Unique identifier for the user
1546
+ * @param sessionId - Unique identifier for the session
1547
+ * @returns Session object with `identify`, `track`, `page`, and `group` methods
1548
+ *
1549
+ * @example
1550
+ * ```typescript
1551
+ * const session = createUserSession('user-123', 'session-456');
1552
+ * await analytics.emit(session.track('Button Clicked', { button: 'signup' }));
1553
+ * await analytics.emit(session.page('Dashboard'));
1554
+ * ```
1555
+ */
1556
+ function createUserSession(userId, sessionId) {
1557
+ const context = new ContextBuilder().setUser(userId).build();
1558
+ return {
1559
+ identify: (traits) => identify(userId, {
1560
+ ...traits,
1561
+ sessionId
1562
+ }, { context }),
1563
+ track: (event, properties) => track(event, {
1564
+ ...properties,
1565
+ sessionId
1566
+ }, { context }),
1567
+ page: (name, properties) => page(void 0, name, {
1568
+ ...properties,
1569
+ sessionId
1570
+ }, { context }),
1571
+ group: (groupId, traits) => group(groupId, {
1572
+ ...traits,
1573
+ sessionId
1574
+ }, { context })
1575
+ };
1576
+ }
1577
+ /**
1578
+ * Creates an anonymous user tracking flow.
1579
+ *
1580
+ * @remarks
1581
+ * This helper creates a session object for tracking anonymous users before
1582
+ * they are identified. When the user signs up or logs in, use the `alias`
1583
+ * method to link their anonymous activity to their user ID.
1584
+ *
1585
+ * @param anonymousId - Unique identifier for the anonymous user
1586
+ * @returns Session object with `track`, `page`, `identify`, and `alias` methods
1587
+ *
1588
+ * @example
1589
+ * ```typescript
1590
+ * const anonymousSession = createAnonymousSession('anon-123');
1591
+ * await analytics.emit(anonymousSession.track('Page Viewed', { page: 'home' }));
1592
+ *
1593
+ * // Later, when user signs up
1594
+ * await analytics.emit(anonymousSession.alias('user-123'));
1595
+ * await analytics.emit(anonymousSession.identify('user-123', { email: 'user@example.com' }));
1596
+ * ```
1597
+ */
1598
+ function createAnonymousSession(anonymousId) {
1599
+ const options = { anonymousId };
1600
+ return {
1601
+ track: (event, properties) => track(event, properties, options),
1602
+ page: (name, properties) => page(void 0, name, properties, options),
1603
+ identify: (userId, traits) => identify(userId, traits, options),
1604
+ alias: (userId) => alias(userId, anonymousId, options)
1605
+ };
1606
+ }
1607
+ /**
1608
+ * Type guard to check if a payload is a track event.
1609
+ *
1610
+ * @param payload - The payload to check
1611
+ * @returns True if the payload is a track event
1612
+ */
1613
+ const isTrackPayload = (payload) => payload.type === "track";
1614
+ /**
1615
+ * Type guard to check if a payload is an identify event.
1616
+ *
1617
+ * @param payload - The payload to check
1618
+ * @returns True if the payload is an identify event
1619
+ */
1620
+ const isIdentifyPayload = (payload) => payload.type === "identify";
1621
+ /**
1622
+ * Type guard to check if a payload is a page event.
1623
+ *
1624
+ * @param payload - The payload to check
1625
+ * @returns True if the payload is a page event
1626
+ */
1627
+ const isPagePayload = (payload) => payload.type === "page";
1628
+ /**
1629
+ * Type guard to check if a payload is a group event.
1630
+ *
1631
+ * @param payload - The payload to check
1632
+ * @returns True if the payload is a group event
1633
+ */
1634
+ const isGroupPayload = (payload) => payload.type === "group";
1635
+ /**
1636
+ * Type guard to check if a payload is an alias event.
1637
+ *
1638
+ * @param payload - The payload to check
1639
+ * @returns True if the payload is an alias event
1640
+ */
1641
+ const isAliasPayload = (payload) => payload.type === "alias";
1642
+ /**
1643
+ * Adds consistent metadata to an analytics event payload.
1644
+ *
1645
+ * @remarks
1646
+ * This helper enriches an event payload with metadata that will be included
1647
+ * in the event's context.app object. Useful for tracking version, source,
1648
+ * or other application-level metadata.
1649
+ *
1650
+ * @param payload - The event payload to enrich
1651
+ * @param metadata - Metadata to add (version, source, or custom properties)
1652
+ * @returns The enriched payload with metadata in context.app
1653
+ *
1654
+ * @example
1655
+ * ```typescript
1656
+ * const event = track('Button Clicked', { button: 'signup' });
1657
+ * const enriched = withMetadata(event, { version: '1.0.0', source: 'webapp' });
1658
+ * await analytics.emit(enriched);
1659
+ * ```
1660
+ */
1661
+ function withMetadata(payload, metadata) {
1662
+ return {
1663
+ ...payload,
1664
+ context: {
1665
+ ...payload.context,
1666
+ app: {
1667
+ ...payload.context?.app,
1668
+ ...metadata
1669
+ }
1670
+ }
1671
+ };
1672
+ }
1673
+ /**
1674
+ * Adds UTM parameters to an analytics event payload.
1675
+ *
1676
+ * @remarks
1677
+ * This helper enriches an event payload with UTM campaign parameters that
1678
+ * will be included in the event's context.campaign object. Useful for
1679
+ * tracking marketing campaign attribution.
1680
+ *
1681
+ * @param payload - The event payload to enrich
1682
+ * @param utm - UTM parameters (source, medium, campaign, term, content)
1683
+ * @returns The enriched payload with UTM parameters in context.campaign
1684
+ *
1685
+ * @example
1686
+ * ```typescript
1687
+ * const event = track('Signup Started', {});
1688
+ * const enriched = withUTM(event, {
1689
+ * source: 'google',
1690
+ * medium: 'cpc',
1691
+ * campaign: 'summer-sale'
1692
+ * });
1693
+ * await analytics.emit(enriched);
1694
+ * ```
1695
+ */
1696
+ function withUTM(payload, utm) {
1697
+ return {
1698
+ ...payload,
1699
+ context: {
1700
+ ...payload.context,
1701
+ campaign: {
1702
+ ...payload.context?.campaign,
1703
+ ...utm
1704
+ }
1705
+ }
1706
+ };
1707
+ }
1708
+
1709
+ //#endregion
1710
+ //#region src/shared/utils/posthog-bootstrap.ts
1711
+ /**
1712
+ * Generate a unique distinct ID (compatible with PostHog format)
1713
+ */
1714
+ function generateDistinctId() {
1715
+ return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 15)}`;
1716
+ }
1717
+ /**
1718
+ * Parse PostHog cookie to extract distinct ID
1719
+ */
1720
+ function parsePostHogCookie(cookieValue, _projectApiKey) {
1721
+ try {
1722
+ const parsed = JSON.parse(cookieValue);
1723
+ if (parsed && typeof parsed.distinct_id === "string") return parsed;
1724
+ return null;
1725
+ } catch {
1726
+ return null;
1727
+ }
1728
+ }
1729
+ /**
1730
+ * Get PostHog cookie name for a project
1731
+ */
1732
+ function getPostHogCookieName(projectApiKey) {
1733
+ return `ph_${projectApiKey}_posthog`;
1734
+ }
1735
+ /**
1736
+ * Extract distinct ID from cookies (Next.js compatible)
1737
+ */
1738
+ function getDistinctIdFromCookies(cookies, projectApiKey) {
1739
+ try {
1740
+ const cookieName = getPostHogCookieName(projectApiKey);
1741
+ if (cookies && typeof cookies.get === "function") {
1742
+ const cookie = cookies.get(cookieName);
1743
+ if (cookie?.value) return parsePostHogCookie(cookie.value, projectApiKey)?.distinct_id ?? null;
1744
+ }
1745
+ if (typeof cookies === "string") {
1746
+ const cookieMatch = cookies.match(new RegExp(`${cookieName}=([^;]+)`));
1747
+ if (cookieMatch?.[1]) return parsePostHogCookie(decodeURIComponent(cookieMatch[1]), projectApiKey)?.distinct_id ?? null;
1748
+ }
1749
+ return null;
1750
+ } catch {
1751
+ return null;
1752
+ }
1753
+ }
1754
+ /**
1755
+ * Create bootstrap data for PostHog client initialization
1756
+ */
1757
+ function createBootstrapData(distinctId) {
1758
+ return { distinctID: distinctId };
1759
+ }
1760
+ /**
1761
+ * Create minimal bootstrap data when full data is unavailable
1762
+ */
1763
+ function createMinimalBootstrapData(distinctId) {
1764
+ return { distinctID: distinctId ?? generateDistinctId() };
1765
+ }
1766
+
1767
+ //#endregion
1768
+ export { AnalyticsManager as _, ContextBuilder as a, validateEventName as b, createAnonymousSession as c, isGroupPayload as d, isIdentifyPayload as f, withUTM as g, withMetadata as h, getDistinctIdFromCookies as i, createUserSession as l, isTrackPayload as m, createMinimalBootstrapData as n, EventBatch as o, isPagePayload as p, generateDistinctId as r, PayloadBuilder as s, createBootstrapData as t, isAliasPayload as u, createAnalyticsManager as v, sanitizeProperties as y };
1769
+ //# sourceMappingURL=posthog-bootstrap-CYfIy_WS.mjs.map