- Add .npmrc with scoped Gitea npm registry for @mosaicstack packages - Create MosaicTelemetryModule (global, lifecycle-aware) at apps/api/src/mosaic-telemetry/ - Create MosaicTelemetryService wrapping TelemetryClient with convenience methods: trackTaskCompletion, getPrediction, refreshPredictions, eventBuilder - Create mosaic-telemetry.config.ts for env var integration via NestJS ConfigService - Register MosaicTelemetryModule in AppModule - Add 32 unit tests covering module init, service methods, disabled mode, dry-run mode, and lifecycle management Refs #369 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
165 lines
4.7 KiB
TypeScript
165 lines
4.7 KiB
TypeScript
import { Injectable, Logger, OnModuleInit, OnModuleDestroy } from "@nestjs/common";
|
|
import { ConfigService } from "@nestjs/config";
|
|
import {
|
|
TelemetryClient,
|
|
type TaskCompletionEvent,
|
|
type PredictionQuery,
|
|
type PredictionResponse,
|
|
type EventBuilder,
|
|
} from "@mosaicstack/telemetry-client";
|
|
import {
|
|
loadMosaicTelemetryConfig,
|
|
toSdkConfig,
|
|
type MosaicTelemetryModuleConfig,
|
|
} from "./mosaic-telemetry.config";
|
|
|
|
/**
|
|
* NestJS service wrapping the @mosaicstack/telemetry-client SDK.
|
|
*
|
|
* Provides convenience methods for tracking task completions and reading
|
|
* crowd-sourced predictions. When telemetry is disabled via
|
|
* MOSAIC_TELEMETRY_ENABLED=false, all methods are safe no-ops.
|
|
*
|
|
* This service is provided globally by MosaicTelemetryModule — any service
|
|
* can inject it without importing the module explicitly.
|
|
*
|
|
* @example
|
|
* ```typescript
|
|
* @Injectable()
|
|
* export class TasksService {
|
|
* constructor(private readonly telemetry: MosaicTelemetryService) {}
|
|
*
|
|
* async completeTask(taskId: string): Promise<void> {
|
|
* // ... complete the task ...
|
|
* const event = this.telemetry.eventBuilder.build({ ... });
|
|
* this.telemetry.trackTaskCompletion(event);
|
|
* }
|
|
* }
|
|
* ```
|
|
*/
|
|
@Injectable()
|
|
export class MosaicTelemetryService implements OnModuleInit, OnModuleDestroy {
|
|
private readonly logger = new Logger(MosaicTelemetryService.name);
|
|
private client: TelemetryClient | null = null;
|
|
private config: MosaicTelemetryModuleConfig | null = null;
|
|
|
|
constructor(private readonly configService: ConfigService) {}
|
|
|
|
/**
|
|
* Initialize the telemetry client on module startup.
|
|
* Reads configuration from environment variables and starts background submission.
|
|
*/
|
|
onModuleInit(): void {
|
|
this.config = loadMosaicTelemetryConfig(this.configService);
|
|
|
|
if (!this.config.enabled) {
|
|
this.logger.log("Mosaic Telemetry is disabled");
|
|
return;
|
|
}
|
|
|
|
if (!this.config.serverUrl || !this.config.apiKey || !this.config.instanceId) {
|
|
this.logger.warn(
|
|
"Mosaic Telemetry is enabled but missing configuration " +
|
|
"(MOSAIC_TELEMETRY_SERVER_URL, MOSAIC_TELEMETRY_API_KEY, or MOSAIC_TELEMETRY_INSTANCE_ID). " +
|
|
"Telemetry will remain disabled."
|
|
);
|
|
this.config = { ...this.config, enabled: false };
|
|
return;
|
|
}
|
|
|
|
const sdkConfig = toSdkConfig(this.config, (error: Error) => {
|
|
this.logger.error(`Telemetry client error: ${error.message}`, error.stack);
|
|
});
|
|
|
|
this.client = new TelemetryClient(sdkConfig);
|
|
this.client.start();
|
|
|
|
const mode = this.config.dryRun ? "dry-run" : "live";
|
|
this.logger.log(`Mosaic Telemetry client started (${mode}) -> ${this.config.serverUrl}`);
|
|
}
|
|
|
|
/**
|
|
* Stop the telemetry client on module shutdown.
|
|
* Flushes any remaining queued events before stopping.
|
|
*/
|
|
async onModuleDestroy(): Promise<void> {
|
|
if (this.client) {
|
|
this.logger.log("Stopping Mosaic Telemetry client...");
|
|
await this.client.stop();
|
|
this.client = null;
|
|
this.logger.log("Mosaic Telemetry client stopped");
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Queue a task completion event for batch submission.
|
|
* No-op when telemetry is disabled.
|
|
*
|
|
* @param event - The task completion event to track
|
|
*/
|
|
trackTaskCompletion(event: TaskCompletionEvent): void {
|
|
if (!this.client) {
|
|
return;
|
|
}
|
|
this.client.track(event);
|
|
}
|
|
|
|
/**
|
|
* Get a cached prediction for the given query.
|
|
* Returns null when telemetry is disabled or if not cached/expired.
|
|
*
|
|
* @param query - The prediction query parameters
|
|
* @returns Cached prediction response, or null
|
|
*/
|
|
getPrediction(query: PredictionQuery): PredictionResponse | null {
|
|
if (!this.client) {
|
|
return null;
|
|
}
|
|
return this.client.getPrediction(query);
|
|
}
|
|
|
|
/**
|
|
* Force-refresh predictions from the telemetry server.
|
|
* No-op when telemetry is disabled.
|
|
*
|
|
* @param queries - Array of prediction queries to refresh
|
|
*/
|
|
async refreshPredictions(queries: PredictionQuery[]): Promise<void> {
|
|
if (!this.client) {
|
|
return;
|
|
}
|
|
await this.client.refreshPredictions(queries);
|
|
}
|
|
|
|
/**
|
|
* Get the EventBuilder for constructing TaskCompletionEvent objects.
|
|
* Returns null when telemetry is disabled.
|
|
*
|
|
* @returns EventBuilder instance, or null if disabled
|
|
*/
|
|
get eventBuilder(): EventBuilder | null {
|
|
if (!this.client) {
|
|
return null;
|
|
}
|
|
return this.client.eventBuilder;
|
|
}
|
|
|
|
/**
|
|
* Whether the telemetry client is currently active and running.
|
|
*/
|
|
get isEnabled(): boolean {
|
|
return this.client?.isRunning ?? false;
|
|
}
|
|
|
|
/**
|
|
* Number of events currently queued for submission.
|
|
* Returns 0 when telemetry is disabled.
|
|
*/
|
|
get queueSize(): number {
|
|
if (!this.client) {
|
|
return 0;
|
|
}
|
|
return this.client.queueSize;
|
|
}
|
|
}
|