@forbidden/game-telemetry (0.1.0)
Installation
@forbidden:registry=https://git.forbiddenprojects.dev/api/packages/forbidden/npm/npm install @forbidden/game-telemetry@0.1.0"@forbidden/game-telemetry": "0.1.0"About this package
@forbidden/game-telemetry
One framework-neutral client for the Forbidden Games API. It owns anonymous-device bootstrap,
session lifecycle, page-view and mission telemetry, bounded retry, and the small browser/Godot Web
adapters. Games provide only their catalog gameId, public API URL, build identifier, and gameplay
signals.
The package contains no service credential. The generated anonymous device secret and backend session token are player-scoped API credentials: the identity may be stored by the caller, while the session token and retry queue stay in memory.
Browser
import { createBrowserTelemetry } from "@forbidden/game-telemetry/browser";
const telemetry = createBrowserTelemetry({
gameId: "ashwater_parish",
baseUrl: import.meta.env.VITE_EUGAMES_API_BASE_URL,
build: import.meta.env.VITE_GIT_SHA,
});
await telemetry.start(); // false on failure; gameplay continues
telemetry.client.mission.start("silent_farm_01");
telemetry.client.mission.complete("silent_farm_01");
start() emits exactly one visit.start and one session.start for the backend session. An
in-flight batch is immutable: its encoded body, event IDs, and request idempotency key remain stable
even when a response is lost and new events arrive. A 409 is not accepted or dropped. The queue is
bounded and in-memory, each encoded batch stays below 64 KiB, and an offline API never blocks the
game. After bounded immediate attempts, only one delayed continuation is scheduled.
The browser adapter uses credential-capable fetch(..., { keepalive: true }) for pagehide
teardown; sendBeacon cannot carry the required authorization headers. A persisted BFCache
pageshow waits for teardown and starts a new resume session. Applications should use this adapter
instead of adding their own unload handler.
Explicit opt-in (When the Room Blinks)
import {
SLEEP_HORROR_EXTENSION,
createGameTelemetryClient,
createSleepHorrorConsent,
} from "@forbidden/game-telemetry";
const consent = createSleepHorrorConsent(localStorage); // unset means denied
const telemetry = createGameTelemetryClient({
gameId: "sleephorror",
baseUrl: import.meta.env.VITE_EUGAMES_API_BASE_URL,
identityStorage: localStorage,
consent,
extensions: [SLEEP_HORROR_EXTENSION],
});
consent.grant();
await telemetry.start();
// consent.withdraw() aborts work, clears the queue/session and erases stored identity.
Refusing consent makes no network request. Gameplay must remain available whether consent is unset, denied, withdrawn, storage fails, or Games API is unavailable.
Godot Web
The @forbidden/game-telemetry/godot-web bridge exposes JSON-free start, resume, mission*,
end, and dispose methods suitable for JavaScriptBridge. Construct the client in the web shell;
do not pass a token or service credential through Godot.
Extension boundary
Core events and mission lifecycle are built in. A game-specific extension must be explicitly registered with an allowlisted name and validator. Payloads are additionally limited by byte size, depth, item count, and forbidden privacy/credential keys. The Games API contract must already accept that extension name; this is not an escape hatch for arbitrary analytics.
Publishing
The package is configured for the Forbidden organization npm registry and prepack always builds a
fresh dist, including from a clean checkout. Publication belongs to the
shared organization package publisher operated by forbidden-operator; repositories must not add
another workflow, service user, token, or .npmrc credential. Consumers pin an immutable published
version and commit the lockfile.
Version 0.1.0 is the first immutable release candidate. Its default-branch merge is the deliberate
publication trigger and must happen only after the digest-pinned delivery-policy runner and the
exact forbidden/eugames npm caller are live. The publisher proves that both version 0.1.0 and
the full-source-SHA dist-tag are absent before mutation, then verifies registry SHA-1, SHA-512 SRI,
version and source-tag readback. Any existing version, conflicting tag or ambiguous registry result
fails closed instead of overwriting package state.
A default-branch publication event is single-use. If infrastructure skips the npm preparation job before it starts, do not rerun that consumed event against changed central workflow source. Land a reviewed, meaningful source-only clarification after the central fix instead; the resulting fresh default-branch SHA is then the only release candidate and provenance tag.
Release evidence distinguishes the original scoped-caller event (push) from execution inside the
reusable workflow (workflow_call). Both identities are validated independently before the
credential-free preparation can mark a package eligible.
Npm preparation uses the qualified delivery runner's installed delivery-contract binary directly;
it does not require a host Docker socket. Before eligibility, the runner proves the exact contract API
version and all packaging dependencies with a credential-free fixture.
The credential-bearing publication boundary first performs a supported, read-only package-list GET
scoped exactly to owner forbidden, type npm, query @forbidden/game-telemetry, and limit 1.
Only an unredirected HTTP 200 with a strict empty or exact-target package array can proceed to the
immutable absence checks. This proves the package capability target without broadening the
package-only token; the forbidden-operator identity remains an owner-provisioned secret invariant,
not a runtime identity claim. The token crosses into the publisher only through a mode-restricted,
single-link file that is removed before registry calls; it is never a command argument or part of
the publication artifact.
Dependencies
Development Dependencies
| ID | Version |
|---|---|
| @eugames/contracts | 0.0.0 |
| typescript | ^5.8.2 |
| vitest | ^3.2.4 |