Files
skills/docs/gaming/games/game-server/src/plugins/analytics-plugin/analytics-plugin.md
2026-05-29 08:20:56 +01:00

3.5 KiB

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

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

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.