the widget reports itself, and only itself

Rollout needs numbers, and the obvious way to get them is the defect this
codebase already has a scar from: a second client posting to /v1/event counted
every pageview twice.

A script tag cannot share the page's @hanzo/event client -- that client is a
module the app holds, with no global -- so coordination is not available. What
is available is not overlapping: three events named for the widget, emitted by
nothing else. No shared transport, no reconciliation, and a double count is not
expressible rather than merely avoided.

Same door, key and opt-out as the recordings, so a reader who declined
measurement is not measured here either.

Co-authored-by: Hanzo Dev <[email protected]>
This commit is contained in:
2026-08-16 12:43:16 -07:00
co-authored by Hanzo Dev
parent 7075a36aab
commit 0877755b9d
3 changed files with 65 additions and 1 deletions
+21
View File
@@ -115,6 +115,27 @@ settings and a visitor reading the panel are talking about the same thing.
serve `control.js` from `api.hanzo.ai` and it calls `api.hanzo.ai`. The routes used are
`/v1/edit`, `/v1/suggest`, `/v1/me`, `/v1/register` and `/v1/s`.
## What it reports
Three events, to `/v1/event` at `insights.hanzo.ai`, with the page's publishable key
and the same opt-out the recordings honour:
| event | when |
| -------------------- | ----------------------------- |
| `controls_opened` | the panel opens |
| `controls_suggested` | a suggestion is filed |
| `controls_edited` | an edit is submitted |
**It never emits a pageview.** Pageviews and autocapture belong to `@hanzo/event`, the
one telemetry client a page runs. This widget is a script tag and cannot reach that
client, because it is a module the app holds. If it reported a pageview there would be
two clients counting the same load through the same door, which is the defect a second
`hz.js` tag already caused: every pageview counted twice.
So the rule is not to coordinate with the other client. It is to never emit an event
another client also emits. These three are named for the widget and emitted by nothing
else, so there is no overlap to reconcile and no shared transport to keep in step.
## Panes
The launcher opens one panel and the panel shows one pane at a time. Suggest an edit and
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@hanzo/control",
"version": "0.2.0",
"version": "0.3.0",
"description": "Hanzo Control \u2014 the controls surface for Hanzo apps and websites: appearance, language, help, and suggesting or editing the page you are viewing.",
"license": "MIT",
"repository": {
+43
View File
@@ -345,6 +345,46 @@
// A short ring buffer of recent route events, captured from load. Degrades to
// just the initial view when the page never client-navigates.
var USAGE = [];
// What this widget reports about ITSELF, and nothing else.
//
// Pageviews and autocapture belong to @hanzo/event, the one telemetry client a
// page runs. This widget is a script tag and cannot reach that client -- it is
// a module the app holds -- so if it reported a pageview there would be two
// clients counting the same load through the same door, which is exactly the
// defect a second hz.js tag already caused: every pageview counted twice.
//
// So the rule is not "coordinate with the other client", it is "never emit an
// event another client also emits". These are named for the widget and emitted
// by nothing else, so the two planes cannot overlap and there is no shared
// transport to keep in step.
//
// Same door as the recordings (`insights.hanzo.ai`), same publishable key, same
// opt-out: a reader who declined measurement is not measured here either.
function report(name, props) {
try {
if (!REPLAY.key || replayOptedOut()) return;
var sid = sessionId();
fetch(REPLAY.host + '/v1/event', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'omit',
body: JSON.stringify({
api_key: REPLAY.key,
event: 'controls_' + name,
distinct_id: sid,
properties: Object.assign(
{ $session_id: sid, $lib: 'hanzo-control', route: location.pathname },
props || {},
),
}),
}).catch(function () {
/* telemetry must never surface on the page being measured */
});
} catch (e) {
/* nor throw on it */
}
}
function pushUsage(kind) {
try {
USAGE.push({ t: Date.now(), route: location.pathname + location.search, kind: kind });
@@ -1257,6 +1297,7 @@
var effectivePath = (path || '').trim() || (CTX.candidates[0] && CTX.candidates[0].path) || PATH || '';
if (action === 'suggest') {
report('suggested');
var payload = editPayload(effectivePath);
payload.suggestion = text;
busy('Sending…');
@@ -1285,6 +1326,7 @@
return;
}
report('edited', { mode: mode });
postEdit(text, effectivePath, mode);
}
@@ -1560,6 +1602,7 @@
}
function open() {
report('opened');
panel.classList.add('open');
fab.style.display = 'none';
CTX.chosen = '';