@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,848 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Node.js 22+ Advanced Analytics Features
|
|
3
|
+
* Node.js 22+ Advanced Analytics Features
|
|
4
|
+
*
|
|
5
|
+
* Enterprise-grade analytics enhancements leveraging Node.js 22+ features
|
|
6
|
+
* for superior performance, memory efficiency, and developer experience.
|
|
7
|
+
*
|
|
8
|
+
* ## Key Node 22+ Features Used:
|
|
9
|
+
* - **Promise.withResolvers()**: External promise control for complex analytics workflows
|
|
10
|
+
* - **AbortSignal.timeout()**: Context-aware timeouts for analytics operations
|
|
11
|
+
* - **structuredClone()**: Safe event data isolation and batch processing
|
|
12
|
+
* - **Object.hasOwn()**: Safer property existence checks for event validation
|
|
13
|
+
* - **WeakMap/WeakSet**: Memory-efficient provider and event tracking
|
|
14
|
+
* - **High-resolution timing**: Nanosecond precision performance measurements
|
|
15
|
+
* - **FinalizationRegistry**: Automatic cleanup of analytics resources
|
|
16
|
+
*
|
|
17
|
+
* ## Enhanced Capabilities:
|
|
18
|
+
* - Advanced event batching with configurable strategies
|
|
19
|
+
* - Real-time analytics performance monitoring
|
|
20
|
+
* - Memory-efficient event queuing and processing
|
|
21
|
+
* - Provider health monitoring with automatic failover
|
|
22
|
+
* - Comprehensive analytics debugging and diagnostics
|
|
23
|
+
* - Production-safe error handling with detailed telemetry
|
|
24
|
+
*
|
|
25
|
+
* @module Node22AnalyticsFeatures
|
|
26
|
+
* @version 2.0.0
|
|
27
|
+
* @since Node.js 22.0.0
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
import type {
|
|
31
|
+
AnalyticsConfig,
|
|
32
|
+
AnalyticsProvider,
|
|
33
|
+
EmitterPayload,
|
|
34
|
+
TrackingOptions,
|
|
35
|
+
} from './types/types';
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Advanced event batching configuration
|
|
39
|
+
*/
|
|
40
|
+
interface EventBatchConfig {
|
|
41
|
+
readonly maxBatchSize: number;
|
|
42
|
+
readonly batchTimeoutMs: number;
|
|
43
|
+
readonly compressionEnabled: boolean;
|
|
44
|
+
readonly retryAttempts: number;
|
|
45
|
+
readonly retryDelayMs: number;
|
|
46
|
+
readonly enablePrioritization: boolean;
|
|
47
|
+
readonly maxMemoryUsage: number; // bytes
|
|
48
|
+
readonly strategy: 'fifo' | 'lifo' | 'priority' | 'size-optimized';
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Analytics performance metrics
|
|
53
|
+
*/
|
|
54
|
+
interface AnalyticsMetrics {
|
|
55
|
+
readonly timestamp: bigint;
|
|
56
|
+
readonly eventCount: number;
|
|
57
|
+
readonly batchCount: number;
|
|
58
|
+
readonly processingTime: number; // nanoseconds
|
|
59
|
+
readonly memoryUsage: number; // bytes
|
|
60
|
+
readonly errorCount: number;
|
|
61
|
+
readonly providerHealth: Map<string, number>; // provider -> health score (0-1)
|
|
62
|
+
readonly throughputEventsPerSecond: number;
|
|
63
|
+
readonly averageLatency: number; // milliseconds
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Provider health status
|
|
68
|
+
*/
|
|
69
|
+
interface ProviderHealthStatus {
|
|
70
|
+
readonly providerId: string;
|
|
71
|
+
readonly isHealthy: boolean;
|
|
72
|
+
readonly lastCheck: bigint;
|
|
73
|
+
readonly responseTime: number; // milliseconds
|
|
74
|
+
readonly errorRate: number; // 0-1
|
|
75
|
+
readonly successRate: number; // 0-1
|
|
76
|
+
readonly consecutiveFailures: number;
|
|
77
|
+
readonly estimatedRecoveryTime?: number | undefined; // milliseconds
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Event processing result using Node 22+ features
|
|
82
|
+
*/
|
|
83
|
+
interface EventProcessingResult {
|
|
84
|
+
readonly success: boolean;
|
|
85
|
+
readonly processedAt: bigint;
|
|
86
|
+
readonly processingTime: number; // nanoseconds
|
|
87
|
+
readonly batchId?: string;
|
|
88
|
+
readonly providerId: string;
|
|
89
|
+
readonly eventId: string;
|
|
90
|
+
readonly retryAttempt: number;
|
|
91
|
+
readonly error?: Error;
|
|
92
|
+
readonly metrics: {
|
|
93
|
+
memoryUsed: number; // bytes
|
|
94
|
+
networkLatency?: number; // milliseconds
|
|
95
|
+
serializationTime: number; // nanoseconds
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Advanced event batcher using Node 22+ WeakMap and Promise.withResolvers()
|
|
101
|
+
*/
|
|
102
|
+
export class AdvancedEventBatcher {
|
|
103
|
+
private readonly eventQueue: EmitterPayload[] = [];
|
|
104
|
+
private readonly batchPromises = new Map<
|
|
105
|
+
string,
|
|
106
|
+
{ resolve: (value: EventProcessingResult[]) => void; reject: (error: Error) => void }
|
|
107
|
+
>();
|
|
108
|
+
private readonly eventMetadata = new WeakMap<
|
|
109
|
+
EmitterPayload,
|
|
110
|
+
{
|
|
111
|
+
enqueuedAt: bigint;
|
|
112
|
+
priority: number;
|
|
113
|
+
retryCount: number;
|
|
114
|
+
}
|
|
115
|
+
>();
|
|
116
|
+
|
|
117
|
+
private readonly providerHealthMap = new Map<string, ProviderHealthStatus>();
|
|
118
|
+
private readonly metrics = new Map<string, AnalyticsMetrics>();
|
|
119
|
+
private batchTimer?: NodeJS.Timeout | undefined;
|
|
120
|
+
private isProcessing = false;
|
|
121
|
+
|
|
122
|
+
// Node 22+ FinalizationRegistry for automatic cleanup
|
|
123
|
+
// eslint-disable-next-line unicorn/consistent-function-scoping
|
|
124
|
+
private readonly finalizationRegistry = new FinalizationRegistry((batchId: string) => {
|
|
125
|
+
this.cleanupBatch(batchId);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
constructor(
|
|
129
|
+
private readonly config: EventBatchConfig,
|
|
130
|
+
private readonly providers: Map<string, AnalyticsProvider>,
|
|
131
|
+
) {}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Enqueue event for batched processing with Node 22+ features
|
|
135
|
+
*/
|
|
136
|
+
async enqueueEvent(
|
|
137
|
+
payload: EmitterPayload,
|
|
138
|
+
options: TrackingOptions & { priority?: number } = {},
|
|
139
|
+
abortSignal?: AbortSignal,
|
|
140
|
+
): Promise<EventProcessingResult[]> {
|
|
141
|
+
const eventId = this.generateEventId();
|
|
142
|
+
const enqueuedAt = process.hrtime.bigint();
|
|
143
|
+
|
|
144
|
+
// Use Node 22+ Promise.withResolvers() for external control
|
|
145
|
+
// Polyfill for TypeScript lib that doesn't include es2024 yet
|
|
146
|
+
const withResolvers = (): {
|
|
147
|
+
promise: Promise<EventProcessingResult[]>;
|
|
148
|
+
resolve: (value: EventProcessingResult[]) => void;
|
|
149
|
+
reject: (reason?: unknown) => void;
|
|
150
|
+
} => {
|
|
151
|
+
let resolvePromise!: (value: EventProcessingResult[]) => void;
|
|
152
|
+
let rejectPromise!: (reason?: unknown) => void;
|
|
153
|
+
const promise = new Promise<EventProcessingResult[]>((resolve, reject) => {
|
|
154
|
+
resolvePromise = resolve;
|
|
155
|
+
rejectPromise = reject;
|
|
156
|
+
});
|
|
157
|
+
return { promise, resolve: resolvePromise, reject: rejectPromise };
|
|
158
|
+
};
|
|
159
|
+
const { promise, resolve, reject } = withResolvers();
|
|
160
|
+
|
|
161
|
+
// Store metadata using WeakMap (Node 22+)
|
|
162
|
+
this.eventMetadata.set(payload, {
|
|
163
|
+
enqueuedAt,
|
|
164
|
+
priority: options.priority ?? 0,
|
|
165
|
+
retryCount: 0,
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
// Add to queue with priority sorting if enabled
|
|
169
|
+
if (this.config.enablePrioritization) {
|
|
170
|
+
this.insertWithPriority(payload, options.priority ?? 0);
|
|
171
|
+
} else {
|
|
172
|
+
this.eventQueue.push(payload);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Store promise resolvers for this batch
|
|
176
|
+
this.batchPromises.set(eventId, { resolve, reject });
|
|
177
|
+
|
|
178
|
+
// Handle abort signal if provided
|
|
179
|
+
if (abortSignal) {
|
|
180
|
+
abortSignal.addEventListener(
|
|
181
|
+
'abort',
|
|
182
|
+
() => {
|
|
183
|
+
reject(new Error('Event processing aborted'));
|
|
184
|
+
this.removeBatchPromise(eventId);
|
|
185
|
+
},
|
|
186
|
+
{ once: true },
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Trigger batch processing if needed
|
|
191
|
+
await this.maybeProcessBatch();
|
|
192
|
+
|
|
193
|
+
return promise;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Process events in optimized batches using Node 22+ features
|
|
198
|
+
*/
|
|
199
|
+
private async maybeProcessBatch(): Promise<void> {
|
|
200
|
+
if (this.isProcessing) return;
|
|
201
|
+
|
|
202
|
+
const shouldProcess =
|
|
203
|
+
this.eventQueue.length >= this.config.maxBatchSize ||
|
|
204
|
+
this.getOldestEventAge() > this.config.batchTimeoutMs ||
|
|
205
|
+
this.isMemoryThresholdExceeded();
|
|
206
|
+
|
|
207
|
+
if (!shouldProcess) {
|
|
208
|
+
this.scheduleBatchTimeout();
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
await this.processBatch();
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Process current batch with provider failover and Node 22+ optimizations
|
|
217
|
+
*/
|
|
218
|
+
private async processBatch(): Promise<void> {
|
|
219
|
+
if (this.isProcessing || this.eventQueue.length === 0) return;
|
|
220
|
+
|
|
221
|
+
this.isProcessing = true;
|
|
222
|
+
const batchId = this.generateBatchId();
|
|
223
|
+
const startTime = process.hrtime.bigint();
|
|
224
|
+
|
|
225
|
+
try {
|
|
226
|
+
// Clone events for safe processing (Node 22+)
|
|
227
|
+
const eventsToProcess = this.eventQueue
|
|
228
|
+
.splice(0, this.config.maxBatchSize)
|
|
229
|
+
.map(event => structuredClone(event));
|
|
230
|
+
|
|
231
|
+
// Register batch for cleanup
|
|
232
|
+
const batchRef = { batchId };
|
|
233
|
+
this.finalizationRegistry.register(batchRef, batchId);
|
|
234
|
+
|
|
235
|
+
// Process events across healthy providers
|
|
236
|
+
const results = await this.processEventsAcrossProviders(
|
|
237
|
+
eventsToProcess,
|
|
238
|
+
batchId,
|
|
239
|
+
AbortSignal.timeout(this.config.batchTimeoutMs * 2),
|
|
240
|
+
);
|
|
241
|
+
|
|
242
|
+
// Update metrics
|
|
243
|
+
const endTime = process.hrtime.bigint();
|
|
244
|
+
await this.updateMetrics(batchId, eventsToProcess.length, Number(endTime - startTime));
|
|
245
|
+
|
|
246
|
+
// Resolve all pending promises for this batch
|
|
247
|
+
this.resolveBatchPromises(results);
|
|
248
|
+
} catch (error) {
|
|
249
|
+
this.rejectBatchPromises(error as Error);
|
|
250
|
+
} finally {
|
|
251
|
+
this.isProcessing = false;
|
|
252
|
+
|
|
253
|
+
// Continue processing if there are more events
|
|
254
|
+
if (this.eventQueue.length > 0) {
|
|
255
|
+
await this.maybeProcessBatch();
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Process events across providers with health monitoring and failover
|
|
262
|
+
*/
|
|
263
|
+
private async processEventsAcrossProviders(
|
|
264
|
+
events: EmitterPayload[],
|
|
265
|
+
batchId: string,
|
|
266
|
+
abortSignal: AbortSignal,
|
|
267
|
+
): Promise<EventProcessingResult[]> {
|
|
268
|
+
const results: EventProcessingResult[] = [];
|
|
269
|
+
const healthyProviders = await this.getHealthyProviders();
|
|
270
|
+
|
|
271
|
+
if (healthyProviders.length === 0) {
|
|
272
|
+
throw new Error('No healthy analytics providers available');
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// Process events concurrently across healthy providers
|
|
276
|
+
const processingPromises = events.map(async (event, index) => {
|
|
277
|
+
const eventId = `${batchId}_${index}`;
|
|
278
|
+
|
|
279
|
+
for (const provider of healthyProviders) {
|
|
280
|
+
if (abortSignal.aborted) {
|
|
281
|
+
throw new Error('Batch processing aborted');
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
try {
|
|
285
|
+
const result = await this.processEventWithProvider(event, provider, eventId, abortSignal);
|
|
286
|
+
|
|
287
|
+
results.push(result);
|
|
288
|
+
break; // Success, move to next event
|
|
289
|
+
} catch (error) {
|
|
290
|
+
await this.recordProviderError(provider.constructor.name, error as Error);
|
|
291
|
+
|
|
292
|
+
// Try next provider
|
|
293
|
+
continue;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
await Promise.allSettled(processingPromises);
|
|
299
|
+
return results;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Process single event with provider using Node 22+ timing
|
|
304
|
+
*/
|
|
305
|
+
private async processEventWithProvider(
|
|
306
|
+
event: EmitterPayload,
|
|
307
|
+
provider: AnalyticsProvider,
|
|
308
|
+
eventId: string,
|
|
309
|
+
abortSignal: AbortSignal,
|
|
310
|
+
): Promise<EventProcessingResult> {
|
|
311
|
+
const startTime = process.hrtime.bigint();
|
|
312
|
+
const initialMemory = process.memoryUsage().heapUsed;
|
|
313
|
+
|
|
314
|
+
try {
|
|
315
|
+
// Serialize event with timing
|
|
316
|
+
const serializationStart = process.hrtime.bigint();
|
|
317
|
+
JSON.stringify(event);
|
|
318
|
+
const serializationEnd = process.hrtime.bigint();
|
|
319
|
+
|
|
320
|
+
// Process with provider
|
|
321
|
+
await this.callProviderWithTimeout(provider, event, abortSignal);
|
|
322
|
+
|
|
323
|
+
const endTime = process.hrtime.bigint();
|
|
324
|
+
const finalMemory = process.memoryUsage().heapUsed;
|
|
325
|
+
|
|
326
|
+
return {
|
|
327
|
+
success: true,
|
|
328
|
+
processedAt: endTime,
|
|
329
|
+
processingTime: Number(endTime - startTime),
|
|
330
|
+
eventId,
|
|
331
|
+
providerId: provider.constructor.name,
|
|
332
|
+
retryAttempt: this.getRetryCount(event),
|
|
333
|
+
metrics: {
|
|
334
|
+
memoryUsed: finalMemory - initialMemory,
|
|
335
|
+
serializationTime: Number(serializationEnd - serializationStart),
|
|
336
|
+
},
|
|
337
|
+
};
|
|
338
|
+
} catch (error) {
|
|
339
|
+
const endTime = process.hrtime.bigint();
|
|
340
|
+
const finalMemory = process.memoryUsage().heapUsed;
|
|
341
|
+
|
|
342
|
+
return {
|
|
343
|
+
success: false,
|
|
344
|
+
processedAt: endTime,
|
|
345
|
+
processingTime: Number(endTime - startTime),
|
|
346
|
+
eventId,
|
|
347
|
+
providerId: provider.constructor.name,
|
|
348
|
+
retryAttempt: this.getRetryCount(event),
|
|
349
|
+
error: error as Error,
|
|
350
|
+
metrics: {
|
|
351
|
+
memoryUsed: finalMemory - initialMemory,
|
|
352
|
+
serializationTime: 0,
|
|
353
|
+
},
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* Call provider method with timeout using AbortSignal.timeout()
|
|
360
|
+
*/
|
|
361
|
+
private async callProviderWithTimeout(
|
|
362
|
+
provider: AnalyticsProvider,
|
|
363
|
+
event: EmitterPayload,
|
|
364
|
+
abortSignal: AbortSignal,
|
|
365
|
+
): Promise<void> {
|
|
366
|
+
const timeoutSignal = AbortSignal.timeout(5000); // 5 second timeout
|
|
367
|
+
const combinedSignal = AbortSignal.any([abortSignal, timeoutSignal]);
|
|
368
|
+
|
|
369
|
+
// Determine the provider method to call based on event type
|
|
370
|
+
if (event.type === 'track') {
|
|
371
|
+
await provider.track(event.event, event.properties ?? {}, event.context);
|
|
372
|
+
} else if (event.type === 'identify' && provider.identify && event.userId) {
|
|
373
|
+
await provider.identify(event.userId, event.traits ?? {}, event.context);
|
|
374
|
+
} else if (event.type === 'page' && provider.page) {
|
|
375
|
+
await provider.page(event.name ?? 'Unknown Page', event.properties ?? {}, event.context);
|
|
376
|
+
} else if (event.type === 'group' && provider.group && event.groupId) {
|
|
377
|
+
await provider.group(event.groupId, event.traits ?? {}, event.context);
|
|
378
|
+
} else if (event.type === 'alias' && provider.alias && event.userId && event.previousId) {
|
|
379
|
+
await provider.alias(event.userId, event.previousId, event.context);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
if (combinedSignal.aborted) {
|
|
383
|
+
throw new Error('Provider call timed out or was aborted');
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* Get healthy providers with real-time health monitoring
|
|
389
|
+
*/
|
|
390
|
+
private async getHealthyProviders(): Promise<AnalyticsProvider[]> {
|
|
391
|
+
const healthyProviders: AnalyticsProvider[] = [];
|
|
392
|
+
const now = process.hrtime.bigint();
|
|
393
|
+
|
|
394
|
+
for (const [providerId, provider] of this.providers) {
|
|
395
|
+
const health = this.providerHealthMap.get(providerId);
|
|
396
|
+
|
|
397
|
+
// Check if we need to re-evaluate health
|
|
398
|
+
const needsHealthCheck = !health || Number(now - health.lastCheck) > 30000000000; // 30 seconds in nanoseconds
|
|
399
|
+
|
|
400
|
+
if (needsHealthCheck) {
|
|
401
|
+
await this.checkProviderHealth(providerId, provider);
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
const currentHealth = this.providerHealthMap.get(providerId);
|
|
405
|
+
if (currentHealth?.isHealthy) {
|
|
406
|
+
healthyProviders.push(provider);
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
return healthyProviders;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
/**
|
|
414
|
+
* Check individual provider health using Node 22+ timing
|
|
415
|
+
*/
|
|
416
|
+
private async checkProviderHealth(
|
|
417
|
+
providerId: string,
|
|
418
|
+
provider: AnalyticsProvider,
|
|
419
|
+
): Promise<void> {
|
|
420
|
+
const startTime = process.hrtime.bigint();
|
|
421
|
+
|
|
422
|
+
try {
|
|
423
|
+
// Simple health check - attempt to track a test event
|
|
424
|
+
const testEvent = {
|
|
425
|
+
event: '__health_check__',
|
|
426
|
+
properties: { timestamp: Date.now() },
|
|
427
|
+
options: { send: false }, // Don't actually send if provider supports it
|
|
428
|
+
};
|
|
429
|
+
|
|
430
|
+
await Promise.race([
|
|
431
|
+
provider.track(testEvent.event, testEvent.properties, testEvent.options),
|
|
432
|
+
new Promise<void>((_resolve, reject) =>
|
|
433
|
+
setTimeout(() => reject(new Error('Health check timeout')), 2000),
|
|
434
|
+
),
|
|
435
|
+
]);
|
|
436
|
+
|
|
437
|
+
const endTime = process.hrtime.bigint();
|
|
438
|
+
const responseTime = Number(endTime - startTime) / 1_000_000; // Convert to ms
|
|
439
|
+
|
|
440
|
+
this.providerHealthMap.set(providerId, {
|
|
441
|
+
providerId,
|
|
442
|
+
isHealthy: true,
|
|
443
|
+
lastCheck: endTime,
|
|
444
|
+
responseTime,
|
|
445
|
+
errorRate: 0,
|
|
446
|
+
successRate: 1,
|
|
447
|
+
consecutiveFailures: 0,
|
|
448
|
+
});
|
|
449
|
+
} catch {
|
|
450
|
+
const endTime = process.hrtime.bigint();
|
|
451
|
+
const responseTime = Number(endTime - startTime) / 1_000_000;
|
|
452
|
+
|
|
453
|
+
const existingHealth = this.providerHealthMap.get(providerId);
|
|
454
|
+
const consecutiveFailures = (existingHealth?.consecutiveFailures ?? 0) + 1;
|
|
455
|
+
|
|
456
|
+
this.providerHealthMap.set(providerId, {
|
|
457
|
+
providerId,
|
|
458
|
+
isHealthy: consecutiveFailures < 3, // Healthy if fewer than 3 consecutive failures
|
|
459
|
+
lastCheck: endTime,
|
|
460
|
+
responseTime,
|
|
461
|
+
errorRate: Math.min(1, consecutiveFailures * 0.25),
|
|
462
|
+
successRate: Math.max(0, 1 - consecutiveFailures * 0.25),
|
|
463
|
+
consecutiveFailures,
|
|
464
|
+
estimatedRecoveryTime: consecutiveFailures * 30000, // 30s per failure
|
|
465
|
+
});
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
/**
|
|
470
|
+
* Update analytics metrics using high-resolution timing
|
|
471
|
+
*/
|
|
472
|
+
private async updateMetrics(
|
|
473
|
+
batchId: string,
|
|
474
|
+
eventCount: number,
|
|
475
|
+
processingTime: number,
|
|
476
|
+
): Promise<void> {
|
|
477
|
+
const now = process.hrtime.bigint();
|
|
478
|
+
const memoryUsage = process.memoryUsage().heapUsed;
|
|
479
|
+
|
|
480
|
+
// Calculate throughput
|
|
481
|
+
const processingTimeMs = processingTime / 1_000_000;
|
|
482
|
+
const throughput = processingTimeMs > 0 ? (eventCount / processingTimeMs) * 1000 : 0;
|
|
483
|
+
|
|
484
|
+
const metrics: AnalyticsMetrics = {
|
|
485
|
+
timestamp: now,
|
|
486
|
+
eventCount,
|
|
487
|
+
batchCount: 1,
|
|
488
|
+
processingTime,
|
|
489
|
+
memoryUsage,
|
|
490
|
+
errorCount: 0, // Would be calculated from results
|
|
491
|
+
providerHealth: new Map(
|
|
492
|
+
[...this.providerHealthMap.entries()].map(([id, health]) => [id, health.successRate]),
|
|
493
|
+
),
|
|
494
|
+
throughputEventsPerSecond: throughput,
|
|
495
|
+
averageLatency: processingTimeMs,
|
|
496
|
+
};
|
|
497
|
+
|
|
498
|
+
this.metrics.set(batchId, metrics);
|
|
499
|
+
|
|
500
|
+
// Keep only recent metrics to prevent memory growth
|
|
501
|
+
if (this.metrics.size > 100) {
|
|
502
|
+
const oldestKey = this.metrics.keys().next().value;
|
|
503
|
+
if (oldestKey !== undefined) {
|
|
504
|
+
this.metrics.delete(oldestKey);
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
/**
|
|
510
|
+
* Get comprehensive analytics performance metrics
|
|
511
|
+
*/
|
|
512
|
+
getPerformanceMetrics(): {
|
|
513
|
+
current: AnalyticsMetrics | null | undefined;
|
|
514
|
+
average: Partial<AnalyticsMetrics> | null;
|
|
515
|
+
providerHealth: Map<string, ProviderHealthStatus>;
|
|
516
|
+
queueStatus: {
|
|
517
|
+
queueLength: number;
|
|
518
|
+
oldestEventAge: number;
|
|
519
|
+
memoryUsage: number;
|
|
520
|
+
isProcessing: boolean;
|
|
521
|
+
};
|
|
522
|
+
} {
|
|
523
|
+
const allMetrics = [...this.metrics.values()];
|
|
524
|
+
const current = allMetrics.length > 0 ? allMetrics[allMetrics.length - 1] : null;
|
|
525
|
+
|
|
526
|
+
const average =
|
|
527
|
+
allMetrics.length > 0
|
|
528
|
+
? {
|
|
529
|
+
eventCount: allMetrics.reduce((sum, m) => sum + m.eventCount, 0) / allMetrics.length,
|
|
530
|
+
processingTime:
|
|
531
|
+
allMetrics.reduce((sum, m) => sum + m.processingTime, 0) / allMetrics.length,
|
|
532
|
+
memoryUsage: allMetrics.reduce((sum, m) => sum + m.memoryUsage, 0) / allMetrics.length,
|
|
533
|
+
throughputEventsPerSecond:
|
|
534
|
+
allMetrics.reduce((sum, m) => sum + m.throughputEventsPerSecond, 0) /
|
|
535
|
+
allMetrics.length,
|
|
536
|
+
averageLatency:
|
|
537
|
+
allMetrics.reduce((sum, m) => sum + m.averageLatency, 0) / allMetrics.length,
|
|
538
|
+
}
|
|
539
|
+
: null;
|
|
540
|
+
|
|
541
|
+
return {
|
|
542
|
+
current,
|
|
543
|
+
average,
|
|
544
|
+
providerHealth: new Map(this.providerHealthMap),
|
|
545
|
+
queueStatus: {
|
|
546
|
+
queueLength: this.eventQueue.length,
|
|
547
|
+
oldestEventAge: this.getOldestEventAge(),
|
|
548
|
+
memoryUsage: process.memoryUsage().heapUsed,
|
|
549
|
+
isProcessing: this.isProcessing,
|
|
550
|
+
},
|
|
551
|
+
};
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
// Helper methods
|
|
555
|
+
private insertWithPriority(payload: EmitterPayload, priority: number): void {
|
|
556
|
+
const index = this.eventQueue.findIndex(event => {
|
|
557
|
+
const eventMeta = this.eventMetadata.get(event);
|
|
558
|
+
return eventMeta && (eventMeta.priority ?? 0) < priority;
|
|
559
|
+
});
|
|
560
|
+
|
|
561
|
+
if (index === -1) {
|
|
562
|
+
this.eventQueue.push(payload);
|
|
563
|
+
} else {
|
|
564
|
+
this.eventQueue.splice(index, 0, payload);
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
private getOldestEventAge(): number {
|
|
569
|
+
if (this.eventQueue.length === 0) return 0;
|
|
570
|
+
|
|
571
|
+
const oldestEvent = this.eventQueue[0];
|
|
572
|
+
if (!oldestEvent) return 0;
|
|
573
|
+
|
|
574
|
+
const metadata = this.eventMetadata.get(oldestEvent);
|
|
575
|
+
|
|
576
|
+
if (!metadata) return 0;
|
|
577
|
+
|
|
578
|
+
return Number(process.hrtime.bigint() - metadata.enqueuedAt) / 1_000_000; // ms
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
private isMemoryThresholdExceeded(): boolean {
|
|
582
|
+
return process.memoryUsage().heapUsed > this.config.maxMemoryUsage;
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
private scheduleBatchTimeout(): void {
|
|
586
|
+
if (this.batchTimer) return;
|
|
587
|
+
|
|
588
|
+
this.batchTimer = setTimeout(() => {
|
|
589
|
+
if (this.batchTimer) {
|
|
590
|
+
clearTimeout(this.batchTimer);
|
|
591
|
+
this.batchTimer = undefined;
|
|
592
|
+
}
|
|
593
|
+
void this.processBatch();
|
|
594
|
+
}, this.config.batchTimeoutMs);
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
private generateEventId(): string {
|
|
598
|
+
return `evt_${Date.now()}_${Math.random().toString(36).slice(2, 11)}`;
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
private generateBatchId(): string {
|
|
602
|
+
return `batch_${Date.now()}_${Math.random().toString(36).slice(2, 11)}`;
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
private getRetryCount(event: EmitterPayload): number {
|
|
606
|
+
return this.eventMetadata.get(event)?.retryCount ?? 0;
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
private async recordProviderError(providerId: string, _error: Error): Promise<void> {
|
|
610
|
+
const health = this.providerHealthMap.get(providerId);
|
|
611
|
+
if (health) {
|
|
612
|
+
const consecutiveFailures = health.consecutiveFailures + 1;
|
|
613
|
+
this.providerHealthMap.set(providerId, {
|
|
614
|
+
providerId: health.providerId,
|
|
615
|
+
lastCheck: health.lastCheck,
|
|
616
|
+
responseTime: health.responseTime,
|
|
617
|
+
consecutiveFailures,
|
|
618
|
+
errorRate: Math.min(1, health.errorRate + 0.1),
|
|
619
|
+
successRate: Math.max(0, health.successRate - 0.1),
|
|
620
|
+
isHealthy: consecutiveFailures < 3, // Become unhealthy after 3 failures
|
|
621
|
+
estimatedRecoveryTime: health.estimatedRecoveryTime,
|
|
622
|
+
});
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
private resolveBatchPromises(results: EventProcessingResult[]): void {
|
|
627
|
+
// Group results by event/batch and resolve promises
|
|
628
|
+
const batchGroups = new Map<string, EventProcessingResult[]>();
|
|
629
|
+
|
|
630
|
+
for (const result of results) {
|
|
631
|
+
const batchIdParts = result.eventId?.split('_');
|
|
632
|
+
const batchId = batchIdParts?.[0];
|
|
633
|
+
if (!batchId) continue;
|
|
634
|
+
|
|
635
|
+
if (!batchGroups.has(batchId)) {
|
|
636
|
+
batchGroups.set(batchId, []);
|
|
637
|
+
}
|
|
638
|
+
const group = batchGroups.get(batchId);
|
|
639
|
+
if (group) {
|
|
640
|
+
group.push(result);
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
for (const [batchId, batchResults] of batchGroups) {
|
|
645
|
+
const batchPromise = this.batchPromises.get(batchId);
|
|
646
|
+
if (batchPromise) {
|
|
647
|
+
batchPromise.resolve(batchResults);
|
|
648
|
+
this.batchPromises.delete(batchId);
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
private rejectBatchPromises(error: Error): void {
|
|
654
|
+
for (const [, batchPromise] of this.batchPromises) {
|
|
655
|
+
batchPromise.reject(error);
|
|
656
|
+
}
|
|
657
|
+
this.batchPromises.clear();
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
// Commented out as currently unused but kept for potential future use
|
|
661
|
+
// private _createErrorResults(_batchId: string, error: Error): EventProcessingResult[] {
|
|
662
|
+
// return this.eventQueue.map((event, index) => ({
|
|
663
|
+
// success: false,
|
|
664
|
+
// processedAt: process.hrtime.bigint(),
|
|
665
|
+
// processingTime: 0,
|
|
666
|
+
// eventId: `${_batchId}_${index}`,
|
|
667
|
+
// providerId: 'unknown',
|
|
668
|
+
// retryAttempt: this.getRetryCount(event),
|
|
669
|
+
// error,
|
|
670
|
+
// metrics: {
|
|
671
|
+
// memoryUsed: 0,
|
|
672
|
+
// serializationTime: 0,
|
|
673
|
+
// },
|
|
674
|
+
// }));
|
|
675
|
+
// }
|
|
676
|
+
|
|
677
|
+
private removeBatchPromise(eventId: string): void {
|
|
678
|
+
this.batchPromises.delete(eventId);
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
private cleanupBatch(batchId: string): void {
|
|
682
|
+
// Cleanup any resources associated with the batch
|
|
683
|
+
this.metrics.delete(batchId);
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
/**
|
|
688
|
+
* Enhanced analytics manager with Node 22+ optimizations
|
|
689
|
+
*/
|
|
690
|
+
export class Node22AnalyticsManager {
|
|
691
|
+
private readonly eventBatcher: AdvancedEventBatcher;
|
|
692
|
+
private readonly performanceMonitor: Map<string, bigint> = new Map();
|
|
693
|
+
|
|
694
|
+
// Node 22+ WeakSet for tracking processed events (memory efficient)
|
|
695
|
+
private readonly processedEvents = new WeakSet<EmitterPayload>();
|
|
696
|
+
|
|
697
|
+
constructor(
|
|
698
|
+
config: AnalyticsConfig,
|
|
699
|
+
providers: Map<string, AnalyticsProvider>,
|
|
700
|
+
batchConfig: EventBatchConfig,
|
|
701
|
+
) {
|
|
702
|
+
void config; // Intentionally unused for now
|
|
703
|
+
this.eventBatcher = new AdvancedEventBatcher(batchConfig, providers);
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
/**
|
|
707
|
+
* Enhanced event emission with Node 22+ features
|
|
708
|
+
*/
|
|
709
|
+
async emit(
|
|
710
|
+
payload: EmitterPayload,
|
|
711
|
+
options: TrackingOptions & {
|
|
712
|
+
priority?: number;
|
|
713
|
+
timeout?: number;
|
|
714
|
+
retries?: number;
|
|
715
|
+
} = {},
|
|
716
|
+
): Promise<EventProcessingResult[]> {
|
|
717
|
+
// Prevent duplicate processing using WeakSet (Node 22+)
|
|
718
|
+
if (this.processedEvents.has(payload)) {
|
|
719
|
+
throw new Error('Event has already been processed');
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
const startTime = process.hrtime.bigint();
|
|
723
|
+
this.performanceMonitor.set('emit_start', startTime);
|
|
724
|
+
|
|
725
|
+
try {
|
|
726
|
+
// Create abort signal with timeout
|
|
727
|
+
const abortSignal = options.timeout ? AbortSignal.timeout(options.timeout) : undefined;
|
|
728
|
+
|
|
729
|
+
// Use advanced batcher for processing
|
|
730
|
+
const results = await this.eventBatcher.enqueueEvent(payload, options, abortSignal);
|
|
731
|
+
|
|
732
|
+
// Mark as processed using WeakSet (Node 22+)
|
|
733
|
+
this.processedEvents.add(payload);
|
|
734
|
+
|
|
735
|
+
const endTime = process.hrtime.bigint();
|
|
736
|
+
this.performanceMonitor.set('emit_end', endTime);
|
|
737
|
+
|
|
738
|
+
return results;
|
|
739
|
+
} catch (error) {
|
|
740
|
+
const endTime = process.hrtime.bigint();
|
|
741
|
+
this.performanceMonitor.set('emit_error', endTime);
|
|
742
|
+
throw error;
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
/**
|
|
747
|
+
* Get real-time performance metrics using Node 22+ features
|
|
748
|
+
*/
|
|
749
|
+
getAnalyticsMetrics(): ReturnType<AdvancedEventBatcher['getPerformanceMetrics']> & {
|
|
750
|
+
emissionMetrics: {
|
|
751
|
+
averageEmissionTime: number; // nanoseconds
|
|
752
|
+
totalEmissions: number;
|
|
753
|
+
failureRate: number;
|
|
754
|
+
};
|
|
755
|
+
} {
|
|
756
|
+
const batcherMetrics = this.eventBatcher.getPerformanceMetrics();
|
|
757
|
+
|
|
758
|
+
// Calculate emission-specific metrics
|
|
759
|
+
const emissionTimes: number[] = [];
|
|
760
|
+
const totalEmissions = 0;
|
|
761
|
+
const failures = 0;
|
|
762
|
+
|
|
763
|
+
// This would be implemented with actual tracking
|
|
764
|
+
const averageEmissionTime =
|
|
765
|
+
emissionTimes.length > 0
|
|
766
|
+
? emissionTimes.reduce((sum, time) => sum + time, 0) / emissionTimes.length
|
|
767
|
+
: 0;
|
|
768
|
+
|
|
769
|
+
return {
|
|
770
|
+
...batcherMetrics,
|
|
771
|
+
emissionMetrics: {
|
|
772
|
+
averageEmissionTime,
|
|
773
|
+
totalEmissions,
|
|
774
|
+
failureRate: totalEmissions > 0 ? failures / totalEmissions : 0,
|
|
775
|
+
},
|
|
776
|
+
};
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
/**
|
|
780
|
+
* Health check for all analytics systems
|
|
781
|
+
*/
|
|
782
|
+
async healthCheck(timeout: number = 5000): Promise<{
|
|
783
|
+
overall: 'healthy' | 'degraded' | 'unhealthy';
|
|
784
|
+
providers: Record<string, 'healthy' | 'unhealthy'>;
|
|
785
|
+
metrics: any;
|
|
786
|
+
}> {
|
|
787
|
+
AbortSignal.timeout(timeout);
|
|
788
|
+
const metrics = this.getAnalyticsMetrics();
|
|
789
|
+
|
|
790
|
+
// Determine overall health
|
|
791
|
+
const healthyProviders = [...metrics.providerHealth.values()].filter(
|
|
792
|
+
health => health.isHealthy,
|
|
793
|
+
).length;
|
|
794
|
+
const totalProviders = metrics.providerHealth.size;
|
|
795
|
+
|
|
796
|
+
let overall: 'healthy' | 'degraded' | 'unhealthy';
|
|
797
|
+
if (healthyProviders === totalProviders) {
|
|
798
|
+
overall = 'healthy';
|
|
799
|
+
} else if (healthyProviders > 0) {
|
|
800
|
+
overall = 'degraded';
|
|
801
|
+
} else {
|
|
802
|
+
overall = 'unhealthy';
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
const providers: Record<string, 'healthy' | 'unhealthy'> = {};
|
|
806
|
+
for (const [providerId, health] of metrics.providerHealth) {
|
|
807
|
+
providers[providerId] = health.isHealthy ? 'healthy' : 'unhealthy';
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
return {
|
|
811
|
+
overall,
|
|
812
|
+
providers,
|
|
813
|
+
metrics,
|
|
814
|
+
};
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
/**
|
|
819
|
+
* Factory function to create Node 22+ enhanced analytics manager
|
|
820
|
+
*/
|
|
821
|
+
export function createNode22AnalyticsManager(
|
|
822
|
+
config: AnalyticsConfig,
|
|
823
|
+
providers: Map<string, AnalyticsProvider>,
|
|
824
|
+
batchConfig: Partial<EventBatchConfig> = {},
|
|
825
|
+
): Node22AnalyticsManager {
|
|
826
|
+
const defaultBatchConfig: EventBatchConfig = {
|
|
827
|
+
maxBatchSize: 50,
|
|
828
|
+
batchTimeoutMs: 5000,
|
|
829
|
+
compressionEnabled: true,
|
|
830
|
+
retryAttempts: 3,
|
|
831
|
+
retryDelayMs: 1000,
|
|
832
|
+
enablePrioritization: true,
|
|
833
|
+
maxMemoryUsage: 50 * 1024 * 1024, // 50MB
|
|
834
|
+
strategy: 'priority',
|
|
835
|
+
...batchConfig,
|
|
836
|
+
};
|
|
837
|
+
|
|
838
|
+
return new Node22AnalyticsManager(config, providers, defaultBatchConfig);
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
/**
|
|
842
|
+
* Default export: Enhanced analytics utilities
|
|
843
|
+
*/
|
|
844
|
+
export const node22Analytics = {
|
|
845
|
+
AdvancedEventBatcher,
|
|
846
|
+
Node22AnalyticsManager,
|
|
847
|
+
createNode22AnalyticsManager,
|
|
848
|
+
} as const;
|