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