跳至主要内容

Web JS — zeq-observer-client.js

The Web JS form factor of the Observer Agent drops into any HTML page with a single <script> tag. From that moment on, the page emits typed observations into a Zeq state machine, computes its own forensic signals locally, queues offline events in localStorage, and runs an 8-zeqond heartbeat ticker without any further wiring.

It is 25 KB uncompressed, has zero dependencies, and is served from https://zeqapi.com/zeq-observer-client.js so a <script src="…"> is sufficient. Same-origin is fine; CORS is permissive.

Drop-in tag

<script
src="https://zeqapi.com/zeq-observer-client.js"
data-slug="zeq07792026349"
data-key="zsm_live_2c5e12b5b3f1ad99…"
data-types="page,nav,visibility,network,error"
data-encrypt="true"
></script>
AttributeRequiredMeaning
data-slugyesThe state machine slug the page emits into
data-keyyesThe state-machine key (zsm_*) authorising the emissions
data-typesnoComma-separated type filter; defaults to all auto-types
data-encryptno"true" flips the agent into R3 phase-locked mode
data-heartbeatnoOverride the 8-zeqond heartbeat cadence (in zeqonds)

The data-key is the state-machine key, not the account API key. See State machines for the distinction.

Public API

// Manual observation — fully typed
window.zeqObserver.observe({
type: "user.action",
state_hash: "<sha256 of relevant state>",
meta: { kind: "checkout-started", cart_total_minor: 4299 }
});

// Force an outbox flush
window.zeqObserver.flush();

// Inspect the current ticker + outbox
const s = window.zeqObserver.status();
// { phase: 0.523, outbox_depth: 0, last_zeqond: 2287568432n,
// session: "abc123…", agent: "zeq-observer-js/1.287.5" }

The browser's Crypto.subtle is used for AES-256-GCM when data-encrypt="true" — the agent will refuse to start in encrypted mode on a non-https:// origin without a ?dev=1 query opt-in.

Backwards-compatibility aliases

The earlier zeq-tracker.js had three globals; they are aliased on window so existing pages keep working:

window.zeqTrack === window.zeqObserver.observe
window.zeqTrackerFlush === window.zeqObserver.flush
window.zeqTrackerStatus === window.zeqObserver.status

The aliases are scheduled for removal in v1.288 — switch new code over to zeqObserver.

Auto-observers

Out of the box the agent installs five lifecycle observers. None of them collect PII; they fire on browser events that any well-behaved analytics agent already hooks.

ObserverFires onmeta.kind
pageFirst paint after DOMContentLoadedpage-loaded
navpopstate and pushState/replaceState patchesnavigation
visibilitydocument.visibilitychangevisible / hidden
networkonline and offline eventsnetwork-up / network-down
errorwindow.onerror and unhandledrejectionerror

Each fires through the same observe() path, so HF6/HF8/HF12/HF14/HF18 are computed for them too. Filter the set with data-types="…" to opt out (e.g. data-types="page,nav" only).

Outbox + heartbeat

Observations queue into localStorage under the key zeq.observer.outbox.<slug>. Capacity defaults to 200 events; overflow drops oldest first and emits a synthetic type: "outbox.overflow" row when the next flush succeeds.

The heartbeat ticker emits a type: "heartbeat" row every 8 zeqonds (≈6.2 s). Its body:

{
"type": "heartbeat",
"payload": {
"observation": "agent-alive",
"state_hash": "<sha256 of session+last_event_zeqond>",
"zeqond": "2287568440",
"phase": 0.617,
"meta": {
"outbox_depth": 0,
"last_event_z": "2287568432",
"session": "abc123…",
"form_factor": "web-js"
},
"agent_signals": { "HF6": 0.142, "HF8": 0.0, "HF12": 0.881, "HF14": 1.0, "HF18": 0.029 }
}
}

When the page goes offline, the heartbeat keeps queuing locally; when the next online event fires, the outbox drains in order. The row that finally succeeds carries meta.gap_zeqonds so the entangled state row makes the offline window an explicit forensic signal.

Local HF computation

The five HF signals shipped with each row are computed against the local outbox and the agent's own ticker. The implementations are ~8 lines each and are inlined in zeq-observer-client.js:

// HF6 — phase-bucket entropy of the last 256 emissions
function hf6(buckets) {
const total = buckets.reduce((a, b) => a + b, 0);
if (!total) return 0;
return -buckets
.map(b => b / total)
.filter(p => p > 0)
.reduce((acc, p) => acc + p * Math.log2(p), 0) / 3; // 8-bucket → log2(8)=3
}

The other four follow the same shape — closed-form, no allocations per call, no setTimeout dependency. The agent is safe to ship inside a 1 kHz event loop.

Build expectations

The single file at apps/zeq-dev/public/zeq-observer-client.js is the canonical artifact. Its three invariants:

  1. No external dependencies. Anything the agent uses must be available in evergreen browsers without a polyfill.
  2. The data-encrypt="true" path uses crypto.subtle directly — no SubtleCrypto polyfill, no ASN.1 parsing.
  3. The full file gzips to under 8 KB and parses in under 5 ms on a 2018-class mobile CPU.

File map

  • apps/zeq-dev/public/zeq-observer-client.js — the agent
  • shared/api-core/src/routes/chain.ts — receiver (/api/chain/:slug/event)
  • shared/api-core/src/lib/zeqField.tszeqEncryptPhaseLocked for the encrypt path