From 58e9b503323363e61e18fff77abe40df13cf1954 Mon Sep 17 00:00:00 2001 From: Stephanie Hobson Date: Mon, 14 Sep 2026 13:54:18 -0700 Subject: [PATCH 1/4] gitignore VSCode --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 7f8d6791..68e67e69 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,4 @@ # Miscellaneous .DS_Store +.vscode \ No newline at end of file From 2f18611a255d32c37a698dc6a13d3fb142392e8e Mon Sep 17 00:00:00 2001 From: Stephanie Hobson Date: Mon, 14 Sep 2026 13:55:16 -0700 Subject: [PATCH 2/4] Set up Agents simmilar to other MozMEAO projects --- AGENTS.md | 242 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ CLAUDE.md | 1 + 2 files changed, 243 insertions(+) create mode 100644 AGENTS.md create mode 100644 CLAUDE.md diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 00000000..a6671530 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,242 @@ +# Repository Guidelines + +## Project Structure & Module Organization + +`ensemble` (v1.2.1, `private: true`, MPL-2.0, `git@github.com:mozilla/ensemble.git`) is the React app behind — the **Firefox Public Data Report**. It is a **pure client-side create-react-app SPA and it contains no data of its own.** Every metric is fetched at runtime from **[ensemble-transposer](https://github.com/mozilla/ensemble-transposer)** (separate repo), which reformats Mozilla's public telemetry and serves JSON from the same `data.firefox.com` domain. If a number on the site looks wrong, the bug is usually not here. + +**`src/config.json` is the spine.** Two arrays, and almost every structural change starts by editing one of them: + +| Array | Shape | Drives | +|---|---|---| +| `dashboards` | `key`, `menuTitle`, `source`, `supportsRegions` | The route table in `src/components/views/Main.jsx` (`/dashboard/`), the nav in `src/components/views/Header.jsx`, and the dashboard's data `source` | +| `nextButtons` | `from`, `to`, `text` | The "Proceed to …" CTA via `src/components/decorators/withNextButton.jsx` | + +**Adding a dashboard is adding one entry to `dashboards`.** The `source` values are **absolute production URLs baked into the repo** — there is no env var for the data source, so you cannot point the app at a local transposer without editing `config.json`. + +**Data flow** (no state library, no hooks, no Redux — `this.state` in class containers, plus a `sessionStorage` key `preferredRegion` set in `DashboardContainer.jsx`): + +| Container | Fetches | Notes | +|---|---|---| +| `containers/DashboardContainer.jsx` | `${source}/index.json` | title, description, metaDescription, sections, dates, metrics, summaryMetrics, `categories` (= regions), defaultCategory | +| `containers/MetricOverviewContainer.jsx` | `${dashboardSource}/${activeRegion}/${slug}/index.json` | title, description, `type: 'line'|'table'`, axes, columns, data, annotations | +| `containers/SummaryMetricContainer.jsx` | same per-metric endpoint | buckets populations under 5% into "Other" | + +`react-refetch@3.0.1`'s `connect()` HOC **is the entire data layer** and is used in exactly those three files. Each one handles `dataFetch.pending` / `.rejected` / `.fulfilled` itself and renders `views/Spinner.jsx` or `views/Error.jsx`. All reshaping is client-side in each container's `formatData` (`ChartContainer` sorts populations by max y and drops nulls; `DataTableContainer` sorts rows by value descending). + +Metric `description` strings are rendered as inline Markdown through `markdown-it('zero')` with only `link`/`entity`/`emphasis`/`sup` enabled, via `dangerouslySetInnerHTML`. `src/tests/jest/MetricOverview.test.jsx` exists **specifically to guard that sanitization** — never widen the enabled-rules list without extending that test. + +``` +src/ +├── index.jsx entry: ReactDOM.render + BrowserRouter +├── config.json the route/dashboard table + next-button flow +├── registerServiceWorker.js CRA helper; only unregister() is called (SW deliberately off) +├── setupTests.js enzyme adapter; puts React and shallow on global +├── components/ +│ ├── containers/ 6 stateful/data-fetching .jsx +│ ├── decorators/ withTracker.jsx (react-ga), withNextButton.jsx +│ └── views/ 19 presentational .jsx +│ ├── styl/ Stylus SOURCE (hand-edited); includes/lib.styl +│ ├── css/ GENERATED, GITIGNORED — holds only a README stub +│ ├── fonts/FiraSans/ 8 weights/styles of webfonts +│ └── img/ 3 assets +├── lib/ +│ ├── lazyLoad.jsx react-loadable wrapper (spinner + error fallback) +│ └── utils.js bumpSort, isFloat, prettifyNumber, getPageTitle +└── tests/ + ├── jest/ 2 unit tests + └── nightwatch/ 13 e2e specs (.js, CommonJS), incl. dashboards/ +``` + +There is no `pages/`, `store/`, `hooks/`, `api/`, `locales/`, or `data/`. `public/` is CRA's checked-in static template (`index.html` with `%PUBLIC_URL%` placeholders and a hardcoded `og:image`, `manifest.json`, `contribute.json`, a Search Console verification file, favicons). `build/` is gitignored output. Routes are exactly `/`, `/contact`, `/dashboard/`, and a catch-all `NotFound`. + +## Current state: read this before estimating anything + +MozMEAO is taking this repo over. 810 commits — 515 in 2018, 92 in 2020, **nothing in 2021–2024**, three in 2025. Last commit `f655559`, 2025-05-07. Assume nothing has been exercised recently. + +- **No CI/CD of any kind exists.** No `.github/`, `.circleci/`, `Dockerfile`, `docker-compose`, `Jenkinsfile`. This is not a quirk of your clone — it was all deliberately removed: CircleCI disabled 2018-02-22 (`433a465`), Docker/Dockerflow removed 2018-08-23 (`d3bb561`), `.github/dependabot.yml` deleted 2020-07-29 (`7c7006f`, "Disable non-security updates from Dependabot"). Issue #79 ("enable CI") is still open. +- **The deploy is a static build on Google Cloud Storage** (confirmed by `x-goog-*` headers on the live site). `https://data.firefox.com/version.json` reports `1.2.1` / commit `f655559`, so **the deployed site matches current `main`**. The mechanism that pushes `build/` to GCS is **not in this repo and is currently unknown** — finding and documenting it is takeover work, not something to guess at. +- **`npm install` fails outright on an Apple Silicon (arm64) Mac.** Confirmed: `chromedriver@84.0.1`'s postinstall exits with `Only Mac 64 bits supported` and takes the whole install down with it. Use `npm install --ignore-scripts` to get a working `node_modules` (you lose the chromedriver binary, which you can't use anyway — see the Nightwatch bullet below). This is a harder failure than "chromedriver's download URL changed"; it doesn't even try to download on this architecture. +- **Confirmed on Node v24.19.0:** `npm run build:app` (and therefore `npm run build`) fails with `ERR_OSSL_EVP_UNSUPPORTED` — webpack 4's chunk hashing uses MD4, which OpenSSL 3 (Node ≥17) rejects — and succeeds once prefixed with `NODE_OPTIONS=--openssl-legacy-provider`. `npm start` compiles through the same webpack pipeline, so expect to need the same prefix. **`npm run lint` and `npm run test:jest` both pass clean with no flag at all** — Jest never invokes webpack, so it never hits the MD4 path. The repo's only Node signal is `engines: node >=8`; there is **no `.nvmrc`**, and historical CI pinned Node 8. +- **`npm install` on npm ≥7 rewrites `package-lock.json` from lockfileVersion 1 to 3.** Confirmed: a ~22,000-line diff. Revert it after installing; don't let it ride along in an unrelated commit. +- **Nightwatch cannot run at all right now.** Its `chromedriver@84.0.1` dependency doesn't install (see above), so there is no chromedriver binary in `node_modules/.bin` to point Nightwatch at. Even setting that aside, Chrome 84 is mid-2020 and chromedriver must major-version-match the installed Chrome. Treat the whole e2e suite as blocked until this is deliberately fixed. +- **A fresh clone will not render styles until you compile Stylus.** `src/components/views/css/*` is gitignored and every component does a side-effect `import './css/Foo.css';` — run `npm run build:css` (confirmed working, no flags needed) or the app fails to resolve those imports. +- **Headline known bug: issue #409** — the hardware dashboard takes >10 s to re-render on resize/zoom and can trigger Firefox's slow-script dialog. Most user-visible defect; lives in the d3/metrics-graphics redraw path (`views/SummaryMetric.jsx`, `containers/ChartContainer.jsx`, `views/Chart.jsx`). +- ~40 open issues, oldest from 2018. Other notables: #313 (`MetricOverviewContainer` does not reject when metric data 404s — cited in an inline comment there), #395 (replace `request`), #333 (replace `@babel/polyfill`), #245 (React code-splitting), #46 (localize content). The remote also carries ~35 stale `dependabot/npm_and_yarn/*` branches and three `%archive-*` branches. +- Upstream data is **live and current** — transposer `dates` arrays are sorted newest-first and currently run to 2026-08-31. The app is unmaintained; the data is not. + +## Build, Test, and Development Commands + +```bash +npm install --ignore-scripts # plain `npm install` fails on Apple Silicon; see Current State + # expect package-lock.json to be rewritten v1 -> v3; revert it after + +NODE_OPTIONS=--openssl-legacy-provider npm start + # npm-run-all --parallel watch:css watch:app -> CRA dev server :3000 + # .env sets BROWSER=firefox, so this opens Firefox +npm run build:css # one-shot Stylus compile; confirmed working, no flags needed + +npm run lint # lint:js-extra (standalone ESLint) + lint:styl (stylint); confirmed clean, no flags +npm run test:jest # CI=true react-scripts test (Jest + enzyme, 2 files); confirmed passing, no flags +npm test # lint -> test:jest -> test:nightwatch:dev; the Nightwatch leg cannot run today + +NODE_OPTIONS=--openssl-legacy-provider npm run build:app # confirmed required on Node >=17; fails with ERR_OSSL_EVP_UNSUPPORTED otherwise +npm run build:version.json # writes build/version.json; must run after build:app, needs git on PATH +npm run size # source-map-explorer on build/static/js/main.* (needs a prior build) +``` + +Run one Jest file by passing a path through: `CI=true npx react-scripts test src/tests/jest/Dashboard.test.jsx` (confirmed working). + +**Environment:** `.env` **is checked into git** and holds only public build-time config — `BROWSER=firefox`, `NODE_ENV=development`, `REACT_APP_GA_TRACKING_ID='UA-00000000-0'` (placeholder), `REACT_APP_SITE_TITLE='Firefox Public Data Report'`, `REACT_APP_VALUE_DECIMAL_PLACES=3`. Consumed by `decorators/withTracker.jsx` and `lib/utils.js`. Override inline: `REACT_APP_SITE_TITLE='…' npm start`. + +## Coding Style & Naming Conventions + +- **`.jsx` for any file containing JSX** — including `src/index.jsx` and `*.test.jsx`. Plain `.js` only for non-JSX modules (`lib/utils.js`, `setupTests.js`, `registerServiceWorker.js`, all Nightwatch specs). +- **PascalCase filenames matching the default-exported component.** Decorators are camelCase with a `with` prefix; `lib/lazyLoad.jsx` is camelCase. +- **One component per file, always `export default`.** There are no named component exports anywhere in the repo; the only named exports at all are the four helpers in `lib/utils.js`. +- **The container/view split is the organizing principle.** `containers/X.jsx` holds state and fetching and renders `views/X.jsx` with formatted props (`ChartContainer`→`Chart`, `DataTableContainer`→`DataTable`). Keep new state out of `views/`. +- Function components are the default (17 of 19 views), written as anonymous default-exported arrows — `export default props => (...)` — with no `displayName` (hence `react/display-name: off`). Use a class only where state, refs, or lifecycle are genuinely needed: all 6 containers plus `SummaryMetric.jsx` (d3 + `React.createRef`), `Spinner.jsx`, `MetricOverview.jsx`, `Footer.jsx`. +- Leading underscore for private class-property handlers (`_onRegionChange`, `_drawChart`, `_setChartWidth`) — applied inconsistently (`setChartSize` in `ChartContainer.jsx`); prefer the underscore in new code. +- Locals named `maybeX` hold a JSX fragment **or `null`** (`maybeDescription`, `maybeSummaryMetrics`, `maybeRegion`, `maybeGraphURL`). Keep the idiom. +- Styles attach via a global CSS side-effect import of the **generated** file at the top of the component: `import './css/Dashboard.css';`. Some components import several (`Chart.jsx` pulls `metrics-graphics/dist/metricsgraphics.css`, `./css/Chart.css`, `./css/Metric.css`). `Metric.css` and `LabelledSelector.css` are shared partials with no component of their own. +- No TypeScript, no PropTypes, no CSS-in-JS or CSS Modules, no Prettier, no i18n. **No MPL license headers on source files** (unlike springfield/bedrock) — do not add them. +- `.editorconfig`: LF, final newline, trim trailing whitespace, 4-space indent for `py,yml,html,css,styl,js,json,md`. `.jsx` is missing from that list but 4-space is the de facto convention. No max line length is configured anywhere. +- Every dependency is **exact-pinned** — no `^` or `~` in `package.json`. Keep it that way. + +**ESLint — there are two of them, and neither sees everything.** `.eslintrc.extra.js` is a standalone config run only by `npm run lint:js-extra`, and that script is `npx eslint --config .eslintrc.extra.js --ext .js --ext .json .` — **`.jsx` is not in the `--ext` list, so the bulk of the application code is never linted by it.** Separately, `react-scripts` 3.4.1 lints during `start`/`build` using its own bundled `eslint-config-react-app`, which appears nowhere in this repo and has no `eslintConfig` key in `package.json`. **Consequence: a change can pass `npm run lint` and still fail `npm run build`, and vice versa.** Run both. + +`.eslintrc.extra.js` errors: `eqeqeq`, `no-var`, `prefer-const`, `no-console`, `no-global-assign`, `no-redeclare` and `no-shadow` — the latter two with `builtinGlobals: true`, so **do not name a variable `name`, `status`, `history`, `event`, etc.** Warnings: `semi: always`, `prefer-arrow-callback`, `comma-dangle` (trailing commas **required** for multiline arrays/objects/imports/exports, **never** for function args/params). Off: `react/prop-types`, `react/display-name`, `jsx-a11y/no-onchange`. `react/no-unescaped-entities` forbids only `>` and `}`. `jsx-a11y/recommended` is on, so a11y violations are errors. **Quote style is not enforced** (there is no `quotes` rule) — single quotes by convention. `.eslintignore` covers `build` and `package-lock.json`. + +## Styling: Stylus, outside webpack + +`src/components/views/styl/*.styl` → `src/components/views/css/*.css` via the `stylus` CLI (`build:css` / `watch:css`). Webpack never sees the Stylus. One `.styl` per component (PascalCase, mirroring the component), plus `Application.styl` (global reset, Fira Sans `@font-face`, body type, ~217 lines), `BrowserHacks.styl`, and shared partials `Metric.styl` and `LabelledSelector.styl`. + +Shared variables live in `src/components/views/styl/includes/lib.styl` and it is nearly empty — `$base-tablet`, `$base-desktop`, `$link-color-normal = #0070ff`, and a `// TODO: padding/margin spacing.`. Import it per-file with `@import 'includes/lib'`; it is not globally injected. **No mixins file; most colors are hardcoded hex inline** (stylint's `colors` check is off). + +**Not BEM.** ID selectors carry the page landmarks — `#application`, `#main-header`, `#main-navigation`, `#dashboard`, `#dashboard-sections`, `#summary-metrics`, `#region`, `#introduction` — with flat lowercase-dash classes for repeated pieces (`.metric-overview`, `.dashboard-section`, `.data-table-wrapper`, `.labelled-selector`, `.next-button`, `.striped`, `.highlighted`, `.bar-label`). Descendant nesting with `&` for states; responsive via `@media $base-tablet` / `@media $base-desktop` blocks at the bottom of each file. **Those element IDs are also the Nightwatch selectors — renaming one breaks the e2e tests.** + +stylint (`.stylintrc`) runs with `maxErrors: 0` and `maxWarnings: 0`, so any finding fails the lint. It enforces the **CSS-like** Stylus dialect, not the terse indented syntax: 4-space indent, single quotes, `brackets: always`, `colons: always`, `semicolons: always`. Also `noImportant: true`, `leadingZero: false` (`.5`, not `0.5`), `zeroUnits: never` (`0`, not `0px`), `universal: never` (no `*`), `prefixVarsWithDollar: always`, `namingConvention: lowercase-dash` with `namingConventionStrict: true`, `zIndexNormalize: 10`. Escape hatches are `// @stylint off` / `on` / `ignore` comments — see the `@font-face` block in `Application.styl`. + +## Writing code for the next reader + +Code is read far more often than it is written, and in this repo the next reader is arriving after a five-year gap. Optimise for them. + +- Use full, descriptive names. No single letters, no invented abbreviations. `activeRegion`, not `r`. +- Comments explain **what and why**, briefly, and only where the logic is non-obvious. Well-named code needs none. Delete comments that restate the code. +- **Comments must stand on their own.** Never reference a spec, a plan document, a requirement label, or a ticket ID *as* the explanation — the comment must make sense to someone who has only the file in front of them. A cross-reference alongside a self-contained explanation is fine; that is how the `metrics-graphics` workaround notes in `views/Chart.jsx` read. +- Never describe code that no longer exists. After a refactor, delete the comments it falsified — this repo already has several. Prefer deleting an obsolete comment, branch, or test over leaving it beside its replacement. + +## Testing Guidelines + +**Jest + enzyme**, through CRA's built-in Jest, with no `jest` config block. Only two files, both in `src/tests/jest/`, named `PascalCase.test.jsx` — **not colocated, no `__tests__` directories.** `src/setupTests.js` puts `React` and `shallow` on `global`, so **test files import neither**: they `import Dashboard from '../../components/views/Dashboard';` and call bare `shallow()`. Existing style is top-level `it(...)` with long descriptive sentences, a `beforeAll` that builds a `requiredProps` object, and assertions via `.find(selector).exists()` and `.html()).toContain(...)`. Shallow rendering only; no snapshots; `react-refetch` is never mocked; there are no container tests. + +**Nightwatch** specs in `src/tests/nightwatch/` are **lowercase `.js`, CommonJS**: `module.exports = { before: browser => …, 'Test name as a sentence': browser => … }`. `nightwatch.conf.js` reads `NIGHTWATCH_TARGET` (`dev` = localhost:3000, `stage` = data-ensemble.stage.mozaws.net, `prod` = data.firefox.com) and **throws if it is unset** — it destructures `undefined`. The `default` env is headless Chrome and excludes `utils.js` and `jsDisabled.js`; the `jsDisabled` env runs only `jsDisabled.js` and must be **non-headless** (JS cannot be disabled in headless Chrome), asserting on the `id="enable-javascript"` `