first commit

This commit is contained in:
2026-05-29 08:20:56 +01:00
commit 949299f58d
6368 changed files with 527176 additions and 0 deletions

View File

@@ -0,0 +1,97 @@
# AnalyticsPlugin
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## 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.