Skip to main content

backend-investigation

Diagnoses backend problems behind Firefox Home New Tab (HNT), across Merino, the curated corpus, admin-api, the article crawler, Picture of the Day and the daily crossword. Use when a Sentry alert fires on one of those services, an editor reports something broken, or a New Tab feature looks empty, wrong, or stale. Backend only, not in-tree browser/extensions/newtab frontend work.

Source facts

Repository
mozilla/firefox-aidev-plugins
Last source activity
October 20, 2026 at 18:30
Detected SKILL.md language
English
Stars
12
Forks
17

Install options

The review-first prompt is selected by default. You can switch to a direct command or download a local copy.

Review the source files

Read SKILL.md and any companion files shown by SkillsMP before deciding whether to install.

File Explorer
5 files

Showing SKILL.md

SKILL.md
Source instructions · Read-only preview
name
backend-investigation
description
Diagnoses backend problems behind Firefox Home New Tab (HNT), across Merino, the curated corpus, admin-api, the article crawler, Picture of the Day and the daily crossword. Use when a Sentry alert fires on one of those services, an editor reports something broken, or a New Tab feature looks empty, wrong, or stale. Backend only, not in-tree browser/extensions/newtab frontend work.
# New Tab (HNT) Backend Investigation Diagnose a problem in the backend services behind Firefox Home New Tab. The page is assembled from features that are served separately and fail separately — content recommendations (Merino, the curated corpus, admin-api, the article crawler, the ML section pipeline, the data pipelines), Picture of the Day, the daily crossword — so establish which one the symptom belongs to before probing. A New Tab feature with no notes in this skill is still yours to diagnose: work it the same way, from the Merino provider and config that serve it. Not for in-tree `browser/extensions/newtab` frontend bugs, and not for Merino's non-New-Tab consumers such as Firefox Suggest. What you owe first is a **diagnosis**: a root cause, the evidence for it, and the impact quantified. Mitigating or fixing the problem usually follows straight from that, so carry the diagnosis far enough to name the change that would resolve it and what would show the recovery. Two groups feel these failures, and every finding should say which one it reached: **Firefox New Tab clients**, meaning everyone the affected feature serves, and, where the feature has them, the **editors and curators** working through curation-admin-tools and admin-api. Access and traps, per system: [references/data-sources.md](references/data-sources.md) ## How we like this done You know how to investigate. These are the preferences that make an investigation here land well: - **Confirm the symptom yourself before explaining it**, whatever the source reported. - **Let the data generate the hypotheses**, one round at a time, rather than enumerating everything up front. Where several are live at once, probe them in parallel instead of serially. - **Measure rather than reason.** Reproduce the request, count the rows, read the code path. The characteristic failure here is a correct query paired with a wrong inference, stated confidently. ## Don't stall on the developer Every pause costs wall-clock, because the developer may not be watching. Default to proceeding: pick the sensible option, say which one you picked, and keep going. Ask a blocking question only when proceeding on any assumption would waste the whole investigation. Raise access needs up front (Step 1), and when a source you cannot reach turns promising later, raise it **once** as a line in `USER-ACTIONS.md` plus one `PushNotification` (see "How to ask"), carrying the whole errand in its own text, e.g. `- [ ] Connect to the AWS Client VPN so corpus MySQL is reachable — settles whether the DE items exist at all`. Give the exact command, not a description of the problem; the "Access requests" section of `references/data-sources.md` has the command for each gated source and how to word the ask. If the answer lives in a dashboard or console you have no credential for, do not ask for access at all — name the view, the filter, and the single number or shape you need, and ask them to read it back. **Then keep investigating the other hypotheses immediately. Do not wait.** Restate it exactly once more — whichever comes first: the unblocked lines run out, or you are three probes into a line you had already judged weaker than the blocked one. That is the second and last ask, and it does not apply if they have already answered. If there is still no response, write up and close out with `Status: blocked`, name the one unblock under "Could not measure", and keep it in the `USER-ACTIONS.md` as an open item. Stopping outright is a last resort: only when the blocked source is the only thing that can settle the question **and** it cannot be reduced to a question they can answer for you — a dashboard, console, or explore is almost never a real stop. ## Workspace Each investigation gets its own subdirectory inside one root directory that the developer keeps their investigations in. Do not assume where that root is. Establish it in this order: 1. **Check the instructions you were already given.** A root recorded on an earlier run is already in front of you, in your loaded instructions. If they name one, use it and do not ask. 2. **Otherwise ask, once, before your first write.** Where the developer keeps investigations cannot be derived, and guessing puts files somewhere they did not choose. Ask it as a single question at the start, and start pinning down the report while you wait rather than idling. 3. **Then remember it**, so the question is asked once per machine rather than once per investigation. Remember the root and the one-subdirectory-per-investigation convention, and say that you saved it. It has to persist for **every** repository, not only the one you happen to be in: investigations get started from whichever service repo is to hand. Then create this investigation's subdirectory inside that root. If the root already holds investigations, follow the naming they use; otherwise `<mon><DD>-<slug>`, e.g. `jul29-empty-de-sections`. Say which directory you created. Reuse an existing one only when its FINDINGS.md is about the same symptom. - `FINDINGS.md` — one living document, from the skeleton in the write-up step. Create it once the directory exists and in any case before you write a conclusion down; the steps that follow write into it as they go, so the write-up step is a final pass over a document that already exists. Rewrite it in place, not as `FINDINGS-v2.md`. - `USER-ACTIONS.md` — the open asks for the developer, one `- [ ]` line each; see "How to ask" in `references/data-sources.md`. Create it with the first ask, not up front. - `HYPOTHESES.md` — the working list, live and dead, each with what would kill it. Hypotheses go here rather than in FINDINGS.md, which carries the conclusion they led to. - Add subdirectories only when you have something to put in them, not up front: `queries/` and `results/` for each query and its output under a matching basename, `api_responses/` for raw JSON from live calls. Save responses even when they look boring; the same request may not return the same thing tomorrow. - Aggregate before saving. This directory sits outside any repo and gets linked into tickets, so prefer a distribution over a dump of rows carrying editor identities or user data. - Rewrite only the FINDINGS.md you wrote and delete only files this session created — anything else in the directory belongs to another investigation. - Python work goes in an isolated venv inside this directory. ## Keep a timeline throughout Maintain a `timestamp (UTC) | observation | source` table as you go rather than assembling one at the end. Where the onset is cheap to establish, get it early: it constrains every other line of enquiry. Where it is not, leave it open and say so. An assumed onset is worse than an unknown one, because everything downstream quietly inherits it. Normalise and label every timestamp, since a subtraction error can manufacture an outage that never happened, and line the window up against the deploys and config changes you turned up. **The scheduling layer is not in UTC.** The assembly crons run in each surface's own timezone, so a UTC comparison invents a one-day gap for part of every day on any surface offset from UTC — worst for the Americas, and `en-US` is the largest surface. Convert per surface, and say which timezone you used. ## Step 1 — pin down the report Before your first probe, read [references/data-sources.md](references/data-sources.md). It carries the traps that silently return wrong answers, and several of them look like an outage. **Check access in your first few minutes, not when a probe fails.** As soon as you know the feature and the symptom, decide which planes the investigation will probably need: BigQuery for counts or history, Sentry for errors, corpus MySQL for editorial state, AWS for the lambdas. Test each one with a single cheap command: `gcloud auth list`, a `bq query --dry_run`, whether any `mcp__sentry__` tools exist, `aws sts get-caller-identity`. Raise every missing one together, in one batch, before you start probing. Don't hold an ask back because there is still something you can do without it. The developer takes minutes to act, and those minutes should overlap your work, not follow it. Asking for access you end up not needing costs one line. Finding the gap an hour in costs the hour. Further asks can follow as new lines open up. Establish these facts from the data. | Fact | Why it matters | |---|---| | Exact symptom, verbatim, and which feature | "Empty section" and "wrong items in section" have disjoint causes | | For a reported metric: its exact definition and source (table, dashboard, filters) | Reproduce the reporter's number before explaining it; a near-miss definition can trend differently | | Who noticed, and how | Alert / editor report / spotted by hand — sets what evidence exists | | First and last seen, **with timezone** | Anchors the timeline; reports usually arrive late | | The stratum — surface, locale, section, client version, or for a daily artifact the date | The stratum is very often the diagnosis | | Still happening right now? | Live reproduction vs. historical forensics | If it is still happening, capture the perishable evidence first — a live request for the feature, current logs, whatever counts or artifact it publishes — and establish what normal is while those probes are in flight. History keeps; live signal does not. **An error rate is not yet a symptom.** Measure the erroring stage's *output* to fix the blast radius: items per section and per-surface freshness for assembly, the newest object under a dated prefix for a publish job, a live Merino request for anything client-facing. An error floor that never reaches the output is a not-incident; errors flat with output at zero is worse than the alert says. **Failing to reproduce is a result, not a blocker.** Record the exact attempt, then switch the question to why the reporter saw it and you do not: a stratum you did not hit, a closed window, a cache, a client version, or a monitor measuring something other than what it claims. `not-incident` needs an answer to that second question; unconfirmed is not refuted. A report relayed from an editor is ambiguous between what editors see in curation-admin-tools and what clients see on the surface. Probe both and state which group you confirmed. Once client-visible impact is confirmed and still happening, say so in one line — what, how big, since when — and keep going. ## Step 2 — establish what "normal" is You have no idea what is currently normal here, so you have no basis on which to reject your own inference. Derive the prior rather than asking for it: - **What the serving path returns now** — a live request for the feature; for recommendations, the surfaces present and not disabled in the section data. - **What the producer last wrote** — per-surface, per-source crawl volume over the last day for recommendations, the newest object under the dated bucket prefix for a publish job. Producer and serving sets differ, and a crawled surface with no sections is not automatically a bug. - **What shipped recently** — commits in the service repos, releases in Sentry, timestamps on model or config artifacts. - **Whether this is normally noisy or seasonal** — a trailing profile of the same metric, by day of week. Do not ask; compute it. For a year-over-year claim, plot the baseline year across the whole window first: a step in the baseline year reads as a change in the current one. - **Whether it has happened before** — Sentry for the same signature, and the service repos' GitHub issues. Where one is unreachable, record it as unchecked rather than empty. What is left is genuinely in the developer's head: anything in flight that a repo or dashboard would not show, and whether a postmortem or incident register already covers this area. Raise that as one short note and carry on without waiting. Keep what you derived and what they told you apart in FINDINGS.md. When a later query contradicts the prior, treat your own inference as the suspect first, and say so. ## Step 3 — form hypotheses from the data, then probe them in parallel Start where pinning down the report left you: the stage whose output is wrong, the affected stratum, the moment it changed. Ask what could produce exactly that, follow the data one hop upstream, and let each result raise the next question. Keep them in `HYPOTHESES.md`, a row each, and write the result that would kill one before you go looking: ```markdown | # | Hypothesis | What would kill it | Probe | Result | Status | ``` Status is live, killed, parked, or confirmed. Killed rows stay; they are what stops the next person re-running them. Work the live ones in parallel: issue the probes as parallel calls in a single batch, handing any line that needs several dependent steps to a subagent. Keep the first wave small and quick, and hold anything slow or wide for the second. Name each query file after its hypothesis so a result cannot be attributed to the wrong line. Then look at what came back, update the rows, and do it again. Being down to one surviving hypothesis is a prompt, not an answer: ask what else could produce what you measured before committing to it. And when a source stops yielding, measure the same thing in another plane — the failure is often invisible in the one you started in. ## Step 4 — stratify before concluding anything An aggregate that looks fine is the normal way these problems hide. Slice by the dimensions the feature actually has — for recommendations **domain, locale, region, surface, section, experiment branch, and client/addon version**, for a once-a-day global artifact barely more than the date — and look for a single stratum at zero or down sharply against its own trailing median. When the report says "some users" with no stratum attached, try experiment branch, region and rollout state first: some markets receive stories only through Nimbus enrolment, which a locale slice cannot see. "Country" is several different fields across the telemetry tables (geo-IP, `Region.home`, a clamped allowlist; see the telemetry traps in data-sources.md), so name which one a region slice uses. **Liveness is not health.** "The job ran successfully" is compatible with total data loss in every one of these pipelines. Count what came out and compare it against what went in. If the measurements exonerate the backend, stop at the service boundary: name the owning team and the contract being broken, and hand it over. A second, unrelated anomaly you trip over on the way is a one-line note plus a task, not a second investigation. ## Step 5 — try to break your own conclusion Before writing anything down as fact, check the boring explanations: a filter in your own query, a silent truncation, an unrepresentative code path, a column that does not mean what its name says, a partially-launched feature. Then verify the outcome and not just the mechanism: a guard that provably runs is not evidence that its effect is correct. Check every comparison group for selection before it counts as evidence: - **Leftover groups.** Users left on an older client or an unexpected surface differ in whatever left them there. - **Matched sets.** "Same items on both surfaces" is conditioned on both rankers' choices, and its composition can shift under you. - **Moving controls.** A control stratum can itself have changed in the window. Report each stratum's absolute level alongside any ratio between strata. **Corroborate as widely as is reasonable.** Every source that can speak to the question is worth reading, not merely a second one: one source agreeing with you is a hypothesis, and each further source that could have failed differently and did not makes the claim harder to overturn. Do this even when the first answer looked clear, because that is where a wrong inference survives. Stop when what is left would only re-read the same plane, and record in FINDINGS.md which sources each claim rests on, so one standing on a single source is visible as such. **Contradictory results are the most valuable thing you can find, so do not smooth them away.** Weigh a result the same whether or not it helps the hypothesis you favour: one that cuts against a hypothesis already carrying evidence matters more, not less. Record it, say plainly what contradicts what, and make the conflict the next question rather than something to be explained past. The same holds when a measurement disagrees with the prior, or with what the developer told you is working. Re-check the measurement first, since a stratum mismatch or a wrong surface identifier is likelier than the system having changed under you. If it survives that, keep both on the record: say which one you trust and why, tell the developer in one line, and carry on. Mark every claim **verified**, **inferred**, or **refuted**, and keep the refuted ones. ## Step 6 — write FINDINGS.md ```markdown # <feature>: <symptom> — investigation **Status:** investigating | root cause identified | blocked **Incident type:** outage | degradation | data-quality defect | user-facing bug | near-miss | not-incident **Severity:** critical | high | medium | low — anchored to the magnitude below, not chosen by feel **Affected:** Firefox clients | editors | both — with a magnitude, not an adjective ## Summary Three sentences: what broke, since when, what the user-visible effect is. ## Timeline (UTC) | Time | Observation | Source | ## Prior **Derived** — what you measured, each with the query or link. **Reported** — verbatim answers from the developer, or `asked <time UTC>, no answer received`. ## Evidence Numbered findings. Each: the claim, the query or command, the result, and verified/inferred/refuted. Reference saved files by path — every number reproducible from this directory. ## Root cause The mechanism, with the code path or config that produces it. "Not established" when it is not. ## Hypotheses The surviving explanation, and the lines worth knowing were ruled out. `HYPOTHESES.md` alongside has the full list with its kill conditions. ## Impact quantified Rows, requests, users, hours, locales. A number, or an explicit "unquantified because X". ## Proposed fix The change as a diff, at file-and-line where you can get there. ## Could not measure Any telemetry plane you could not reach, and the access that would unblock it. ``` ## Step 7 — close the loop - **Keep unresolved access asks** open in `USER-ACTIONS.md` and listed in FINDINGS.md under "Could not measure", rather than dropping them, and report what you could not reach as a finding rather than an inconvenience you routed around. - **Link prior art** — older investigation, ticket, postmortem, or a related error already known. - **Leave the follow-up as a task**, owned by whoever will act: confirm the fix shipped and the metric actually recovered. State that you have done so rather than asking whether to. - Hand back a two-line verdict plus the FINDINGS.md path. Do not paste the document into chat. Follow it with the skill improvements from Step 8, one line each, and say that you will ask next whether to open a PR for them. They are for the developer only and never go into a Slack post. ### Posting to `#hnt-dev-be-alerts` Updates worth sharing go to `#hnt-dev-be-alerts`. If the investigation started from an alert or message posted there in the last day or two, find that one message and reply **in its thread**, so the diagnosis stays attached to the alert people already saw. Otherwise start a new thread. **Ask before every single message, showing the exact text and where it will go.** Approval for one post is not approval for the next, and this is a shared channel colleagues act on. If approval does not come, keep the draft in FINDINGS.md and carry on; an unposted update is not a reason to stop. ## Step 8 — improve this skill As you write the verdict, look back over the investigation for what this skill got wrong or left out, so the list can go out with the conclusions (Step 7). What counts: - **Wrong or stale information** in this skill or its references: a path, table, project, cadence or command that turned out not to be true. - **An assumption you made that the skill should have warned against**, or a trap you fell into that it does not list. - **Missing access**: a credential or tool you needed that "Access requests" does not cover, or an unblock command there that did not work. - **A source that settled the question** and that the skill does not name. Keep only what would change the next investigation, not facts about this one incident. Each candidate needs evidence from this run: the command and its output, the commit, the page. Check that the current skill text still says the wrong thing, since the plugin may have been updated since it loaded. If nothing qualifies, say so in one line with the conclusions and skip the question. Once the conclusions are handed back, the time-sensitive part is over, so this is the place for a blocking question. Ask once with `AskUserQuestion`, `multiSelect: true`, one option per fix. The label is the fix in a few words. The description gives what was wrong, the evidence and the proposed change. A question holds at most four options, so with more fixes, split them across questions by kind and drop the weakest. If they select nothing, or answer that they want none, stop. For the selected fixes, open a **draft** PR against `mozilla/firefox-aidev-plugins` that edits `plugins/newtab/skills/backend-investigation/`: - Work in a fresh clone outside the investigation directory. Use `gh repo fork --clone` if they cannot push to the repo. - Edit in place and match the surrounding voice and density. Correct a wrong line rather than adding a caveat next to it. - Title it `newtab/backend-investigation: <summary>`. In the body, give each fix with its evidence. - The repository is public. Leave out user data, editor identities, internal links and anything from the investigation directory that is not already in the skill. Hand back the PR link. Selecting the fixes is the approval to open the draft; anything beyond that, such as requesting reviewers or marking it ready, is theirs to do.
View on GitHub