mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-08-18 16:02:10 +00:00
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>
This commit is contained in:
@@ -94,6 +94,13 @@ Use `tokio::sync::mpsc` (bounded) for channels. Avoid `std::thread::sleep` in as
|
||||
|
||||
Always use rust-analyzer LSP for go-to-definition, find-references, and type info. Do not guess at module paths.
|
||||
|
||||
## Feature Telemetry
|
||||
|
||||
`FEATURE_USAGE_KINDS` in `windmill-api-workspaces/src/workspaces.rs` is an allowlist: a
|
||||
`(feature, kind)` pair missing from it is dropped by `valid_feature_usage_event` with a bare
|
||||
`continue` — no error, and the route still returns 204. Adding a counter on the frontend without
|
||||
registering it here records nothing. See `docs/feature-telemetry.md`.
|
||||
|
||||
## Axum Handlers
|
||||
|
||||
Destructure extractors directly in function signatures:
|
||||
|
||||
@@ -7,9 +7,47 @@ description: Svelte coding guidelines for the Windmill frontend. MUST use when w
|
||||
|
||||
Apply these Windmill-specific patterns when writing Svelte code in `frontend/`. For general Svelte 5 syntax (runes, snippets, event handling), use the Svelte MCP server.
|
||||
|
||||
## Before writing any UI (MUST)
|
||||
|
||||
Do both of these before the first line of markup — not after, and not only when something
|
||||
looks unfamiliar.
|
||||
|
||||
**1. Find the component that already exists.** `frontend/src/lib/components/common/index.ts`
|
||||
is the design-system barrel — 28 lines, read it in full. It exports far more than the three
|
||||
documented below: `Alert`, `Badge`, `Breadcrumb`, `Drawer`/`DrawerContent`, `Menu`/`MenuItem`,
|
||||
`Tabs`/`Tab`/`TabContent`, `Skeleton`, `FileInput`, `RadioCard`, `Section`, `Kbd`, `ActionRow`,
|
||||
`ClearableInput`, `CopyButton`, `SecondsInput`, `UndoRedo`, `Url`.
|
||||
|
||||
The barrel is not the full picture either: `common/` has 34 subdirectories and only 23 exports,
|
||||
so `modal/`, `popup/`, `stepper/`, `tooltip/`, `checkbox/`, `table/`, `contextmenu/`,
|
||||
`confirmationModal/`, `calendarPicker/`, `fileUpload/`, `toggleButton-v2/` and more exist but
|
||||
must be imported by path. Selects, text inputs and melt-based primitives sit next to `common/`
|
||||
in `components/select/`, `components/text_input/`, `components/meltComponents/`.
|
||||
|
||||
The tree holds 1,600+ components — grep `frontend/src/lib/components` for the thing you're about
|
||||
to build; it almost certainly exists. Building a new one is the last resort, not the first move.
|
||||
|
||||
**2. Read the guideline for what you're building.** `frontend/brand-guidelines.md` is the
|
||||
authority on how it should look and read. Don't load all 34k chars — jump to the section:
|
||||
|
||||
| Building | Section to read |
|
||||
|---|---|
|
||||
| Any new screen or component | `# Components` (Core Rules, Quick Reference) |
|
||||
| Buttons, CTAs | `## Buttons` — hierarchy matters, only one Accent per view |
|
||||
| Colors, surfaces, borders | `# Color system` (Quick Reference, Do's and Don'ts) |
|
||||
| Text, labels, headings | `# Typography` — note `## Text Casing`, sentence case throughout |
|
||||
| Spacing, grids, page structure | `# Spacing & Layout`; `# Layout` → `## Form` for forms |
|
||||
| Shadows, overlays, depth | `# Elevation` |
|
||||
| Icons | `# Iconography` |
|
||||
| Wording of any UI copy | `# Voice & Communication`, `# Tone of Voice` |
|
||||
|
||||
Get the line range with `grep -n '^#' frontend/brand-guidelines.md`, then read just that span.
|
||||
|
||||
## Windmill UI Components (MUST use)
|
||||
|
||||
Always use Windmill's design-system components. Never use raw HTML elements.
|
||||
Always use Windmill's design-system components. Never use raw HTML elements. The three below
|
||||
are the ones you'll reach for most often — they are examples, not the catalog. For anything
|
||||
else, go back to the barrel and grep.
|
||||
|
||||
### Buttons — `<Button>`
|
||||
|
||||
@@ -70,6 +108,14 @@ Form components (TextInput, Toggle, Select, etc.) should use the unified size sy
|
||||
- Use Windmill's theming classes for colors/surfaces (see `frontend/brand-guidelines.md`)
|
||||
- Read component props JSDoc before using them
|
||||
|
||||
## Feature Telemetry
|
||||
|
||||
New user-facing UX is the main source of `feature_usage` counters — propose them in the plan, not
|
||||
as a separate question, and read `docs/feature-telemetry.md` first. `logFeatureUsage()` from
|
||||
`$lib/utils/featureUsage` is only half the change: the `(feature, kind)` pair must also be
|
||||
registered in the backend allowlist or every event is silently discarded, and the disclosure copy
|
||||
in `InstanceSettings.svelte` must name what you added.
|
||||
|
||||
## Svelte MCP Server
|
||||
|
||||
Use the Svelte MCP tools when working on Svelte code:
|
||||
|
||||
@@ -5,9 +5,22 @@ Open-source platform for internal tools, workflows, API integrations, background
|
||||
## Workflow
|
||||
|
||||
1. **Understand**: Before coding, explore the codebase (see Code Navigation below). Use `outline` to understand file structure, `body` to read specific symbols, `def`/`callers`/`callees` to trace code, `Grep` to find usages. Read `docs/` for domain context.
|
||||
2. **Plan**: For non-trivial changes, use plan mode. For large features, break into reviewable stages
|
||||
2. **Plan**: For non-trivial changes, use plan mode. For large features, break into reviewable stages.
|
||||
For a new user-facing feature, put the `feature_usage` telemetry in the plan as a proposed item
|
||||
(see `docs/feature-telemetry.md`) so the user can keep or drop it — don't ask separately, and
|
||||
don't instrument bugfixes or refactors.
|
||||
3. **Execute**: Follow coding patterns from skills (`rust-backend`, `svelte-frontend`)
|
||||
4. **Validate**: After every change, run the appropriate checks per `docs/validation.md`
|
||||
4. **Validate**: After every change, run the appropriate checks per `docs/validation.md`, then
|
||||
**exercise the change on the running instance**. Type-checks are not verification. Whatever the
|
||||
change touches, get that path actually running, and stand up whatever that takes — this is
|
||||
expected, not a last resort. A few examples, not a closed list: drive the UI with the Playwright
|
||||
MCP, run a real job of the kind you touched, restart the backend with the cargo features the
|
||||
path needs (`backend/CLAUDE.md`), put a stub in front of an upstream, start MinIO for an S3
|
||||
path, plant state with SQL, exercise it through the `wmill` CLI. If the path you need has no
|
||||
obvious way in, invent one rather than skipping it; `docs/` carries recipes for several areas.
|
||||
If it needs a credential or a third-party account, ask for one rather than skipping the test or
|
||||
inventing a value. If you genuinely cannot exercise it, say which path went unexercised instead
|
||||
of implying it was verified.
|
||||
|
||||
## Documentation
|
||||
|
||||
@@ -17,6 +30,9 @@ Open-source platform for internal tools, workflows, API integrations, background
|
||||
reaches the DB only through the API, so `Connection::Http` paths are never taken by a plain
|
||||
`cargo run`; a normal build cannot start one at all.
|
||||
- **Enterprise**: `docs/enterprise.md` — EE file conventions and PR workflow
|
||||
- **Product telemetry**: `docs/feature-telemetry.md` — when to instrument a new feature with
|
||||
`feature_usage`, and the four-step recipe. An unregistered `(feature, kind)` pair is dropped
|
||||
silently, so frontend-only instrumentation records nothing.
|
||||
- **Backend patterns**: use the `rust-backend` skill when writing Rust code
|
||||
- **Frontend patterns**: use the `svelte-frontend` skill when writing Svelte code. Do NOT edit svelte files unless you have read that skill.
|
||||
- **Frontend UUIDs**: do not call `crypto.randomUUID()` in frontend code. Import `randomUUID` from `$lib/utils/uuid` instead.
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
# 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](#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:**
|
||||
|
||||
```ts
|
||||
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:
|
||||
|
||||
```sql
|
||||
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.
|
||||
+2
-2
@@ -2,8 +2,8 @@
|
||||
|
||||
- **Coding patterns**: MUST use the `svelte-frontend` skill when writing Svelte code
|
||||
- **Validation**: `docs/validation.md` — `npm run check:fast` (2s) for iteration, `npm run check` (50s) for final PR
|
||||
- **UI components**: use Windmill's design-system components (Button, TextInput, Select) — never raw HTML elements
|
||||
- **Brand/design**: `frontend/brand-guidelines.md`
|
||||
- **UI components**: use Windmill's design-system components — never raw HTML elements. Start from the barrel `src/lib/components/common/index.ts` and grep `src/lib/components/`; the component you need almost certainly exists
|
||||
- **Brand/design**: `frontend/brand-guidelines.md` — read the relevant section before building UI, not after; the `svelte-frontend` skill maps which section covers what
|
||||
- **Backend API**: routes in `../backend/windmill-api/openapi.yaml`, generated types in `src/lib/gen/`
|
||||
- **Regenerate client**: `npm run generate-backend-client` after backend API changes
|
||||
|
||||
|
||||
Reference in New Issue
Block a user