Logged-player analytics
online-services-front sends UI component analytics to Heimdall only when the current authentication type represents a logged player and a non-empty player access token is available. On a cache miss, the edge validates that bearer through the Player API. A successful validation remains accepted for the configured five-minute positive cache window.
Request path
The prepared dev1 frontend is configured with one exact cross-origin URL:
https://heimdall-edge.spark-test.newtoncelestial.com/api/heimdall/events
The dedicated hostname is owned by the Heimdall Ingress. One exact Ingress rule sends only /api/heimdall/events to the edge Service. Verified frontend origins are CORS and trusted tenant/site mappings; they are not Heimdall Ingress hosts. Raw Domainator and config-provider exports must be normalized and reconciled first. The normalized file must contain matching host sets and only unique valid route and tenant/site entries. edgevalues validates the whole normalized input and rejects it without output on any drift; it does not filter raw records.
The frontend rejects an analytics URL before attaching the bearer token when the URL:
- is not exactly
/api/heimdall/eventsfor local same-origin development or the dedicated dev1 URL above - differs by scheme, hostname, port, path, trailing slash, query, fragment, or URL credentials
This is an exact string allowlist, not a suffix or same-zone check. The configured value is rejected before the token is queued or attached. Cookies are omitted, redirects fail instead of being followed, no referrer is sent, and the existing player bearer is sent only in the Authorization header.
Generic frontend Helm defaults keep analytics_enabled: "0" and analytics_api_url: "". Local development enables analytics with the exact relative path /api/heimdall/events, which Vite proxies to the local edge. The deployed spark-dev1 override is owned by automation-micros-argo and sets analyticsEnabled: "1" with the exact dedicated HTTPS URL. Production and other environments remain off until they receive their own reviewed endpoint, origin map, and deployment approval. This is prepared configuration only; it does not authorize a remote deployment.
The browser never supplies the player-validation IP header. In an environment deployment, the Ingress must remove any incoming value and set one dedicated X-Heimdall-Client-IP header from the connection metadata. Heimdall accepts that header only when the direct peer belongs to HEIMDALL_EDGE_TRUSTED_PROXY_CIDRS; an untrusted peer, missing or duplicate header, or malformed IP fails closed. Local mode may omit the CIDR list and use the direct socket IP instead.
Browser request
POST /api/heimdall/events HTTP/1.1
Origin: https://casino.example
Authorization: Bearer <player-access-token>
Content-Type: application/json
{
"event_id": "9f62b6b2-f3ad-4f38-b166-febd8e936401",
"body": {
"schema_version": 1,
"session_id": "session-1",
"page_visit_id": "visit-1",
"timestamp": "2026-07-13T09:15:00.123Z",
"user": {},
"page": {},
"interaction": {},
"product": {},
"event_payload": {
"component": "checkout-button",
"action": "click"
}
}
}
Ordinary analytics omit event_kind and Heimdall assigns the trusted event type
ui.analytics.v1. Casino presence adds only the allowlisted browser hint
"event_kind": "casino_presence". Its EventData payload uses
product.feature_used: "casino_presence" and:
{
"action": "opened",
"presence_session_id": "e48f2618-2a2c-4dd1-8a14-0d14122c02b7"
}
The permitted actions are opened, heartbeat, and closed. A closed event
uses close_reason equal to last_tab or logout. Heimdall maps the hint to
the trusted event type player.casino-presence.v1, derives its partition key
from trusted tenant, site, and player identity, and routes it only to
heimdall.player-casino-presence.v1. Unknown non-empty event kinds are rejected
with HTTP 400. Browser-supplied event type, topic, identity, and partition key
never participate in this mapping.
The maximum encoded request size is 256 KiB and the maximum object or array depth is 10. Session and page-visit identifiers must be non-empty strings no longer than 256 bytes. The bearer is limited to 4,096 characters. The timestamp must be RFC 3339.
Frontend delivery policy
- Only authentication types
userandtokenare considered logged-player sessions. - One event UUID and one serialized request body are created before the first attempt.
- At most three attempts are made.
- Network errors, timeouts,
502,503, and504are retryable. - Permanent responses, including
400,401,403,413,429, and500, are not retried. Retry-Afteris honored up to five seconds; otherwise exponential backoff with jitter starts at 250 ms.- Each attempt has a five-second timeout.
- At most four requests run concurrently and at most 100 events wait in memory.
- The frontend accepts 100 analytics events per minute by default;
analyticsMaxEventsPerMinutecan lower or raise that local browser bound. keepaliveis used only while each body and the four-request group stay within the browser quota.
This queue is deliberately bounded and memory-only. Analytics delivery must not block gameplay or grow without limit when Heimdall is unavailable.
Casino presence reuses this queue. Tabs coordinate through a versioned local
storage lease and BroadcastChannel, with storage events and periodic local
reconciliation as fallback. One elected leader sends a heartbeat every 30
seconds; followers send no presence traffic. Leadership transfers reuse the
same presence_session_id without another opened event. Last-tab and logout
closes are best effort and use the existing keepalive path. If local storage is
denied or the coordination record is corrupt, presence fails closed rather than
sending one heartbeat stream per tab. A future consumer must treat 120 seconds
without a heartbeat as offline because browser close delivery is not guaranteed.
CORS contract
Dev1 uses the dedicated cross-origin URL. The browser therefore preflights the request because Authorization and Content-Type: application/json are not CORS-safelisted. The OPTIONS request contains the browser Origin, requested method, and requested header names, but no bearer or event body. Local development may continue to use the exact same-origin path.
Each allowed origin is configured exactly in the generated origin map. Environment overlays mount the map from the chart-managed heimdall-edge-origins ConfigMap; HEIMDALL_EDGE_ORIGINS_JSON is retained only for explicitly selected, size-safe local inline use. CORS is a browser-origin gate, not authentication: non-browser clients can set an Origin header, so the player bearer and player validation remain mandatory. The edge:
- accepts only configured origins
- accepts
POSTandOPTIONS - permits only
AuthorizationandContent-Typerequest headers - caches a successful preflight for 600 seconds
- exposes
Retry-After - does not emit
Access-Control-Allow-Credentials - never uses wildcard or
nullorigins
Preflight is answered before player validation and the POST handler's in-flight controls. Browser caching for 600 seconds reduces repeat traffic, but cold sessions and cache expiry still create unauthenticated OPTIONS load. Capacity and ingress checks must exercise both OPTIONS and POST, and an unexpected preflight increase must be monitored separately from accepted event volume.
Allowed-origin application errors keep Access-Control-Allow-Origin, so the frontend can read statuses such as 401, 413, 429, and 503. A missing or unapproved origin receives 403 without an allow-origin header.
Origin configuration is canonical and exact: use lowercase schemes and hostnames, omit default ports, and do not add a path, query, fragment, or credentials. Non-local origins must use HTTPS. Do not add the dedicated analytics hostname to the map unless a UI is actually served from it; the map identifies the calling frontend, not the receiving Ingress.
Player validation and Redis caching
An origin entry supplies trusted tenant_id, site_id, and player_url values. The dev contract permits only the Quasar URL http://spark-dev1-quasar-svc.core-spark-test1.svc.cluster.local/getPlayer. The production overlay retains http://micros-prod-na-01-player-api-svc.micros-prod-na-01.svc.cluster.local:8666/casinoapi/getPlayer, but that target is disabled and has not been live-verified for Heimdall.
The edge calls the configured endpoint with:
- method
GET - the browser bearer in
Authorization - JSON body
{ "ip": "<canonical-client-ip>", "site_id": <trusted-positive-decimal-site-id> } - a two-second default timeout
- redirects disabled
Only positive decimal client_player_id and global_player_id values are accepted, either at the response root or under data. Player responses are capped at 64 KiB. HEIMDALL_EDGE_PLAYER_INVALID_STATUSES controls which upstream 4xx responses mean a definitive invalid player and defaults to 401,403,422; 422 covers Quasar's inactive-player response without making every upstream failure permanent. 429 always remains rate limited, while other failures are treated as player-validation unavailability.
Successful resolutions are cached in Redis for 300 seconds. Definitive invalid-player results are cached for five seconds. Transient failures are not cached. Cache and rate-limit keys use an HMAC digest; the bearer and canonical client IP contribute only to that digest and are never stored as raw Redis keys or values. The bearer and trusted IP reach the edge and configured player-validation endpoint, but neither is logged or copied into the event sent to ingest. Redis values contain the validated player IDs and Redis is therefore an authentication-critical dependency. Local and distributed miss coalescing limit duplicate player-validation calls during a burst.
Read-only live contract proof confirmed that the public BFF and direct Quasar calls both returned HTTP 200 and the same positive client_player_id and global_player_id pair. The bearer and identifiers were redacted. The dev public certificate was expired, so the BFF check required local curl --insecure; the internal Quasar HTTP call was unaffected. This verifies the dev response contract, not a Heimdall deployment or production readiness.
Trusted identity and privacy
The browser body is treated as untrusted. Before forwarding, the edge:
- sets tenant and site from the origin map
- replaces
user.user_idanduser.user_global_idwith validated Player API values - sets
user.user_authtotrue - fixes
sourcetoonline-services-front - assigns
ui.analytics.v1to ordinary analytics andplayer.casino-presence.v1only to the allowlistedcasino_presencehint - never adds the trusted client IP used for player validation to the analytics envelope
Retains useful UI context such as component names, selectors, DOM text, and safe URL information. The sanitizer recursively removes or redacts:
- authorization, cookies, passwords, tokens, API keys, private keys, and credentials
- URL user information and sensitive query or fragment pairs
- payment card numbers, security codes, IBANs, and bank-account fields
- sensitive values inside serialized JSON strings
Safe query parameters and fragments remain available. Other identifiers and arbitrary event fields can also remain. Do not intentionally place secrets or unnecessary personal data in analytics payloads: the sanitizer is a denylist-based defense-in-depth boundary, not a DLP system or a guarantee that Kafka events contain no personal data.
Response behavior
| Status | Meaning | Frontend retry |
|---|---|---|
202 | JetStream acknowledged the event | No |
400 | Invalid request or sanitized session ID changed | No |
401 | Missing, malformed, or invalid player bearer | No |
403 | Origin or preflight is not allowed | No |
413 | Request exceeds 256 KiB | No |
429 | Redis-backed rate limit reached | No |
502, 503, 504 | Temporary edge or downstream failure | Yes, within the three-attempt budget |
Local verification
Start Heimdall and send the built-in browser event:
make local-up
make local-edge-inject
The local allowlist includes http://localhost:5000 and http://localhost:7000. online-services-front proxies the exact analytics path to VITE_HEIMDALL_EDGE_URL, which defaults to http://localhost:18084.
Verify readiness, queue drain, and Kafka advancement rather than trusting the HTTP status alone:
curl --fail http://localhost:18084/ready
curl --fail http://localhost:18081/admin/v1/queues