@clduab11/gemini-flow
Version:
Revolutionary AI agent swarm coordination platform with Google Services integration, multimedia processing, and production-ready monitoring. Features 8 Google AI services, quantum computing capabilities, and enterprise-grade security.
869 lines (769 loc) • 22.7 kB
text/typescript
/**
* Real User Monitoring (RUM) Implementation
* Comprehensive real user performance and experience monitoring
*/
import { EventEmitter } from "events";
import { Logger } from "../utils/logger";
interface RUMConfig {
enabled: boolean;
apiKey: string;
endpoint: string;
samplingRate: number; // 0-1 (percentage of users to monitor)
sessionTimeout: number; // minutes
features: {
performanceMetrics: boolean;
errorTracking: boolean;
userJourney: boolean;
customMetrics: boolean;
heatmaps: boolean;
sessionRecording: boolean;
};
privacy: {
maskSensitiveData: boolean;
excludeFields: string[];
ipAnonymization: boolean;
};
}
interface RUMMetrics {
// Core Web Vitals
lcp?: number; // Largest Contentful Paint
fid?: number; // First Input Delay
cls?: number; // Cumulative Layout Shift
fcp?: number; // First Contentful Paint
ttfb?: number; // Time to First Byte
// Navigation Timing
domContentLoaded?: number;
loadComplete?: number;
// Custom Performance Metrics
timeToInteractive?: number;
speedIndex?: number;
totalBlockingTime?: number;
// Resource Timing
resources?: ResourceTiming[];
// User Experience
userAgent: string;
viewport: { width: number; height: number };
connectionType?: string;
deviceMemory?: number;
// Page Information
url: string;
referrer: string;
timestamp: number;
}
interface ResourceTiming {
name: string;
type: string;
duration: number;
size: number;
cached: boolean;
}
interface RUMError {
message: string;
stack?: string;
filename?: string;
lineno?: number;
colno?: number;
timestamp: number;
url: string;
userAgent: string;
userId?: string;
sessionId: string;
}
interface UserJourney {
sessionId: string;
userId?: string;
startTime: number;
endTime?: number;
pageViews: PageView[];
interactions: UserInteraction[];
conversions: Conversion[];
errors: RUMError[];
}
interface PageView {
url: string;
title: string;
timestamp: number;
duration?: number;
exitType?: "navigation" | "reload" | "close";
}
interface UserInteraction {
type: "click" | "scroll" | "input" | "hover" | "resize";
element?: string;
timestamp: number;
coordinates?: { x: number; y: number };
value?: string;
}
interface Conversion {
type: string;
value: number;
timestamp: number;
metadata?: Record<string, any>;
}
export class RealUserMonitor extends EventEmitter {
private logger: Logger;
private config: RUMConfig;
private sessionId: string;
private userId?: string;
private currentJourney: UserJourney;
private metricsBuffer: RUMMetrics[] = [];
private errorsBuffer: RUMError[] = [];
private isInitialized: boolean = false;
private observers: Map<string, PerformanceObserver> = new Map();
constructor(config: RUMConfig) {
super();
this.config = config;
this.logger = new Logger("RealUserMonitor");
this.sessionId = this.generateSessionId();
this.currentJourney = this.initializeJourney();
}
/**
* Initialize RUM monitoring
*/
async initialize(): Promise<void> {
if (!this.config.enabled || this.isInitialized) {
return;
}
try {
// Check if we should monitor this user (sampling)
if (Math.random() > this.config.samplingRate) {
this.logger.debug(
"User excluded from RUM monitoring due to sampling rate",
);
return;
}
// Initialize performance monitoring
if (this.config.features.performanceMetrics) {
await this.initializePerformanceMonitoring();
}
// Initialize error tracking
if (this.config.features.errorTracking) {
this.initializeErrorTracking();
}
// Initialize user journey tracking
if (this.config.features.userJourney) {
this.initializeUserJourneyTracking();
}
// Initialize custom metrics
if (this.config.features.customMetrics) {
this.initializeCustomMetrics();
}
// Start data collection
this.startDataCollection();
this.isInitialized = true;
this.logger.info("RUM monitoring initialized");
} catch (error) {
this.logger.error("Failed to initialize RUM monitoring:", error);
throw error;
}
}
/**
* Initialize performance monitoring
*/
private async initializePerformanceMonitoring(): Promise<void> {
if (typeof window === "undefined" || !window.PerformanceObserver) {
this.logger.warn("PerformanceObserver not available");
return;
}
// Observe Navigation Timing
try {
const navObserver = new PerformanceObserver((list) => {
const entries = list.getEntries();
for (const entry of entries) {
this.collectNavigationMetrics(entry as PerformanceNavigationTiming);
}
});
navObserver.observe({ type: "navigation", buffered: true });
this.observers.set("navigation", navObserver);
} catch (error) {
this.logger.warn("Navigation timing observer failed:", error);
}
// Observe Resource Timing
try {
const resourceObserver = new PerformanceObserver((list) => {
const entries = list.getEntries();
for (const entry of entries) {
this.collectResourceMetrics(entry as PerformanceResourceTiming);
}
});
resourceObserver.observe({ type: "resource", buffered: true });
this.observers.set("resource", resourceObserver);
} catch (error) {
this.logger.warn("Resource timing observer failed:", error);
}
// Observe Paint Timing
try {
const paintObserver = new PerformanceObserver((list) => {
const entries = list.getEntries();
for (const entry of entries) {
this.collectPaintMetrics(entry as PerformancePaintTiming);
}
});
paintObserver.observe({ type: "paint", buffered: true });
this.observers.set("paint", paintObserver);
} catch (error) {
this.logger.warn("Paint timing observer failed:", error);
}
// Observe Largest Contentful Paint
try {
const lcpObserver = new PerformanceObserver((list) => {
const entries = list.getEntries();
const lastEntry = entries[entries.length - 1] as any;
if (lastEntry) {
this.collectLCPMetric(lastEntry.startTime);
}
});
lcpObserver.observe({ type: "largest-contentful-paint", buffered: true });
this.observers.set("lcp", lcpObserver);
} catch (error) {
this.logger.warn("LCP observer failed:", error);
}
// Observe First Input Delay
try {
const fidObserver = new PerformanceObserver((list) => {
const entries = list.getEntries();
for (const entry of entries) {
this.collectFIDMetric(
(entry as any).processingStart - entry.startTime,
);
}
});
fidObserver.observe({ type: "first-input", buffered: true });
this.observers.set("fid", fidObserver);
} catch (error) {
this.logger.warn("FID observer failed:", error);
}
// Collect Layout Shift metrics
try {
const clsObserver = new PerformanceObserver((list) => {
const entries = list.getEntries();
for (const entry of entries) {
if (!(entry as any).hadRecentInput) {
this.collectCLSMetric((entry as any).value);
}
}
});
clsObserver.observe({ type: "layout-shift", buffered: true });
this.observers.set("cls", clsObserver);
} catch (error) {
this.logger.warn("CLS observer failed:", error);
}
}
/**
* Initialize error tracking
*/
private initializeErrorTracking(): void {
if (typeof window === "undefined") return;
// Global error handler
window.addEventListener("error", (event) => {
const error: RUMError = {
message: event.message,
stack: event.error?.stack,
filename: event.filename,
lineno: event.lineno,
colno: event.colno,
timestamp: Date.now(),
url: window.location.href,
userAgent: navigator.userAgent,
sessionId: this.sessionId,
userId: this.userId,
};
this.errorsBuffer.push(error);
this.currentJourney.errors.push(error);
this.emit("error", error);
});
// Unhandled promise rejection handler
window.addEventListener("unhandledrejection", (event) => {
const error: RUMError = {
message: `Unhandled Promise Rejection: ${event.reason}`,
stack: event.reason?.stack,
timestamp: Date.now(),
url: window.location.href,
userAgent: navigator.userAgent,
sessionId: this.sessionId,
userId: this.userId,
};
this.errorsBuffer.push(error);
this.currentJourney.errors.push(error);
this.emit("error", error);
});
}
/**
* Initialize user journey tracking
*/
private initializeUserJourneyTracking(): void {
if (typeof window === "undefined") return;
// Track page views
this.trackPageView();
// Track navigation changes (SPA)
const originalPushState = history.pushState;
const originalReplaceState = history.replaceState;
history.pushState = (...args) => {
originalPushState.apply(history, args);
this.trackPageView();
};
history.replaceState = (...args) => {
originalReplaceState.apply(history, args);
this.trackPageView();
};
window.addEventListener("popstate", () => {
this.trackPageView();
});
// Track user interactions
this.trackUserInteractions();
// Track page visibility changes
document.addEventListener("visibilitychange", () => {
if (document.hidden) {
this.onPageHidden();
} else {
this.onPageVisible();
}
});
// Track page unload
window.addEventListener("beforeunload", () => {
this.finalizeSession();
});
}
/**
* Initialize custom metrics collection
*/
private initializeCustomMetrics(): void {
// Create global API for custom metrics
if (typeof window !== "undefined") {
(window as any).geminiFlowRUM = {
track: (
name: string,
value: number,
metadata?: Record<string, any>,
) => {
this.trackCustomMetric(name, value, metadata);
},
trackConversion: (
type: string,
value: number,
metadata?: Record<string, any>,
) => {
this.trackConversion(type, value, metadata);
},
setUserId: (userId: string) => {
this.setUserId(userId);
},
};
}
}
/**
* Start periodic data collection and transmission
*/
private startDataCollection(): void {
// Send data every 30 seconds
setInterval(() => {
this.sendBufferedData();
}, 30000);
// Send data on page visibility change
if (typeof document !== "undefined") {
document.addEventListener("visibilitychange", () => {
if (document.hidden) {
this.sendBufferedData();
}
});
}
}
/**
* Collect navigation metrics
*/
private collectNavigationMetrics(entry: PerformanceNavigationTiming): void {
const metrics: RUMMetrics = {
ttfb: entry.responseStart - entry.requestStart,
domContentLoaded:
entry.domContentLoadedEventEnd - entry.domContentLoadedEventStart,
loadComplete: entry.loadEventEnd - entry.loadEventStart,
url: window.location.href,
referrer: document.referrer,
timestamp: Date.now(),
userAgent: navigator.userAgent,
viewport: {
width: window.innerWidth,
height: window.innerHeight,
},
connectionType: (navigator as any).connection?.effectiveType,
deviceMemory: (navigator as any).deviceMemory,
};
this.metricsBuffer.push(metrics);
this.emit("metrics", metrics);
}
/**
* Collect resource metrics
*/
private collectResourceMetrics(entry: PerformanceResourceTiming): void {
const resource: ResourceTiming = {
name: entry.name,
type: this.getResourceType(entry.name),
duration: entry.duration,
size: entry.transferSize || 0,
cached: entry.transferSize === 0 && entry.decodedBodySize > 0,
};
// Add to current metrics
const currentMetrics = this.metricsBuffer[this.metricsBuffer.length - 1];
if (currentMetrics) {
if (!currentMetrics.resources) {
currentMetrics.resources = [];
}
currentMetrics.resources.push(resource);
}
}
/**
* Collect paint metrics
*/
private collectPaintMetrics(entry: PerformancePaintTiming): void {
const currentMetrics = this.metricsBuffer[this.metricsBuffer.length - 1];
if (currentMetrics) {
if (entry.name === "first-contentful-paint") {
currentMetrics.fcp = entry.startTime;
}
}
}
/**
* Collect LCP metric
*/
private collectLCPMetric(value: number): void {
const currentMetrics = this.metricsBuffer[this.metricsBuffer.length - 1];
if (currentMetrics) {
currentMetrics.lcp = value;
}
}
/**
* Collect FID metric
*/
private collectFIDMetric(value: number): void {
const currentMetrics = this.metricsBuffer[this.metricsBuffer.length - 1];
if (currentMetrics) {
currentMetrics.fid = value;
}
}
/**
* Collect CLS metric
*/
private collectCLSMetric(value: number): void {
const currentMetrics = this.metricsBuffer[this.metricsBuffer.length - 1];
if (currentMetrics) {
if (!currentMetrics.cls) {
currentMetrics.cls = 0;
}
currentMetrics.cls += value;
}
}
/**
* Track page view
*/
private trackPageView(): void {
const pageView: PageView = {
url: window.location.href,
title: document.title,
timestamp: Date.now(),
};
// Set duration for previous page view
const lastPageView =
this.currentJourney.pageViews[this.currentJourney.pageViews.length - 1];
if (lastPageView && !lastPageView.duration) {
lastPageView.duration = Date.now() - lastPageView.timestamp;
}
this.currentJourney.pageViews.push(pageView);
this.emit("pageView", pageView);
}
/**
* Track user interactions
*/
private trackUserInteractions(): void {
if (typeof document === "undefined") return;
// Track clicks
document.addEventListener(
"click",
(event) => {
const interaction: UserInteraction = {
type: "click",
element: this.getElementSelector(event.target as Element),
timestamp: Date.now(),
coordinates: { x: event.clientX, y: event.clientY },
};
this.currentJourney.interactions.push(interaction);
},
{ passive: true },
);
// Track scrolling
let scrollTimeout: NodeJS.Timeout;
document.addEventListener(
"scroll",
() => {
clearTimeout(scrollTimeout);
scrollTimeout = setTimeout(() => {
const interaction: UserInteraction = {
type: "scroll",
timestamp: Date.now(),
coordinates: { x: window.scrollX, y: window.scrollY },
};
this.currentJourney.interactions.push(interaction);
}, 100);
},
{ passive: true },
);
// Track input changes
document.addEventListener(
"input",
(event) => {
const target = event.target as HTMLInputElement;
// Mask sensitive data if configured
let value = target.value;
if (this.config.privacy.maskSensitiveData) {
if (
target.type === "password" ||
target.type === "email" ||
this.config.privacy.excludeFields.includes(target.name)
) {
value = "[MASKED]";
}
}
const interaction: UserInteraction = {
type: "input",
element: this.getElementSelector(target),
timestamp: Date.now(),
value,
};
this.currentJourney.interactions.push(interaction);
},
{ passive: true },
);
}
/**
* Track custom metric
*/
public trackCustomMetric(
name: string,
value: number,
metadata?: Record<string, any>,
): void {
const metric = {
name,
value,
metadata,
timestamp: Date.now(),
sessionId: this.sessionId,
userId: this.userId,
url: typeof window !== "undefined" ? window.location.href : "",
};
this.emit("customMetric", metric);
this.sendCustomMetric(metric);
}
/**
* Track conversion
*/
public trackConversion(
type: string,
value: number,
metadata?: Record<string, any>,
): void {
const conversion: Conversion = {
type,
value,
timestamp: Date.now(),
metadata,
};
this.currentJourney.conversions.push(conversion);
this.emit("conversion", conversion);
}
/**
* Set user ID
*/
public setUserId(userId: string): void {
this.userId = userId;
this.currentJourney.userId = userId;
}
/**
* Handle page hidden
*/
private onPageHidden(): void {
this.sendBufferedData();
}
/**
* Handle page visible
*/
private onPageVisible(): void {
// Resume tracking
}
/**
* Finalize session
*/
private finalizeSession(): void {
this.currentJourney.endTime = Date.now();
// Set duration for last page view
const lastPageView =
this.currentJourney.pageViews[this.currentJourney.pageViews.length - 1];
if (lastPageView && !lastPageView.duration) {
lastPageView.duration = Date.now() - lastPageView.timestamp;
lastPageView.exitType = "close";
}
this.sendBufferedData();
this.sendUserJourney();
}
/**
* Send buffered data
*/
private async sendBufferedData(): Promise<void> {
if (this.metricsBuffer.length === 0 && this.errorsBuffer.length === 0) {
return;
}
try {
const payload = {
sessionId: this.sessionId,
userId: this.userId,
metrics: this.metricsBuffer.splice(0),
errors: this.errorsBuffer.splice(0),
timestamp: Date.now(),
};
await this.sendToEndpoint("metrics", payload);
} catch (error) {
this.logger.error("Failed to send RUM data:", error);
}
}
/**
* Send user journey data
*/
private async sendUserJourney(): Promise<void> {
try {
await this.sendToEndpoint("journey", this.currentJourney);
} catch (error) {
this.logger.error("Failed to send user journey:", error);
}
}
/**
* Send custom metric
*/
private async sendCustomMetric(metric: any): Promise<void> {
try {
await this.sendToEndpoint("custom", metric);
} catch (error) {
this.logger.error("Failed to send custom metric:", error);
}
}
/**
* Send data to endpoint
*/
private async sendToEndpoint(endpoint: string, data: any): Promise<void> {
const url = `${this.config.endpoint}/${endpoint}`;
// Use sendBeacon if available for reliability
if (typeof navigator !== "undefined" && navigator.sendBeacon) {
const blob = new Blob([JSON.stringify(data)], {
type: "application/json",
});
navigator.sendBeacon(url, blob);
} else {
// Fallback to fetch
fetch(url, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${this.config.apiKey}`,
},
body: JSON.stringify(data),
keepalive: true,
}).catch(() => {
// Ignore fetch errors for RUM data
});
}
}
/**
* Utility functions
*/
private generateSessionId(): string {
return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
}
private initializeJourney(): UserJourney {
return {
sessionId: this.sessionId,
userId: this.userId,
startTime: Date.now(),
pageViews: [],
interactions: [],
conversions: [],
errors: [],
};
}
private getResourceType(name: string): string {
if (name.match(/\.(js|jsx|ts|tsx)$/)) return "script";
if (name.match(/\.(css|scss|sass)$/)) return "stylesheet";
if (name.match(/\.(png|jpg|jpeg|gif|svg|webp)$/)) return "image";
if (name.match(/\.(woff|woff2|ttf|eot)$/)) return "font";
if (name.match(/\.(mp4|webm|ogg|avi)$/)) return "video";
if (name.match(/\.(mp3|wav|ogg|m4a)$/)) return "audio";
return "other";
}
private getElementSelector(element: Element): string {
if (!element) return "";
// Use data-testid if available
const testId = element.getAttribute("data-testid");
if (testId) return `[data-testid="${testId}"]`;
// Use ID if available
if (element.id) return `#${element.id}`;
// Use class names
if (element.className) {
const classes = element.className.split(" ").filter((c) => c.trim());
if (classes.length > 0) {
return `.${classes.join(".")}`;
}
}
// Use tag name as fallback
return element.tagName.toLowerCase();
}
/**
* Cleanup resources
*/
public destroy(): void {
// Stop all observers
for (const [name, observer] of this.observers) {
observer.disconnect();
this.logger.debug(`Stopped ${name} observer`);
}
this.observers.clear();
// Send final data
this.finalizeSession();
this.isInitialized = false;
this.logger.info("RUM monitoring destroyed");
}
/**
* Get current session statistics
*/
public getSessionStats(): any {
return {
sessionId: this.sessionId,
userId: this.userId,
startTime: this.currentJourney.startTime,
duration: Date.now() - this.currentJourney.startTime,
pageViews: this.currentJourney.pageViews.length,
interactions: this.currentJourney.interactions.length,
conversions: this.currentJourney.conversions.length,
errors: this.currentJourney.errors.length,
metricsCollected: this.metricsBuffer.length,
};
}
}
// Default configuration
export const DEFAULT_RUM_CONFIG: RUMConfig = {
enabled: true,
apiKey: process.env.RUM_API_KEY || "",
endpoint: process.env.RUM_ENDPOINT || "https://rum.gemini-flow.com/api",
samplingRate: 0.1, // Monitor 10% of users
sessionTimeout: 30, // 30 minutes
features: {
performanceMetrics: true,
errorTracking: true,
userJourney: true,
customMetrics: true,
heatmaps: false,
sessionRecording: false,
},
privacy: {
maskSensitiveData: true,
excludeFields: ["password", "email", "ssn", "credit_card"],
ipAnonymization: true,
},
};
// Export types
export type { RUMConfig, RUMMetrics, RUMError, UserJourney };