@od-oneapp/analytics 2026.2.1501-canary.1 → 2026.2.1701

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 (65) hide show
  1. package/{ai-Co8hBoEj.mjs → ai-YMnynb-t.mjs} +2 -2
  2. package/{ai-Co8hBoEj.mjs.map → ai-YMnynb-t.mjs.map} +1 -1
  3. package/{client-C_4MPbIp.mjs → client-CcFTauAh.mjs} +1 -1
  4. package/{client-C_4MPbIp.mjs.map → client-CcFTauAh.mjs.map} +1 -1
  5. package/{client-BOIcr_5V.mjs → client-CeOLjbac.mjs} +1 -1
  6. package/{client-BOIcr_5V.mjs.map → client-CeOLjbac.mjs.map} +1 -1
  7. package/{client-BiJJm6CH.mjs → client-D339NFJS.mjs} +1 -1
  8. package/{client-BiJJm6CH.mjs.map → client-D339NFJS.mjs.map} +1 -1
  9. package/client-next.d.mts +7 -7
  10. package/client-next.mjs +10 -10
  11. package/client.d.mts +9 -9
  12. package/client.mjs +115 -7
  13. package/client.mjs.map +1 -1
  14. package/{config-BUMHyePt.d.mts → config-DPS6bSYo.d.mts} +2 -2
  15. package/{config-BUMHyePt.d.mts.map → config-DPS6bSYo.d.mts.map} +1 -1
  16. package/{config-6Mwe7b2O.mjs → config-P6P5adJg.mjs} +1 -1
  17. package/{config-6Mwe7b2O.mjs.map → config-P6P5adJg.mjs.map} +1 -1
  18. package/{ecommerce-DGG1FbiH.mjs → ecommerce-Cgu4wlux.mjs} +2 -2
  19. package/{ecommerce-DGG1FbiH.mjs.map → ecommerce-Cgu4wlux.mjs.map} +1 -1
  20. package/{emitters-BvEelkxS.mjs → emitters-6-nKo8i-.mjs} +1 -1
  21. package/{emitters-BvEelkxS.mjs.map → emitters-6-nKo8i-.mjs.map} +1 -1
  22. package/{emitters-BAXagFjt.d.mts → emitters-DldkVSPp.d.mts} +2 -2
  23. package/{emitters-BAXagFjt.d.mts.map → emitters-DldkVSPp.d.mts.map} +1 -1
  24. package/index-BfNWgfa5.d.mts +1494 -0
  25. package/index-BfNWgfa5.d.mts.map +1 -0
  26. package/{index-Cnz0VecZ.d.mts → index-BkIWe--N.d.mts} +2 -2
  27. package/{index-Cnz0VecZ.d.mts.map → index-BkIWe--N.d.mts.map} +1 -1
  28. package/{index-qlFPt3-1.d.mts → index-jPzXRn52.d.mts} +3 -3
  29. package/{index-qlFPt3-1.d.mts.map → index-jPzXRn52.d.mts.map} +1 -1
  30. package/{manager-B6kowr9x.d.mts → manager-DvRRjza6.d.mts} +9 -3
  31. package/{manager-B6kowr9x.d.mts.map → manager-DvRRjza6.d.mts.map} +1 -1
  32. package/package.json +7 -2
  33. package/{posthog-bootstrap-DDToYA4X.mjs → posthog-bootstrap-CYfIy_WS.mjs} +7 -6
  34. package/posthog-bootstrap-CYfIy_WS.mjs.map +1 -0
  35. package/{posthog-bootstrap-CRl1Pk_E.d.mts → posthog-bootstrap-DWxFrxlt.d.mts} +3 -3
  36. package/{posthog-bootstrap-CRl1Pk_E.d.mts.map → posthog-bootstrap-DWxFrxlt.d.mts.map} +1 -1
  37. package/providers-http-client.d.mts +1 -1
  38. package/providers-http-client.mjs +1 -1
  39. package/providers-http-server.d.mts +1 -1
  40. package/providers-http-server.mjs +1 -1
  41. package/server-edge.d.mts +3 -3
  42. package/server-edge.mjs +2 -2
  43. package/server-next.d.mts +11 -10
  44. package/server-next.d.mts.map +1 -1
  45. package/server-next.mjs +6 -6
  46. package/server.d.mts +10 -10
  47. package/server.mjs +6 -6
  48. package/service-Duqnlppl.mjs +1049 -0
  49. package/service-Duqnlppl.mjs.map +1 -0
  50. package/shared.d.mts +4 -4
  51. package/shared.mjs +3 -3
  52. package/{types-DnfdguWa.d.mts → types-BxBnNQ0V.d.mts} +1 -1
  53. package/{types-DnfdguWa.d.mts.map → types-BxBnNQ0V.d.mts.map} +1 -1
  54. package/{types-Brm88i6e.d.mts → types-CBvxUEaF.d.mts} +1 -1
  55. package/{types-Brm88i6e.d.mts.map → types-CBvxUEaF.d.mts.map} +1 -1
  56. package/types.d.mts +3 -3
  57. package/{vercel-types-Crq8m8c1.d.mts → vercel-types-lwakUfoI.d.mts} +1 -1
  58. package/{vercel-types-Crq8m8c1.d.mts.map → vercel-types-lwakUfoI.d.mts.map} +1 -1
  59. package/logs-B4zFs5kX.mjs +0 -108
  60. package/logs-B4zFs5kX.mjs.map +0 -1
  61. package/posthog-bootstrap-DDToYA4X.mjs.map +0 -1
  62. package/validation-DZc_nvoN.d.mts +0 -24
  63. package/validation-DZc_nvoN.d.mts.map +0 -1
  64. package/validation-Dll9R6h6.mjs +0 -315
  65. package/validation-Dll9R6h6.mjs.map +0 -1
