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>
| Attribute | Required | Meaning |
|---|---|---|
data-slug | yes | The state machine slug the page emits into |
data-key | yes | The state-machine key (zsm_*) authorising the emissions |
data-types | no | Comma-separated type filter; defaults to all auto-types |
data-encrypt | no | "true" flips the agent into R3 phase-locked mode |
data-heartbeat | no | Override 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.
| Observer | Fires on | meta.kind |
|---|---|---|
page | First paint after DOMContentLoaded | page-loaded |
nav | popstate and pushState/replaceState patches | navigation |
visibility | document.visibilitychange | visible / hidden |
network | online and offline events | network-up / network-down |
error | window.onerror and unhandledrejection | error |
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:
- No external dependencies. Anything the agent uses must be available in evergreen browsers without a polyfill.
- The
data-encrypt="true"path usescrypto.subtledirectly — no SubtleCrypto polyfill, no ASN.1 parsing. - 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 agentshared/api-core/src/routes/chain.ts— receiver (/api/chain/:slug/event)shared/api-core/src/lib/zeqField.ts—zeqEncryptPhaseLockedfor the encrypt path