Files
windmill/docs/feature-telemetry.md
T
hugocasa cbcdcbd28b docs(agents): guidance for building a feature — reuse, telemetry, live verification
Three recurring gaps, all cases where a pointer existed but nothing triggered
on it.

Component reuse. The svelte-frontend skill documented three components with
props, which reads as the whole catalog; the barrel exports 23 and common/ has
34 subdirectories against those 23. So "never use raw HTML elements" was an
instruction agents could not follow. Added a mandatory discovery step: read
the barrel, grep the tree, and treat the documented three as examples.

Brand guidelines. frontend/brand-guidelines.md is 34k characters referenced by
bare path, which nothing opens speculatively. Added a table mapping what you
are building to the section that governs it, entered with grep rather than a
full read.

Product telemetry. feature_usage has 14 registered actions across three
features, and an unregistered (feature, kind) pair is dropped by
valid_feature_usage_event with a bare continue — no error, still a 204 — so
frontend-only instrumentation silently records nothing. New
docs/feature-telemetry.md carries the criteria for when to instrument, the
four-step recipe including the allowlist and the InstanceSettings disclosure,
and the privacy rules. Raised in the plan for user-facing work, not as a
separate question, and not at all for bugfixes or refactors.

Also: validation now ends at exercising the change on the running instance,
with standing permission to spin up whatever that takes.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-12 18:23:30 +02:00

4.5 KiB

Feature usage telemetry

feature_usage is the product-telemetry accumulator: day-bucketed counters that roll into the anonymous usage-stats payload. It answers "does anyone use this, and which variant do they pick" without any identifying data leaving the instance.

It currently carries 14 registered actions across three features (ai_session, ai_chat, flow_editor). Nearly all of the product is uninstrumented, so new user-facing work is the opportunity to change that.

When to instrument

Raise it in the plan, with the concrete vocabulary written out, and let the user keep or drop it in one line. Don't stop and ask as a standalone question.

Propose it when a new user-facing affordance leaves a real question open:

  • a new panel, mode, tab, toggle, or entry point — is it discovered and used at all?
  • competing UX paths, or a new default — which one wins?
  • an opt-in or beta gate — what is the take rate?
  • a multi-step flow — where do people stop?

Stay silent for bugfixes, refactors, internal plumbing, and anything whose useful signal would need per-item identifiers (paths, names, prompts, code) — those cannot be logged at all, see Privacy rules. If the answer wouldn't change a decision, instrumenting is overkill; say nothing.

Designing the vocabulary

Field Meaning Limits
feature Product area: ai_chat, flow_editor ≤50 chars
kind The action within it: message, panel_placement. (feature, kind) is the allowlisted pair ≤50 chars
key A facet of the action — mode, tab kind, tool name, provider:model. Aggregation groups by (feature, kind, key), so this is what splits one counter into comparable buckets ≤100 chars, identifier-shaped, optional
entity_id An opaque random id (e.g. a session id) when you need per-entity distributions rather than a flat count ≤50 chars, identifier-shaped, optional
value Increment, default 1 clamped to 1…1,000,000

Identifier-shaped means ASCII alphanumerics plus _ - : . / — no spaces. Anything else is rejected.

Supplying entity_id is what unlocks the distribution stats: the payload reports entity_count, total_value, median_value, p90_value, and inactive_3d_entity_count per (feature, kind, key). Omit it for a plain "how many times did this happen" counter. Keep the key vocabulary closed and small — enumerate the values in a TS union next to the call site, the way flowEditorTelemetry.ts does, so the whole set is reviewable in one place.

The recipe

Four steps. Skipping step 1 or 3 fails quietly.

1. Register the pair in FEATURE_USAGE_KINDS (backend/windmill-api-workspaces/src/workspaces.rs). An unregistered (feature, kind) is dropped by valid_feature_usage_event with a bare continue — no error, no log, still a 204 to the browser. Frontend-only instrumentation records nothing and looks like it worked.

2. Log from the frontend:

import { logFeatureUsage } from '$lib/utils/featureUsage'

logFeatureUsage('flow_editor', 'panel_placement', { key: 'force_detach' })

Fire-and-forget. Events sum locally per (workspace, feature, kind, key, entityId) and flush every 30s, on visibilitychange → hidden, and on pagehide; 50 events per request, and a failed batch is dropped rather than retried.

3. Update the disclosure. InstanceSettings.svelte lists what a non-minimal payload contains (two places — the copy appears twice). A new counter that isn't named there means the instance under-discloses what it sends. This has already drifted once.

4. Verify a row lands. The silent-drop path means "no error" proves nothing:

SELECT feature, kind, key, entity_id, day, value FROM feature_usage ORDER BY updated_at DESC LIMIT 10;

Privacy rules

Only aggregated counts ever leave the instance, and only when telemetry is enabled and minimal mode is off. Never put a path, prompt, script body, workspace name, email, or any user identifier into key or entity_id. Entity ids must be opaque random ids, never anything that maps back to a user or a resource. If the signal you want can only be expressed with identifying data, it cannot be collected — drop it.

Counters aggregate over the last 30 days; rows are pruned after 60.

Backend-only features

Ingestion is frontend-only: log_feature_usage is an HTTP route the browser posts to, and there is no Rust-side helper. A feature with no UI cannot be instrumented today without adding one. Scope the default to user-facing work, and say so rather than implying backend coverage exists.