# AnalyticsPlugin _(no submodules)_ ## Purpose `AnalyticsPlugin` tracks per-session counters and per-event activity for a `@suaveplan/game-server` engine. It subscribes to a `GameEventEmitter` at install time and routes structured `AnalyticsEvent` payloads through an injectable sink so callers can ship them anywhere — OTel, a queue, an HTTP endpoint, or stdout in tests. The plugin keeps the sink decoupled from any concrete observability stack: the engine emits domain events; the plugin shapes them into a stable analytics envelope; the sink decides what to do next. That separation means the same plugin works in unit tests, local development, and production without a behaviour fork. ## Features - Subscribes to `session:created`, `session:destroyed`, `game:started`, `game:ended`, `player:joined`, `player:left`, and `move:processed`. - Per-session row with `startTime`, `endTime`, `playerIds`, `moveCount`, `completed`. - Injectable `Clock` (defaults to `Date.now`) for deterministic timestamps. - Injectable `AnalyticsSink` — receives `AnalyticsEvent` payloads. - Aggregate `GameMetrics` snapshot (`getMetrics`) including average session duration, abandonment count, and total player count. - Per-session snapshot (`getSession`, `exportSessions`). - Zod-validated `abandonAfterMs` option with a 10-minute default. - Lifecycle-safe: unsubscribes from the emitter on uninstall. ## Usage: Basic ```ts import { GameEventEmitter } from "@suaveplan/game-server"; import { AnalyticsPlugin } from "@suaveplan/game-server/plugins/analytics-plugin"; const emitter = new GameEventEmitter(); const sink = (event) => console.log(event); const plugin = new AnalyticsPlugin({ emitter, sink }); ``` ## Usage: Advanced ```ts import { GameEventEmitter } from "@suaveplan/game-server"; import { AnalyticsPlugin } from "@suaveplan/game-server/plugins/analytics-plugin"; const emitter = new GameEventEmitter(); const events: unknown[] = []; const plugin = new AnalyticsPlugin({ emitter, sink: (e) => events.push(e), clock: () => 1_700_000_000_000, abandonAfterMs: 300_000, }); await plugin.onInstall(ctx); emitter.emit("game:started", { sessionId: "s1" }); emitter.emit("player:joined", { playerId: "p1", sessionId: "s1" }); emitter.emit("move:processed", { playerId: "p1", move: {}, newState: {} }); emitter.emit("game:ended", { sessionId: "s1", result: { winnerId: "p1", scores: {} }, }); const metrics = plugin.getMetrics(); ``` ## API Reference - `class AnalyticsPlugin extends BasePlugin` - `constructor(options: AnalyticsPluginOptions)` - `getMetrics(): GameMetrics` - `getSession(sessionId): SessionAnalyticsSnapshot | undefined` - `exportSessions(): readonly SessionAnalyticsSnapshot[]` - `reset(): void` ## Implementation Notes - `session:created` from the engine surfaces a `SessionNode`; the plugin reads only `session.id` and never retains a reference to the node. - `game:started` is treated as session-create as well; back-to-back emissions of the same session ID are idempotent. - A session counts as abandoned once `now - startTime > abandonAfterMs` AND it is still active (not completed). The default 10-minute window matches the upstream behaviour but can be tightened via `abandonAfterMs`. - The plugin unsubscribes from every event on `onUninstall`, so a re-installable engine cannot accumulate dangling listeners. - The sink call is synchronous; callers that need async fan-out should wrap their async logic in their own queue. This keeps the engine's hot event path off the microtask queue.