first commit
This commit is contained in:
@@ -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.
|
||||
Reference in New Issue
Block a user