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.
95%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
Low
Low-risk findings worth noting
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
You know how to investigate. These are the preferences that make an investigation here land well:
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.
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:
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.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.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.
Before your first probe, read 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.
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 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.
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:
| # | 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.
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.
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:
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.
# <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.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.#hnt-dev-be-alertsUpdates 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.
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:
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/:
gh repo fork --clone if they cannot
push to the repo.newtab/backend-investigation: <summary>. In the body, give each fix with its evidence.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.
mozilla/firefox-aidev-plugins
0edd81f
If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.