@@ -0,0 +1,1049 @@
1
+ import { t as ConsoleProvider } from "./console-8bND3mMU.mjs";
2
+ import { b as validateEventName, v as createAnalyticsManager, y as sanitizeProperties } from "./posthog-bootstrap-CYfIy_WS.mjs";
3
+ import { t as PROVIDER_REQUIREMENTS } from "./config-P6P5adJg.mjs";
4
+ import { HttpServerProvider } from "./providers-http-server.mjs";
5
+ import { logError, logInfo, logWarn } from "@od-oneapp/shared/logger";
6
+ import { logDebug as logDebug$1, logError as logError$1, logInfo as logInfo$1, logWarn as logWarn$1 } from "@od-oneapp/shared/logs";
7
+ import { z } from "zod";
8
+
9
+ //#region ../../integrations/segment/src/analytics-provider/server.ts
10
+ var SegmentServerProvider = class {
11
+ name = "segment";
12
+ analytics = null;
13
+ config;
14
+ isInitialized = false;
15
+ constructor(config) {
16
+ if (!config.writeKey) throw new Error("Segment writeKey is required");
17
+ this.config = {
18
+ options: config.options,
19
+ writeKey: config.writeKey
20
+ };
21
+ }
22
+ async initialize() {
23
+ if (this.isInitialized) return;
24
+ const { Analytics } = await import(
25
+ /* webpackChunkName: "segment-analytics" */
26
+ "@segment/analytics-next"
27
+ );
28
+ this.analytics = new Analytics({
29
+ writeKey: this.config.writeKey,
30
+ ...this.config.options
31
+ });
32
+ this.isInitialized = true;
33
+ }
34
+ async track(event, properties = {}, _context) {
35
+ if (!this.analytics) return;
36
+ await this.analytics.track({
37
+ event,
38
+ properties
39
+ });
40
+ }
41
+ async identify(userId, traits = {}, _context) {
42
+ if (!this.analytics) return;
43
+ await this.analytics.identify({
44
+ userId,
45
+ traits
46
+ });
47
+ }
48
+ async page(name, properties = {}, _context) {
49
+ if (!this.analytics) return;
50
+ await this.analytics.page({
51
+ ...name && { name },
52
+ properties
53
+ });
54
+ }
55
+ async group(groupId, traits = {}, _context) {
56
+ if (!this.analytics) return;
57
+ await this.analytics.group({
58
+ groupId,
59
+ traits
60
+ });
61
+ }
62
+ async alias(userId, previousId, _context) {
63
+ if (!this.analytics) return;
64
+ await this.analytics.alias({
65
+ userId,
66
+ previousId
67
+ });
68
+ }
69
+ };
70
+
71
+ //#endregion
72
+ //#region ../../integrations/vercel/src/analytics-provider/server.ts
73
+ var VercelServerProvider = class {
74
+ name = "vercel";
75
+ isInitialized = false;
76
+ config;
77
+ constructor(config) {
78
+ this.config = config;
79
+ }
80
+ async initialize() {
81
+ if (this.isInitialized) return;
82
+ this.isInitialized = true;
83
+ }
84
+ async track(event, properties = {}) {
85
+ if (!this.isInitialized) return;
86
+ this.config;
87
+ }
88
+ async identify(userId, traits = {}) {
89
+ await this.track("User Identified", {
90
+ userId,
91
+ ...traits
92
+ });
93
+ }
94
+ async page(name, properties = {}) {
95
+ await this.track("Page View (Server)", {
96
+ page: name,
97
+ ...properties
98
+ });
99
+ }
100
+ async group(groupId, traits = {}) {
101
+ await this.track("Group Identified", {
102
+ groupId,
103
+ ...traits
104
+ });
105
+ }
106
+ async alias(userId, previousId) {
107
+ await this.track("User Aliased", {
108
+ previousId,
109
+ userId
110
+ });
111
+ }
112
+ };
113
+
114
+ //#endregion
115
+ //#region src/server/manager.ts
116
+ /**
117
+ * @fileoverview Server analytics manager with static provider registry
118
+ * Server analytics manager with static provider registry
119
+ */
120
+ const SERVER_PROVIDERS = {
121
+ console: (config) => new ConsoleProvider(config),
122
+ http: (config) => new HttpServerProvider(config),
123
+ segment: (config) => new SegmentServerProvider(config),
124
+ vercel: (config) => new VercelServerProvider(config)
125
+ };
126
+ /**
127
+ * Create and initialize a server analytics instance
128
+ * This is the primary way to create analytics for server-side applications
129
+ *
130
+ * @example
131
+ * ```typescript
132
+ * const analytics = await createServerAnalytics({
133
+ * providers: {
134
+ * segment: { writeKey: process.env.SEGMENT_KEY! },
135
+ * },
136
+ * });
137
+ * await analytics.page('/admin', { title: 'Admin Dashboard' });
138
+ * ```
139
+ * @param config - Analytics configuration including providers and settings
140
+ * @returns Promise resolving to initialized analytics manager
141
+ */
142
+ async function createServerAnalytics(config) {
143
+ const manager = createAnalyticsManager(config, SERVER_PROVIDERS);
144
+ await manager.initialize();
145
+ return manager;
146
+ }
147
+ /**
148
+ * Create a server analytics instance without initializing
149
+ * Useful when you need to control initialization timing
150
+ *
151
+ * @example
152
+ * ```typescript
153
+ * const analytics = createServerAnalyticsUninitialized(config);
154
+ * if (shouldEmit) {
155
+ * await analytics.initialize();
156
+ * await analytics.track('CRON Completed');
157
+ * }
158
+ * ```
159
+ * @param config - Analytics configuration including providers and settings
160
+ * @returns Uninitialized analytics manager instance
161
+ */
162
+ function createServerAnalyticsUninitialized(config) {
163
+ return createAnalyticsManager(config, SERVER_PROVIDERS);
164
+ }
165
+
166
+ //#endregion
167
+ //#region src/shared/utils/validation.ts
168
+ /**
169
+ * @fileoverview Validation utilities for analytics configuration
170
+ *
171
+ * This module provides comprehensive validation for analytics configurations,
172
+ * including provider validation, environment-specific checks, and helpful
173
+ * warnings for common misconfigurations.
174
+ *
175
+ * **Features**:
176
+ * - Configuration structure validation
177
+ * - Provider-specific field validation
178
+ * - Environment variable validation
179
+ * - Environment-specific warnings
180
+ * - Detailed error reporting
181
+ *
182
+ * @module @od-oneapp/analytics/shared/utils/validation
183
+ */
184
+ /**
185
+ * Comprehensive configuration validation.
186
+ *
187
+ * Validates the entire analytics configuration structure, including:
188
+ * - Configuration object structure
189
+ * - Providers object existence
190
+ * - Individual provider configurations
191
+ * - Environment-specific warnings
192
+ *
193
+ * Accepts `unknown` input for defensive validation of potentially malformed configs.
194
+ *
195
+ * @param {unknown} config - Analytics configuration to validate
196
+ * @returns {ValidationResult} Validation result with errors, warnings, and validity status
197
+ *
198
+ * @example
199
+ * ```typescript
200
+ * const result = validateAnalyticsConfig(config);
201
+ * if (!result.isValid) {
202
+ * console.error('Validation errors:', result.errors);
203
+ * }
204
+ * if (result.warnings.length > 0) {
205
+ * console.warn('Warnings:', result.warnings);
206
+ * }
207
+ * ```
208
+ */
209
+ function validateAnalyticsConfig(config) {
210
+ const errors = [];
211
+ const warnings = [];
212
+ if (!config || typeof config !== "object") {
213
+ errors.push({
214
+ provider: "global",
215
+ field: "config",
216
+ message: "Analytics configuration is required and must be an object"
217
+ });
218
+ return {
219
+ isValid: false,
220
+ errors,
221
+ warnings
222
+ };
223
+ }
224
+ const typedConfig = config;
225
+ if (!typedConfig.providers || typeof typedConfig.providers !== "object") {
226
+ errors.push({
227
+ provider: "global",
228
+ field: "providers",
229
+ message: "Providers configuration is required and must be an object"
230
+ });
231
+ return {
232
+ isValid: false,
233
+ errors,
234
+ warnings
235
+ };
236
+ }
237
+ if (Object.keys(typedConfig.providers).length === 0) warnings.push("No providers configured. Analytics will not track any events.");
238
+ for (const [providerName, providerConfig] of Object.entries(typedConfig.providers)) {
239
+ const providerErrors = validateProvider(providerName, providerConfig);
240
+ errors.push(...providerErrors);
241
+ }
242
+ const isBrowser = typeof window !== "undefined";
243
+ if (isBrowser && typedConfig.providers.mixpanel) warnings.push("Mixpanel provider configured on client-side. Consider using server-side for better performance.");
244
+ if (!isBrowser && typedConfig.providers.vercel) warnings.push("Vercel Analytics has limited server-side support. Consider using client-side for better features.");
245
+ return {
246
+ isValid: errors.length === 0,
247
+ errors,
248
+ warnings
249
+ };
250
+ }
251
+ /**
252
+ * Validate a single provider configuration.
253
+ *
254
+ * Checks that the provider is known and that all required fields are present.
255
+ *
256
+ * @param {string} providerName - Name of the provider to validate
257
+ * @param {ProviderConfig} config - Provider configuration to validate
258
+ * @returns {ValidationError[]} Array of validation errors (empty if valid)
259
+ *
260
+ * @example
261
+ * ```typescript
262
+ * const errors = validateProvider('posthog', { apiKey: 'phc_xxx' });
263
+ * if (errors.length > 0) {
264
+ * console.error('Provider errors:', errors);
265
+ * }
266
+ * ```
267
+ */
268
+ function validateProvider(providerName, config) {
269
+ const errors = [];
270
+ const knownProviders = [
271
+ "segment",
272
+ "posthog",
273
+ "vercel",
274
+ "console",
275
+ "mixpanel"
276
+ ];
277
+ if (!knownProviders.includes(providerName)) {
278
+ errors.push({
279
+ provider: providerName,
280
+ field: "name",
281
+ message: `Unknown provider '${providerName}'. Known providers: ${knownProviders.join(", ")}`
282
+ });
283
+ return errors;
284
+ }
285
+ const requiredFields = PROVIDER_REQUIREMENTS[providerName] ?? [];
286
+ for (const field of requiredFields) {
287
+ const value = config[field];
288
+ if (!value) errors.push({
289
+ provider: providerName,
290
+ field,
291
+ message: `Required field '${field}' is missing for provider '${providerName}'`
292
+ });
293
+ else if (typeof value === "string" && value.trim() === "") errors.push({
294
+ provider: providerName,
295
+ field,
296
+ message: `Required field '${field}' cannot be empty for provider '${providerName}'`
297
+ });
298
+ }
299
+ switch (providerName) {
300
+ case "segment":
301
+ if (config.writeKey && !isValidSegmentWriteKey(config.writeKey)) errors.push({
302
+ provider: providerName,
303
+ field: "writeKey",
304
+ message: "Segment writeKey appears to be invalid format"
305
+ });
306
+ break;
307
+ case "posthog":
308
+ if (config.apiKey && !isValidPostHogApiKey(config.apiKey)) errors.push({
309
+ provider: providerName,
310
+ field: "apiKey",
311
+ message: "PostHog apiKey appears to be invalid format"
312
+ });
313
+ break;
314
+ }
315
+ return errors;
316
+ }
317
+ /**
318
+ * Validates Segment write key format.
319
+ *
320
+ * Checks that the write key matches Segment's expected format:
321
+ * - Exactly 32 alphanumeric characters
322
+ * - Not a placeholder value
323
+ *
324
+ * @param {string} writeKey - Write key to validate
325
+ * @returns {boolean} `true` if valid, `false` otherwise
326
+ *
327
+ * @internal
328
+ */
329
+ function isValidSegmentWriteKey(writeKey) {
330
+ if (!/^[\dA-Za-z]{32}$/.test(writeKey)) return false;
331
+ if ([
332
+ "your-write-key",
333
+ "paste-key-here",
334
+ "xxxxxxxx",
335
+ "example"
336
+ ].some((p) => writeKey.toLowerCase().includes(p.toLowerCase()))) return false;
337
+ return true;
338
+ }
339
+ /**
340
+ * Validates PostHog API key format.
341
+ *
342
+ * Checks that the API key matches PostHog's expected format:
343
+ * - Starts with `phc_` prefix
344
+ * - Followed by 43 alphanumeric/dash/underscore characters
345
+ * - Not a placeholder value
346
+ *
347
+ * @param {string} apiKey - API key to validate
348
+ * @returns {boolean} `true` if valid, `false` otherwise
349
+ *
350
+ * @internal
351
+ */
352
+ function isValidPostHogApiKey(apiKey) {
353
+ if (!/^phc_[\w-]{43}$/.test(apiKey)) return false;
354
+ if ([
355
+ "your-api-key",
356
+ "paste-key-here",
357
+ "xxxxxxxx",
358
+ "example"
359
+ ].some((p) => apiKey.toLowerCase().includes(p.toLowerCase()))) return false;
360
+ return true;
361
+ }
362
+ /**
363
+ * Utility to throw validation errors (for strict validation).
364
+ *
365
+ * Validates the configuration and throws an error if validation fails.
366
+ * Useful for ensuring configuration is valid before using analytics.
367
+ *
368
+ * @param {AnalyticsConfig} config - Analytics configuration to validate
369
+ * @throws {Error} If configuration validation fails
370
+ *
371
+ * @example
372
+ * ```typescript
373
+ * try {
374
+ * validateConfigOrThrow(config);
375
+ * // Configuration is valid, proceed
376
+ * } catch (error) {
377
+ * console.error('Invalid config:', error.message);
378
+ * }
379
+ * ```
380
+ */
381
+ function validateConfigOrThrow(config) {
382
+ const result = validateAnalyticsConfig(config);
383
+ if (!result.isValid) {
384
+ const errorMessages = result.errors.map((error) => `${error.provider}.${error.field}: ${error.message}`).join("\n");
385
+ throw new Error(`Analytics configuration validation failed:\n${errorMessages}`);
386
+ }
387
+ if (result.warnings.length > 0 && config.onError) config.onError(/* @__PURE__ */ new Error("Analytics configuration warnings"), {
388
+ provider: "analytics",
389
+ method: "validateConfig",
390
+ warnings: result.warnings
391
+ });
392
+ }
393
+ /**
394
+ * Development helper to check configuration.
395
+ *
396
+ * Logs configuration details and validation results for debugging.
397
+ * Only useful in development environments.
398
+ *
399
+ * @param {AnalyticsConfig} config - Analytics configuration to debug
400
+ * @returns {Promise<void>} Promise that resolves when debugging is complete
401
+ *
402
+ * @example
403
+ * ```typescript
404
+ * if (process.env.NODE_ENV === 'development') {
405
+ * await debugConfig(config);
406
+ * }
407
+ * ```
408
+ */
409
+ async function debugConfig(config) {
410
+ const result = validateAnalyticsConfig(config);
411
+ logInfo("Analytics Configuration Debug", {
412
+ config,
413
+ validationResult: result
414
+ });
415
+ if (result.errors.length > 0) logError("Analytics configuration errors: Validation failed", { errors: result.errors });
416
+ if (result.warnings.length > 0) logWarn("Analytics configuration warnings", { warnings: result.warnings });
417
+ }
418
+
419
+ //#endregion
420
+ //#region src/shared/ingestion/schemas.ts
421
+ /**
422
+ * @fileoverview Event Ingestion Schemas and Validation
423
+ *
424
+ * Defines Zod schemas for validating event ingestion payloads. These schemas
425
+ * ensure type-safe, secure event ingestion with proper validation.
426
+ *
427
+ * **Key Features**:
428
+ * - CloudEvents-style fields for future interoperability
429
+ * - Support for single events and batched arrays
430
+ * - Strict validation of required fields
431
+ * - Typed extensions per event category
432
+ *
433
+ * @module @od-oneapp/analytics/shared/ingestion/schemas
434
+ */
435
+ /**
436
+ * Property value schema - safe, serializable values only.
437
+ */
438
+ const PropertyValueSchema = z.union([
439
+ z.string(),
440
+ z.number(),
441
+ z.boolean(),
442
+ z.null(),
443
+ z.date()
444
+ ]);
445
+ /**
446
+ * Property object schema with nested structure support.
447
+ */
448
+ const PropertyObjectSchema = z.record(z.string(), z.lazy(() => z.union([
449
+ PropertyValueSchema,
450
+ z.array(PropertyValueSchema),
451
+ z.record(z.string(), z.unknown())
452
+ ])));
453
+ /**
454
+ * Emitter context schema - contextual information about the environment.
455
+ */
456
+ const EmitterContextSchema = z.object({
457
+ app: z.object({
458
+ name: z.string().optional(),
459
+ version: z.string().optional(),
460
+ build: z.string().optional(),
461
+ namespace: z.string().optional()
462
+ }).optional(),
463
+ campaign: z.object({
464
+ name: z.string().optional(),
465
+ source: z.string().optional(),
466
+ medium: z.string().optional(),
467
+ term: z.string().optional(),
468
+ content: z.string().optional()
469
+ }).passthrough().optional(),
470
+ device: z.object({
471
+ id: z.string().optional(),
472
+ manufacturer: z.string().optional(),
473
+ model: z.string().optional(),
474
+ name: z.string().optional(),
475
+ type: z.string().optional(),
476
+ version: z.string().optional()
477
+ }).optional(),
478
+ ip: z.string().optional(),
479
+ library: z.object({
480
+ name: z.string(),
481
+ version: z.string()
482
+ }).optional(),
483
+ locale: z.string().optional(),
484
+ network: z.object({
485
+ bluetooth: z.boolean().optional(),
486
+ carrier: z.string().optional(),
487
+ cellular: z.boolean().optional(),
488
+ wifi: z.boolean().optional()
489
+ }).optional(),
490
+ os: z.object({
491
+ name: z.string().optional(),
492
+ version: z.string().optional()
493
+ }).optional(),
494
+ page: z.object({
495
+ path: z.string().optional(),
496
+ referrer: z.string().optional(),
497
+ search: z.string().optional(),
498
+ title: z.string().optional(),
499
+ url: z.string().optional()
500
+ }).optional(),
501
+ screen: z.object({
502
+ density: z.number().optional(),
503
+ height: z.number().int().positive().optional(),
504
+ width: z.number().int().positive().optional()
505
+ }).optional(),
506
+ timezone: z.string().optional(),
507
+ groupId: z.string().optional(),
508
+ userAgent: z.string().optional(),
509
+ channel: z.enum([
510
+ "server",
511
+ "browser",
512
+ "mobile",
513
+ "api"
514
+ ]).optional(),
515
+ location: z.object({
516
+ city: z.string().optional(),
517
+ country: z.string().optional(),
518
+ latitude: z.number().optional(),
519
+ longitude: z.number().optional(),
520
+ region: z.string().optional()
521
+ }).optional()
522
+ }).passthrough();
523
+ /**
524
+ * Base payload schema - common fields for all event types.
525
+ */
526
+ const BasePayloadSchema = z.object({
527
+ anonymousId: z.string().max(255).optional(),
528
+ userId: z.string().max(255).optional(),
529
+ timestamp: z.union([z.string().datetime(), z.date()]).optional(),
530
+ originalTimestamp: z.union([z.string().datetime(), z.date()]).optional(),
531
+ context: EmitterContextSchema.optional(),
532
+ messageId: z.string().uuid().optional()
533
+ }).refine((data) => Boolean(data.anonymousId) || Boolean(data.userId), { message: "Either anonymousId or userId must be provided" });
534
+ /**
535
+ * Track event payload schema.
536
+ * Uses safeExtend() because BasePayloadSchema has refinements (Zod v4 requirement).
537
+ */
538
+ const TrackEventSchema = BasePayloadSchema.safeExtend({
539
+ type: z.literal("track"),
540
+ event: z.string().min(1).max(255),
541
+ properties: PropertyObjectSchema.optional()
542
+ });
543
+ /**
544
+ * Identify event payload schema.
545
+ * Uses safeExtend() because BasePayloadSchema has refinements (Zod v4 requirement).
546
+ */
547
+ const IdentifyEventSchema = BasePayloadSchema.safeExtend({
548
+ type: z.literal("identify"),
549
+ userId: z.string().min(1).max(255),
550
+ traits: PropertyObjectSchema.optional()
551
+ });
552
+ /**
553
+ * Page event payload schema.
554
+ * Uses safeExtend() because BasePayloadSchema has refinements (Zod v4 requirement).
555
+ */
556
+ const PageEventSchema = BasePayloadSchema.safeExtend({
557
+ type: z.literal("page"),
558
+ name: z.string().max(255).optional(),
559
+ category: z.string().max(255).optional(),
560
+ properties: PropertyObjectSchema.optional()
561
+ });
562
+ /**
563
+ * Screen event payload schema.
564
+ * Uses safeExtend() because BasePayloadSchema has refinements (Zod v4 requirement).
565
+ */
566
+ const ScreenEventSchema = BasePayloadSchema.safeExtend({
567
+ type: z.literal("screen"),
568
+ name: z.string().max(255).optional(),
569
+ category: z.string().max(255).optional(),
570
+ properties: PropertyObjectSchema.optional()
571
+ });
572
+ /**
573
+ * Group event payload schema.
574
+ * Uses safeExtend() because BasePayloadSchema has refinements (Zod v4 requirement).
575
+ */
576
+ const GroupEventSchema = BasePayloadSchema.safeExtend({
577
+ type: z.literal("group"),
578
+ groupId: z.string().min(1).max(255),
579
+ traits: PropertyObjectSchema.optional()
580
+ });
581
+ /**
582
+ * Alias event payload schema.
583
+ * Uses safeExtend() because BasePayloadSchema has refinements (Zod v4 requirement).
584
+ */
585
+ const AliasEventSchema = BasePayloadSchema.safeExtend({
586
+ type: z.literal("alias"),
587
+ userId: z.string().min(1).max(255),
588
+ previousId: z.string().min(1).max(255)
589
+ });
590
+ /**
591
+ * Union of all event payload schemas.
592
+ */
593
+ const EventPayloadSchema = z.discriminatedUnion("type", [
594
+ TrackEventSchema,
595
+ IdentifyEventSchema,
596
+ PageEventSchema,
597
+ ScreenEventSchema,
598
+ GroupEventSchema,
599
+ AliasEventSchema
600
+ ]);
601
+ /**
602
+ * Single event ingestion request schema.
603
+ */
604
+ const SingleEventRequestSchema = EventPayloadSchema;
605
+ /**
606
+ * Batch event ingestion request schema.
607
+ *
608
+ * The batch size limit is enforced at the service level via `maxBatchSize` config
609
+ * (default: 100) rather than in the schema, allowing for flexible configuration.
610
+ */
611
+ const BatchEventRequestSchema = z.object({ batch: z.array(EventPayloadSchema).min(1) });
612
+ /**
613
+ * Combined ingestion request schema - accepts single event or batch.
614
+ */
615
+ const IngestionRequestSchema = z.union([BatchEventRequestSchema, SingleEventRequestSchema]);
616
+ /**
617
+ * Event processing result.
618
+ */
619
+ const EventResultSchema = z.object({
620
+ id: z.string().uuid(),
621
+ messageId: z.string().uuid().optional(),
622
+ type: z.enum([
623
+ "track",
624
+ "identify",
625
+ "page",
626
+ "screen",
627
+ "group",
628
+ "alias"
629
+ ]),
630
+ status: z.enum(["accepted", "rejected"]),
631
+ error: z.string().optional()
632
+ });
633
+ /**
634
+ * Successful ingestion response schema.
635
+ */
636
+ const IngestionSuccessResponseSchema = z.object({
637
+ success: z.literal(true),
638
+ accepted: z.number().int().nonnegative(),
639
+ rejected: z.number().int().nonnegative(),
640
+ results: z.array(EventResultSchema),
641
+ receivedAt: z.string().datetime()
642
+ });
643
+ /**
644
+ * Error response schema.
645
+ */
646
+ const IngestionErrorResponseSchema = z.object({
647
+ success: z.literal(false),
648
+ code: z.string(),
649
+ error: z.string(),
650
+ fieldErrors: z.array(z.object({
651
+ path: z.array(z.union([z.string(), z.number()])),
652
+ message: z.string()
653
+ })).optional()
654
+ });
655
+ /**
656
+ * Combined response schema.
657
+ */
658
+ const IngestionResponseSchema = z.union([IngestionSuccessResponseSchema, IngestionErrorResponseSchema]);
659
+
660
+ //#endregion
661
+ //#region src/shared/ingestion/service.ts
662
+ /**
663
+ * @fileoverview Event Ingestion Service
664
+ *
665
+ * Provides server-side event ingestion functionality. Handles validation,
666
+ * normalization, and forwarding of events to the analytics system.
667
+ *
668
+ * **Key Features**:
669
+ * - Validates incoming events against Zod schemas
670
+ * - Normalizes event payloads with defaults
671
+ * - Forwards events to AnalyticsManager
672
+ * - Tracks ingestion metrics
673
+ * - Handles batch processing efficiently
674
+ *
675
+ * @module @od-oneapp/analytics/shared/ingestion/service
676
+ */
677
+ const DEFAULT_CONFIG = {
678
+ maxBatchSize: 100,
679
+ maxPayloadSize: 1024 * 1024,
680
+ stripPII: true,
681
+ stripHTML: true,
682
+ eventTimeout: 5e3,
683
+ batchConcurrency: 10
684
+ };
685
+ /**
686
+ * Generate a UUID v4.
687
+ */
688
+ function generateUUID() {
689
+ return crypto.randomUUID();
690
+ }
691
+ /**
692
+ * Get current ISO timestamp.
693
+ */
694
+ function getCurrentTimestamp() {
695
+ return (/* @__PURE__ */ new Date()).toISOString();
696
+ }
697
+ /**
698
+ * Check if request is a batch request.
699
+ */
700
+ function isBatchRequest(request) {
701
+ return "batch" in request && Array.isArray(request.batch);
702
+ }
703
+ /**
704
+ * Normalize an event payload with defaults and server metadata.
705
+ */
706
+ function normalizeEvent(event, context, receivedAt) {
707
+ const normalized = { ...event };
708
+ if (!normalized.messageId) normalized.messageId = generateUUID();
709
+ if (!normalized.timestamp) normalized.timestamp = receivedAt;
710
+ if (!normalized.originalTimestamp) normalized.originalTimestamp = normalized.timestamp;
711
+ normalized.context = {
712
+ ...normalized.context,
713
+ channel: normalized.context?.channel ?? "api",
714
+ ip: context.ip ?? normalized.context?.ip,
715
+ userAgent: context.userAgent ?? normalized.context?.userAgent,
716
+ library: normalized.context?.library ?? {
717
+ name: context.source,
718
+ version: context.sdkVersion ?? "unknown"
719
+ }
720
+ };
721
+ if (!normalized.userId && context.userId) normalized.userId = context.userId;
722
+ return normalized;
723
+ }
724
+ /**
725
+ * Convert EventPayload to EmitterPayload format for AnalyticsManager.
726
+ * Explicitly maps fields to ensure type safety.
727
+ */
728
+ function toEmitterPayload(event) {
729
+ const basePayload = {
730
+ userId: event.userId,
731
+ anonymousId: event.anonymousId,
732
+ timestamp: event.timestamp,
733
+ context: event.context,
734
+ messageId: event.messageId
735
+ };
736
+ switch (event.type) {
737
+ case "track": return {
738
+ type: "track",
739
+ event: event.event,
740
+ properties: event.properties,
741
+ ...basePayload
742
+ };
743
+ case "identify": return {
744
+ type: "identify",
745
+ traits: event.traits,
746
+ ...basePayload
747
+ };
748
+ case "page": return {
749
+ type: "page",
750
+ name: event.name,
751
+ category: event.category,
752
+ properties: event.properties,
753
+ ...basePayload
754
+ };
755
+ case "screen": return {
756
+ type: "screen",
757
+ name: event.name,
758
+ category: event.category,
759
+ properties: event.properties,
760
+ ...basePayload
761
+ };
762
+ case "group": return {
763
+ type: "group",
764
+ groupId: event.groupId,
765
+ traits: event.traits,
766
+ ...basePayload
767
+ };
768
+ case "alias": return {
769
+ type: "alias",
770
+ previousId: event.previousId,
771
+ ...basePayload
772
+ };
773
+ default: throw new Error(`Unknown event type: ${event.type}`);
774
+ }
775
+ }
776
+ /**
777
+ * Event Ingestion Service.
778
+ *
779
+ * Handles validation, normalization, and forwarding of analytics events.
780
+ * Designed for high-volume ingestion with batching and rate limiting support.
781
+ *
782
+ * @example
783
+ * ```typescript
784
+ * const service = new IngestionService(analyticsManager);
785
+ *
786
+ * const result = await service.ingest(requestBody, {
787
+ * source: 'web-app',
788
+ * tenantId: 'tenant-123',
789
+ * userId: 'user-456',
790
+ * });
791
+ *
792
+ * if (result.success) {
793
+ * console.log(`Accepted ${result.accepted} events`);
794
+ * }
795
+ * ```
796
+ */
797
+ var IngestionService = class {
798
+ config;
799
+ constructor(analyticsManager, config = {}) {
800
+ this.analyticsManager = analyticsManager;
801
+ this.config = {
802
+ ...DEFAULT_CONFIG,
803
+ ...config
804
+ };
805
+ }
806
+ /**
807
+ * Parse and validate the ingestion request payload.
808
+ *
809
+ * @param payload - Raw request payload (parsed JSON)
810
+ * @returns Parsed and validated request, or error response
811
+ */
812
+ parseRequest(payload) {
813
+ try {
814
+ const payloadSize = JSON.stringify(payload).length;
815
+ if (payloadSize > this.config.maxPayloadSize) return {
816
+ success: false,
817
+ error: {
818
+ success: false,
819
+ code: "PAYLOAD_TOO_LARGE",
820
+ error: `Payload size ${payloadSize} exceeds maximum ${this.config.maxPayloadSize} bytes`
821
+ }
822
+ };
823
+ } catch {
824
+ return {
825
+ success: false,
826
+ error: {
827
+ success: false,
828
+ code: "INVALID_JSON",
829
+ error: "Failed to serialize payload for size check"
830
+ }
831
+ };
832
+ }
833
+ const result = IngestionRequestSchema.safeParse(payload);
834
+ if (!result.success) return {
835
+ success: false,
836
+ error: {
837
+ success: false,
838
+ code: "VALIDATION_ERROR",
839
+ error: "Request validation failed",
840
+ fieldErrors: result.error.issues.map((err) => ({
841
+ path: err.path.map((p) => typeof p === "symbol" ? String(p) : p),
842
+ message: err.message
843
+ }))
844
+ }
845
+ };
846
+ if (isBatchRequest(result.data) && result.data.batch.length > this.config.maxBatchSize) return {
847
+ success: false,
848
+ error: {
849
+ success: false,
850
+ code: "BATCH_TOO_LARGE",
851
+ error: `Batch size ${result.data.batch.length} exceeds maximum ${this.config.maxBatchSize}`
852
+ }
853
+ };
854
+ return {
855
+ success: true,
856
+ data: result.data
857
+ };
858
+ }
859
+ /**
860
+ * Ingest events from a validated request.
861
+ *
862
+ * @param request - Validated ingestion request
863
+ * @param context - Ingestion context from the caller
864
+ * @returns Ingestion response with per-event results
865
+ */
866
+ async ingest(request, context) {
867
+ const startTime = process.hrtime.bigint();
868
+ const receivedAt = getCurrentTimestamp();
869
+ const events = isBatchRequest(request) ? request.batch : [request];
870
+ const results = [];
871
+ const metrics = {
872
+ totalReceived: events.length,
873
+ accepted: 0,
874
+ rejected: 0,
875
+ byType: {},
876
+ processingTimeMs: 0
877
+ };
878
+ const processedEvents = [];
879
+ for (const event of events) {
880
+ const eventId = generateUUID();
881
+ try {
882
+ if (event.type === "track") {
883
+ const validation = validateEventName(event.event);
884
+ if (!validation.valid) {
885
+ results.push({
886
+ id: eventId,
887
+ messageId: event.messageId,
888
+ type: event.type,
889
+ status: "rejected",
890
+ error: `Invalid event name: ${validation.reason}`
891
+ });
892
+ metrics.rejected++;
893
+ continue;
894
+ }
895
+ }
896
+ const normalized = normalizeEvent(event, context, receivedAt);
897
+ if ("properties" in normalized && normalized.properties) {
898
+ const sanitized = sanitizeProperties(normalized.properties, {
899
+ stripPII: this.config.stripPII,
900
+ stripHTML: this.config.stripHTML,
901
+ allowDangerousKeys: false
902
+ });
903
+ normalized.properties = sanitized.data;
904
+ if (sanitized.warnings.length > 0) logDebug$1("Event properties sanitized", {
905
+ eventId,
906
+ warnings: sanitized.warnings
907
+ });
908
+ }
909
+ if ("traits" in normalized && normalized.traits) normalized.traits = sanitizeProperties(normalized.traits, {
910
+ stripPII: this.config.stripPII,
911
+ stripHTML: this.config.stripHTML,
912
+ allowDangerousKeys: false
913
+ }).data;
914
+ const emitterPayload = toEmitterPayload(normalized);
915
+ processedEvents.push(emitterPayload);
916
+ results.push({
917
+ id: eventId,
918
+ messageId: normalized.messageId,
919
+ type: event.type,
920
+ status: "accepted"
921
+ });
922
+ metrics.accepted++;
923
+ metrics.byType[event.type] = (metrics.byType[event.type] ?? 0) + 1;
924
+ } catch (error) {
925
+ const errorMessage = error instanceof Error ? error.message : "Unknown error";
926
+ results.push({
927
+ id: eventId,
928
+ messageId: event.messageId,
929
+ type: event.type,
930
+ status: "rejected",
931
+ error: errorMessage
932
+ });
933
+ metrics.rejected++;
934
+ logWarn$1("Event processing failed", {
935
+ eventId,
936
+ type: event.type,
937
+ error: errorMessage
938
+ });
939
+ }
940
+ }
941
+ if (processedEvents.length > 0) (async () => {
942
+ try {
943
+ await this.analyticsManager.emitBatch(processedEvents, {
944
+ timeout: this.config.eventTimeout,
945
+ concurrency: this.config.batchConcurrency,
946
+ failFast: false
947
+ });
948
+ } catch (error) {
949
+ logError$1("Failed to emit events to analytics manager", {
950
+ error: error instanceof Error ? error.message : "Unknown error",
951
+ eventCount: processedEvents.length,
952
+ traceId: context.traceId
953
+ });
954
+ }
955
+ })();
956
+ const endTime = process.hrtime.bigint();
957
+ metrics.processingTimeMs = Number(endTime - startTime) / 1e6;
958
+ logInfo$1("Event ingestion completed", {
959
+ traceId: context.traceId,
960
+ source: context.source,
961
+ tenantId: context.tenantId,
962
+ totalReceived: metrics.totalReceived,
963
+ accepted: metrics.accepted,
964
+ rejected: metrics.rejected,
965
+ processingTimeMs: metrics.processingTimeMs.toFixed(2),
966
+ byType: metrics.byType
967
+ });
968
+ return {
969
+ success: true,
970
+ accepted: metrics.accepted,
971
+ rejected: metrics.rejected,
972
+ results,
973
+ receivedAt
974
+ };
975
+ }
976
+ /**
977
+ * Process a raw request body through parsing and ingestion.
978
+ *
979
+ * Convenience method that combines parseRequest and ingest.
980
+ *
981
+ * @param payload - Raw request payload
982
+ * @param context - Ingestion context
983
+ * @returns Ingestion response (success or error)
984
+ */
985
+ async processRequest(payload, context) {
986
+ const parseResult = this.parseRequest(payload);
987
+ if (!parseResult.success) return parseResult.error;
988
+ return this.ingest(parseResult.data, context);
989
+ }
990
+ };
991
+ /**
992
+ * Create an ingestion service instance.
993
+ *
994
+ * @param analyticsManager - Initialized AnalyticsManager instance
995
+ * @param config - Optional service configuration
996
+ * @returns Configured IngestionService instance
997
+ *
998
+ * @example
999
+ * ```typescript
1000
+ * import { createServerAnalytics } from '@od-oneapp/analytics/server';
1001
+ * import { createIngestionService } from '@od-oneapp/analytics/server';
1002
+ *
1003
+ * const analytics = await createServerAnalytics(config);
1004
+ * const ingestionService = createIngestionService(analytics);
1005
+ * ```
1006
+ */
1007
+ function createIngestionService(analyticsManager, config) {
1008
+ return new IngestionService(analyticsManager, config);
1009
+ }
1010
+ /**
1011
+ * Validate a single event payload.
1012
+ *
1013
+ * Useful for pre-validation before queuing events.
1014
+ *
1015
+ * @param payload - Event payload to validate
1016
+ * @returns Validation result with parsed data or errors
1017
+ */
1018
+ function validateEventPayload(payload) {
1019
+ const result = EventPayloadSchema.safeParse(payload);
1020
+ if (!result.success) return {
1021
+ success: false,
1022
+ errors: result.error.issues.map((e) => `${e.path.map((p) => typeof p === "symbol" ? String(p) : p).join(".")}: ${e.message}`)
1023
+ };
1024
+ return {
1025
+ success: true,
1026
+ data: result.data
1027
+ };
1028
+ }
1029
+ /**
1030
+ * Validate a batch of event payloads.
1031
+ *
1032
+ * @param payloads - Array of event payloads to validate
1033
+ * @returns Validation result with parsed data or errors
1034
+ */
1035
+ function validateBatchPayload(payloads) {
1036
+ const result = BatchEventRequestSchema.safeParse({ batch: payloads });
1037
+ if (!result.success) return {
1038
+ success: false,
1039
+ errors: result.error.issues.map((e) => `${e.path.map((p) => typeof p === "symbol" ? String(p) : p).join(".")}: ${e.message}`)
1040
+ };
1041
+ return {
1042
+ success: true,
1043
+ data: result.data.batch
1044
+ };
1045
+ }
1046
+
1047
+ //#endregion
1048
+ export { debugConfig as a, validateProvider as c, validateEventPayload as i, createServerAnalytics as l, createIngestionService as n, validateAnalyticsConfig as o, validateBatchPayload as r, validateConfigOrThrow as s, IngestionService as t, createServerAnalyticsUninitialized as u };
1049
+ //# sourceMappingURL=service-Duqnlppl.mjs.map