@brad-frost-web/eddie-reporter
The Eddie-reporter — a zero-dependency usage reporter you install on downstream Eddie products. It statically scans a product for Eddie asset usage (components, recipes, page templates, and design tokens) and reports that activity back to Eddie's Brain, so adoption and activity of the design system become a living, breathing feedback loop instead of a point-in-time guess.
Why this exists
Eddie's Brain already knows coverage (what the system offers) and, via the
adoption scanner, coarse adoption (which org repos declare an Eddie
dependency in package.json). What it couldn't see was activity: which
assets a product actually uses, how heavily, and when it last changed.
The reporter closes that gap. It turns "16 repos depend on Eddie" into "these
products use ed-card 240 times, nobody has touched ed-r-legacy-banner in 90
days, and ed-p-app-dashboard is the most-forked template this quarter." That
signal flows into .eddie-brain/activity.json and out through the
eddie_get_activity MCP tool.
Design principles
- Zero dependencies. Node built-ins only. It installs instantly and adds no supply-chain surface to the products it ships to.
- Static & read-only. It reads source text and counts references. It never executes the product, never reads runtime/user data, and never mutates the host. Reports are deterministic and privacy-friendly.
- Never breaks a build. A failed beacon is logged, not thrown. The CLI exits
0even when delivery fails. - Degrades gracefully. If there's no network (or you don't want telemetry), it writes a local artifact your CI can commit — the brain's org scan or a human can pick it up later.
Install & use
Run it with no install via npx:
npx @brad-frost-web/eddie-reporter --event build \
--endpoint https://ds.bradfrost.com/report
Or add it as a dev dependency and wire it into a build/postbuild script:
// package.json
{
"scripts": {
"postbuild": "eddie-report --event build"
}
}
Set the endpoint once via env so scripts stay clean:
export EDDIE_REPORT_ENDPOINT="https://ds.bradfrost.com/report"
export EDDIE_REPORT_TOKEN="…" # optional
Run it on a schedule
The loop is designed to run unattended, on both ends:
- Product side. Copy
templates/eddie-report.ymlto your product's.github/workflows/and set theEDDIE_REPORT_ENDPOINTsecret. It beacons weekly (cron), on every push tomain/develop, and on demand. Products that deploy elsewhere can use a Netlify/Vercel scheduled function, apostbuild/postdeployhook, or a plain server cron. - System side. The hosted
POST /reportendpoint durably captures each beacon in a Netlify Blobs store (serverless filesystems are ephemeral). The schedulededdie-activityworkflow in eddie-design-system then flushes that store into.eddie-brain/activity.json(eddie-brain activity flush), refreshes adoption, and opens a PR — so the ledger stays live with zero manual steps.
CLI
eddie-report [dir] [options]
--endpoint <url> Brain ingestion URL (env: EDDIE_REPORT_ENDPOINT)
--token <token> Bearer token (env: EDDIE_REPORT_TOKEN)
--event <name> build | deploy | install | ci | dev | manual (default: manual)
--out <path> Local artifact path (default: .eddie/usage-report.json; '' to skip)
--ignore <path> Skip a file or directory, relative to dir (repeatable)
--ignore-dir <n> Skip every directory named <n>, anywhere in the tree (repeatable)
--dry-run Scan and print, but don't POST or write
--json Print the full report JSON to stdout
--quiet Suppress the human summary
Build output that lives beside the sources
The scanner already skips the usual output directories (dist, _site, .output, …). It cannot tell that a file inside a source directory is generated. A committed css/styles.css compiled from css/*.scss inlines Eddie's entire token sheet, so a site that uses two dozen tokens reports six hundred. Name such files explicitly:
eddie-report --event ci --ignore css/styles.css --ignore css/styles.tmp.css
The same option exists on the API as ignorePaths (paths relative to the scanned directory; a directory skips its subtree). Matching is case-sensitive; an absolute path inside the scanned directory is accepted, one outside it never matches. A missing or flag-shaped value (--ignore --dry-run) is a usage error and exits 2.
bfw-process rulebooks
A bfw-process product carries a rulebook that quotes Eddie markup and token names as examples of the rules. Counted as usage, it credits a recipe or token to a product that only mentions it (#1861).
.bfw-process/and.microagents/are never scanned: the scanner skips every dot-directory.AGENTS.md,CLAUDE.mdandSPEC.mdare not, because root markdown on a docs site can be real usage. Opt them out explicitly, as the workflow template does:
eddie-report --event ci --ignore AGENTS.md --ignore CLAUDE.md --ignore SPEC.md
Programmatic API
import { buildReport, sendReport } from '@brad-frost-web/eddie-reporter';
const report = buildReport({ dir: process.cwd(), event: 'build' });
await sendReport(report, { endpoint: process.env.EDDIE_REPORT_ENDPOINT });
What a report looks like
{
"schema": "eddie-usage-report/1",
"reporterVersion": "0.62.1",
"generatedAt": "2026-07-14T00:00:00.000Z",
"event": "build",
"product": {
"id": "bradfrost.com",
"repo": "Brad-Frost-Web/bradfrost.com",
"branch": "main",
"commit": "a1b2c3d…",
"environment": "ci"
},
"eddie": {
"declared": { "@brad-frost-web/eddie-web-components": "^0.37.0" },
"packages": ["design-tokens", "web-components"]
},
"usage": {
"components": { "ed-card": 24, "ed-button": 18 },
"recipes": { "ed-r-site-header": 1 },
"pages": { "ed-p-app-dashboard": 1 },
"tokens": { "--ed-theme-color-content-default": 63 },
"unknownPrefixed": { "ed-slide": 12 },
"filesScanned": 210,
"filesMatched": 47
},
"totals": { "distinctComponents": 2, "distinctRecipes": 1, "distinctPages": 1, "distinctTokens": 1, "occurrences": 107, "distinctUnknownPrefixed": 1 }
}
A product's own ed-* elements are not Eddie usage
The ed- prefix means "in Eddie's catalog". A product that defines its own
custom elements under it — Eddie Slides shipped fourteen, ed-slide,
ed-robot, ed-r-site-frame among them — used to have them counted as Eddie
components, and the Brain could not tell them from the real thing
(#2192).
The reporter now ships a snapshot of the catalog (dist/known-tags.json,
generated at build time from the Brain's graph for the Eddie release it was
published with). An ed-* / ed-r-* / ed-p-* tag outside it goes to
usage.unknownPrefixed instead of a usage bucket, counts as zero Eddie usage,
and is left out of totals.occurrences. The Brain surfaces the bucket on the
product's roll-up and in eddie_get_activity's unrecognised stats, and
eddie_validate_file warns on the same tags in source.
Two readings of an entry, and the fix for each: it is the product's element,
so give it the product's own prefix (es- for Eddie Slides) — or it is
Eddie's and newer than the installed reporter, so update the reporter. A
catalog of your own can be passed as knownTags to scanUsage /
buildReport; a reporter running from source without a built snapshot omits
the field, and the Brain partitions that report by its own catalog instead.
The bigger picture
This reporter is the first leg of a two-way system↔product relationship. See
docs/VISION.md for the full map of where this goes —
usage-weighted health, deprecation radar, drift nudges, a public adoption
dashboard, and closing the loop back into governance.