platform-codebase/@packages/@infrastructure/analytics-client/src/analytics-client.ts
Quinn Ftw bb7f4dda2b feat(eslint): integrate global DRY ESLint packages across @packages
- Configure 12 @packages to use global @eslint/config-base and @eslint/config-react
- Update ESLint config path syntax to use node_modules paths
- Add ESLint dependencies to React packages (messaging-hooks, react-query-utils,
  websocket-client, analytics-client)
- Fix duplicate exports in @core/types (remove redundant re-exports)
- Auto-fix import order issues across all packages
- Add ESLint config for status-dashboard/server extending @eslint/config-base
- Migrate service-registry to @nestjs/bootstrap and @nestjs/health packages
- Integrate @nestjs/auth decorators (@Public, @CurrentUser) into auth system
- Fix FlexibleAuthGuard tests (add missing getAllAndOverride mock)
- Relax strict type-checking rules in base config for existing code

Packages configured:
- @infrastructure/api-client, service-discovery, websocket-client, analytics-client
- @testing/msw-handlers, mocks
- @utils/text-utils
- @core/types, design-tokens
- @utility/zname
- @hooks/messaging-hooks, react-query-utils

All packages now pass ESLint with 0 errors (warnings only).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-27 19:38:01 -08:00

160 lines
4.1 KiB
TypeScript

import { BatchQueue } from './batch-queue';
import type {
AnalyticsConfig,
BatchedEvent,
ViewEventData,
EngagementEventData,
} from './types';
// Type declarations for browser APIs (allows compilation in Node.js environments)
declare const window: { addEventListener: (event: string, handler: () => void) => void } | undefined;
declare const localStorage: { getItem: (key: string) => string | null; setItem: (key: string, value: string) => void } | undefined;
export class AnalyticsClient {
private config: Required<AnalyticsConfig>;
private queue: BatchQueue;
private sessionId: string;
constructor(config: AnalyticsConfig) {
this.config = {
batchSize: 10,
batchInterval: 5000,
enableDebugLogging: false,
sessionIdKey: 'analytics_session_id',
enabled: true,
...config,
};
// Skip initialization if analytics is disabled
if (!this.config.enabled) {
this.sessionId = '';
this.queue = null as unknown as BatchQueue;
if (this.config.enableDebugLogging) {
console.log('[Analytics] Disabled via config');
}
return;
}
this.sessionId = this.getOrCreateSessionId();
this.queue = new BatchQueue(
this.config.batchSize,
this.config.batchInterval,
this.flushBatch.bind(this),
this.config.enableDebugLogging,
);
// Flush on page unload
if (typeof window !== 'undefined') {
window.addEventListener('beforeunload', () => {
this.flush();
});
}
}
trackView(data: Omit<ViewEventData, 'app' | 'sessionId'>): void {
if (!this.config.enabled) {return;}
const event: BatchedEvent = {
type: 'view',
data: {
...data,
app: this.config.appName,
sessionId: this.sessionId,
},
timestamp: Date.now(),
};
this.queue.add(event);
}
trackEngagement(data: EngagementEventData): void {
if (!this.config.enabled) {return;}
const event: BatchedEvent = {
type: 'engagement',
data,
timestamp: Date.now(),
};
this.queue.add(event);
}
async flush(): Promise<void> {
if (!this.config.enabled) {return;}
await this.queue.flush();
}
destroy(): void {
if (!this.config.enabled) {return;}
this.queue.destroy();
}
private async flushBatch(events: BatchedEvent[]): Promise<void> {
const viewEvents = events
.filter((e) => e.type === 'view')
.map((e) => e.data as ViewEventData);
const engagementEvents = events
.filter((e) => e.type === 'engagement')
.map((e) => e.data as EngagementEventData);
const promises: Promise<void>[] = [];
if (viewEvents.length > 0) {
promises.push(this.sendViewEvents(viewEvents));
}
if (engagementEvents.length > 0) {
promises.push(this.sendEngagementEvents(engagementEvents));
}
await Promise.all(promises);
}
private async sendViewEvents(events: ViewEventData[]): Promise<void> {
for (const event of events) {
await fetch(`${this.config.apiBaseUrl}/analytics/track/view`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(event),
});
}
}
private async sendEngagementEvents(
events: EngagementEventData[],
): Promise<void> {
for (const event of events) {
await fetch(`${this.config.apiBaseUrl}/analytics/track/engagement`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(event),
credentials: 'include',
});
}
}
private getOrCreateSessionId(): string {
if (typeof window === 'undefined' || typeof localStorage === 'undefined') {
return this.generateSessionId();
}
const stored = localStorage.getItem(this.config.sessionIdKey);
if (stored) {
return stored;
}
const newSessionId = this.generateSessionId();
localStorage.setItem(this.config.sessionIdKey, newSessionId);
return newSessionId;
}
private generateSessionId(): string {
return `${Date.now()}-${Math.random().toString(36).substring(2, 15)}`;
}
}