Bug Report
beta · Family qa-docs · Phase develop · Sizes lean, full · ~2,150 tokens
The document that gets one defect fixed by someone who was not there: what you did, what you expected, what happened instead, and where. Written as an anomaly report rather than a diagnosis, because at the moment of writing nobody yet knows whether the cause is a code flaw, a configuration, or a misunderstanding.
The short card. Why the document is shaped this way, and the argument behind every rule here, is in
bug-report_companion.md. A fully worked instance is
bug-report_example.md.
When to use
Section titled “When to use”- Something behaved differently from what you expected, and someone other than you will have to look at it.
- The fix will outlive your working memory: a different sprint, a different team, a different person.
- Someone outside the team reported it. Their report needs a home even if the fix takes ten minutes.
- The defect affects a release decision, so its severity has to be visible to whoever holds the gate.
- The work is regulated or audited and the closed record is evidence.
When NOT to use
Section titled “When NOT to use”- You can fix it now, alone, today, and nobody outside the team saw it. Fix it. A report you write and close yourself in the same hour is overhead, not process.
- It is a request, not a defect. If nothing promised the behavior you want, that is a change request. The working test: a defect means the software does not work the way it says it will; an enhancement means it does not work the way someone wants.
- It is a live production incident. Get service back first. Incident response and defect management have different goals; the bug report comes after, or alongside, and is not the thing that restores service.
- It is really three problems. File three. One report, one defect, or it can never be closed.
- You want a postmortem. A postmortem covers an event: timeline, contributing factors, actions. A bug report covers one flaw and is usually an input to one.
Report the anomaly, not the diagnosis
Section titled “Report the anomaly, not the diagnosis”The standards do not call this a bug report. They call it an anomaly report or an incident report, because at the moment you write it nobody knows whether the cause is a code flaw, a configuration, stale data, or a misunderstanding of what the software was supposed to do.
That is not pedantry, it is practical advice about how to write the thing. Report what you observed and what you expected. If you have a theory, label it as a theory and put it last. Reports that lead with a diagnosis get argued with; reports that lead with a reproduction get fixed.
Pick a variant
Section titled “Pick a variant”Lean (four sections) is the intake form: Summary, Steps to Reproduce, Expected and Actual Behavior, Environment and Reproducibility. Use this for anything a non-tester will fill in. Every field beyond these four costs you reports from support agents, salespeople and users, and the elements you most want are already the expensive ones.
Full (eight sections) adds Evidence, Impact/Severity/Priority, Triage and Ownership, and Resolution and Regression Guard. Three of those four are filled in by other people, after filing - which is the real split. Lean is what arrives; full is what the record becomes.
Use full when the defect is tracked formally: a release gate reads its severity, ownership crosses teams, or the closed record is audit evidence.
Severity is not priority
Section titled “Severity is not priority”| Means | Usually set by | Example | |
|---|---|---|---|
| Severity | How much damage it does | QA or the reporting team | Data exposed across an entitlement boundary: high |
| Priority | How soon it must be fixed | Product manager or triage | Cosmetic error on the launch homepage: high |
The two crossing cases are the proof they are independent:
- High severity, low priority. A crash in a rarely used legacy path. Maximum damage, minimal exposure.
- Low severity, high priority. A misspelled word on the homepage during a launch. No damage, maximum visibility.
Two honest caveats. The ownership rule above is convention, not standard - the certification glossary defines both terms and says nothing about who assigns them. And there is no standard severity scale: four, five and six-level scales are all in daily use, and S1-S4 numbering means different things in different places. Pick one, define the levels in words, and put the definitions where reporters can see them.
If your tracker has only one field, which is common - Jira removed its Severity field deliberately, on the grounds that it confused business users - decide as a team whether that field means damage or urgency, write the decision down, and stop relitigating it per ticket.
Quality rubric (self-grade)
Section titled “Quality rubric (self-grade)”Score each 0, 1 or 2. Under 10 out of 16 and the report will come back with questions instead of a fix.
| # | Criterion | 0 | 1 | 2 |
|---|---|---|---|---|
| 1 | Reproducible | No steps | Steps that start mid-flow | Numbered steps from a named starting state |
| 2 | Expected stated | Missing | Implied | Stated explicitly, with where the expectation comes from |
| 3 | Actual stated | Vague (“wrong”) | Described | Precise and observable, with the value or message |
| 4 | One defect | Several bundled | Mostly one | Exactly one, closeable on its own |
| 5 | Environment | Not given | Partial | Build, environment, account and configuration |
| 6 | Reproducibility rate | Not mentioned | “Sometimes” | A count: n out of m attempts |
| 7 | Observation, not diagnosis | Leads with a cause | Mixed | Observation first, theory labeled and last |
| 8 | Tone | Blames a person | Neutral-ish | Describes system behavior only |
Named anti-patterns (the usual wrecks)
Section titled “Named anti-patterns (the usual wrecks)”- No expected behavior. The most common real defect in real reports. The reader cannot tell whether the software is wrong or you are.
- The diagnosis report. “The cache is broken” when what you saw was a stale number. If the theory is wrong, you have sent the reader down your wrong path.
- Steps that start in the middle. No account, no data, no configuration, and a “works for me” close.
- The everything-report. Three problems in one ticket. It can never be closed.
- Blaming the developer. Costs you the collaborative fix and gets you a defensive one.
- Severity as a negotiating position. Inflating severity corrupts the only signal a release gate reads, and once counts or caps are watched it becomes systematic.
- Reopening a closed bug for a regression. Open a new one and link it, or you lose the record of what the original fix actually did.
- Closing “cannot reproduce” as though that settled it. It is a large studied category with many causes; linking related reports is a documented way to make progress on it.
- Counting bugs. The moment defect counts become targets they get gamed: one bug split into five tickets, trivial bugs filed to hit quotas, relabeling to stay under a severity cap.
Pairing with a skill
Section titled “Pairing with a skill”pairs_with: [deliver-edge-cases]. There is no testing or QA skill in the pm-skills library (finding
EC-4 in the repository’s STATE.md), and the fit here is looser than for this bundle’s two siblings: an
edge-case catalog is written before the fact and a bug report after it. The honest connection is that a
defect found in the wild is evidence the failure surface was under-mapped, and it is worth feeding back into
the catalog. Everything in this template is filled by hand.
The artifacts
Section titled “The artifacts”bug-report_template-lean.md · ~2,150 tokens
---title: "{{one_line_summary}}"report_id: "{{report_id}}"reported_by: "{{reporter}}"reported_on: "{{date}}"affected_build: "{{build_or_version}}"status: "{{report_status}}"doc_type: bug-reportsize: leansource_template: bug-reportsource_template_version: 0.1.0---
<!--LEAN BUG REPORT. The intake form: what you saw, how to see it again, what you expected instead, and where.Four sections, because this is the one document in this family written by people who are not testers -support agents, salespeople, users, developers in a hurry - and every extra field is a reason to give up andsay nothing. To carry the record through triage and resolution (see bug-report_template-full.md), ADDsections; never rename or reorder the ones below, because the full variant is a strict superset of this one.
YOU ARE REPORTING AN ANOMALY, NOT DIAGNOSING A DEFECT. At the moment you write this you do not know whetherthe cause is a code flaw, a configuration, stale data, or a misunderstanding of the intended behavior. Thestandards call this document an "anomaly report" or "incident report" for exactly that reason. Report whatyou observed and what you expected; let the investigation decide what it was. Writing as though you alreadyknow is what produces the defensive reply. See bug-report_companion.md sections 1 and 2.
THE ONE THING MOST REPORTS GET WRONG: they leave out what SHOULD have happened. Across roughly 3,000 realreports, the observed behavior appeared in 93.5 percent and the expected behavior in only 35.2 percent. Itfeels obvious to you and it is frequently not obvious to the reader. See bug-report_companion.md section 3.
DESCRIBE THE SYSTEM, NOT THE PERSON. A report that reads as an accusation gets a defensive fix or no fix.
HOW TO FILL THIS IN1. Read the comment under each heading: WHAT it wants, WHY it matters (with a pointer into bug-report_companion.md), guiding questions to ASK, a GOOD and a WEAK example, and the TRAP to avoid.2. Replace each {{placeholder}} with your content. If you can only fill in two sections, fill in Steps to Reproduce and Expected and Actual Behavior; those two carry most of the value.3. If a section does not apply, write "N/A" and one line of why. "Could not reproduce" is a real answer and a useful one - do not invent steps to fill the space.4. Before you file it: DELETE every HTML comment. They are guidance, not content.-->
# {{report_id}}: {{one_line_summary}}
## Summary
<!-- WHAT One or two sentences: the observable failure, where it happens, and who it affects. This is what a reader sees in a triage queue before deciding whether to open the report. WHY A triage list is read fast. A summary that names the observable failure and its context can be routed without opening it; one that names a suspected cause sends the reader down your hypothesis instead of to the evidence. Deep dive: bug-report_companion.md section 3 (Summary). ASK What actually went wrong, in plain words? Where does it happen (feature, screen, endpoint)? Who does it affect, and roughly how many? Is anything lost or exposed? GOOD "A user who is not entitled to a region can see that region's totals through a shared saved view. Affects any recipient of a shared view whose filter includes data they cannot otherwise access." WEAK "Permissions are broken." (no observable behavior, no location, no affected population - and it is a diagnosis, which may be wrong) TRAP Writing your theory as the summary. If you have a theory, it is welcome, but put it at the end of Expected and Actual Behavior and label it as a guess. -->
{{summary}}
## Steps to Reproduce
<!-- WHAT Numbered steps from a known starting state to the failure, short enough that someone will actually run them. WHY This is the deliverable. The aim of the whole document is to let the reader see the failure themselves, and the research consistently puts steps to reproduce at the top of what developers want. It is also what they most often do not get. Deep dive: bug-report_companion.md section 3 (Steps to Reproduce). ASK What state do you start from - which account, which data, which configuration? What exactly did you do, in order? Which step is the one where it goes wrong? What is the shortest path that still triggers it? Have you tried it twice? GOOD "1. Sign in as an analyst with access to AMER only (test account R). 2. Open dashboard DB-7. 3. Select the shared view 'EMEA weekly'. 4. Read the Total Revenue tile. -> The tile shows the EMEA total, which this account should not be able to see." WEAK "Open a shared view and look at the total." (which account, which entitlements, which dashboard, which view? Two readers will construct two different tests and one of them will not reproduce it) TRAP Starting in the middle. "Open the app" hides the account, the data and the configuration, which are usually the parts that matter. If you cannot reproduce it, say so plainly here and put everything you do know in Environment and Reproducibility - an honest "seen once, could not repeat" is far more useful than invented steps. -->
{{steps_to_reproduce}}
## Expected and Actual Behavior
<!-- WHAT What you expected to happen, what happened instead, and where your expectation comes from. WHY Two reports in three never say what should have happened, and it is the single most-omitted element in real bug reports. It feels obvious to you; the reader often genuinely does not know the intended behavior, especially if they did not build that part. Deep dive: bug-report_companion.md section 3 (Expected and Actual Behavior). ASK What did you expect, precisely? What happened instead, precisely? Where does the expectation come from - an acceptance criterion, documentation, the previous release, or your own assumption? Would a reasonable person expect the same? GOOD "Expected: the Total Revenue tile shows only regions this account is entitled to, so AMER only. (The design says entitlement is re-checked when a recipient opens a shared view.) Actual: the tile shows the combined AMER + EMEA total, and the EMEA rows are correctly hidden from the table below - so the row filter works and the total appears not to." WEAK "It shows the wrong number." (wrong compared to what? The reader cannot tell whether this is a bug or a misunderstanding, and their fastest move is to close it as working-as-designed) TRAP Skipping the expectation because it feels self-evident, or stating a cause instead of an observation. "The aggregate is computed before the filter" is a useful guess - label it as one and keep it separate from what you actually saw. -->
{{expected_and_actual}}
## Environment and Reproducibility
<!-- WHAT Where you saw it (build, environment, account, device, browser, configuration) and how reliably it happens. WHY Environment detail is the defense against "works for me", and reproducibility is a field rather than an adjective: many bugs are intermittent, and how often it happens changes both the diagnosis and the priority. Non-reproducible reports are a large studied category, not a personal failing. Deep dive: bug-report_companion.md section 3 (Environment and Reproducibility). ASK Which build or version? Which environment (production, staging, local)? Which account, and with what permissions? Which browser, device, OS? How many times out of how many attempts did it happen? Did anything else change around the same time? Have you searched for an existing report? GOOD "Staging, build 2.3.1, saved_views flag on. Test account R (AMER entitlement only), Chrome 141 on macOS. Reproduced 5 times out of 5, including after a hard refresh and in a private window. No existing report found for 'shared view total'." WEAK "Latest version, my machine, happens sometimes." (no build, no account, no configuration, and 'sometimes' is not a frequency) TRAP Leaving out the account and its permissions on anything access-related. On permission bugs the account IS the test case, and a report without it cannot be reproduced at all. -->
{{environment_and_reproducibility}}bug-report_template-full.md · ~3,800 tokens
---title: "{{one_line_summary}}"report_id: "{{report_id}}"reported_by: "{{reporter}}"reported_on: "{{date}}"affected_build: "{{build_or_version}}"severity: "{{severity}}"priority: "{{priority}}"assigned_to: "{{assignee}}"status: "{{report_status}}"doc_type: bug-reportsize: fullsource_template: bug-reportsource_template_version: 0.1.0---
<!--FULL BUG REPORT. The tracked record: the reporter's four sections, plus the evidence, the classification, thetriage decision, and what was actually wrong and what now stops it recurring.
NOTICE WHO FILLS IN WHAT. The first four sections are the reporter's, written at intake, and Evidence isusually theirs too (attach it while you still have it). The remaining three - Impact/Severity/Priority,Triage and Ownership, and Resolution and Regression Guard - are filled in by other people, later, as thereport moves through triage and resolution. That is the real difference between the two variants: lean is theintake form, full is what the record becomes. Do not put this version in front of a user or a support agent;use the lean one and let the record grow.
THIS VARIANT IS A STRICT SUPERSET OF THE LEAN ONE. The four lean sections appear here in the same order, withthe same headings and the same placeholders, and four sections are added.
YOU ARE REPORTING AN ANOMALY, NOT DIAGNOSING A DEFECT. At the moment the report is written nobody knowswhether the cause is a code flaw, a configuration, stale data, or a misunderstanding of intended behavior.The standards call this document an "anomaly report" or "incident report" for exactly that reason. Seebug-report_companion.md sections 1 and 2.
DESCRIBE THE SYSTEM, NOT THE PERSON. A report that reads as an accusation gets a defensive fix or no fix.
HOW TO FILL THIS IN1. Read the comment under each heading: WHAT it wants, WHY it matters (with a pointer into bug-report_companion.md), guiding questions to ASK, a GOOD and a WEAK example, and the TRAP to avoid. For tables, PRIORITY explains the ordering rule and ROW HINT says what a good row contains.2. Replace each {{placeholder}} with your content. Sections 5 to 8 stay empty until there is something true to put in them; an empty triage section is honest, an invented one is not.3. If a section does not apply, write "N/A" and one line of why.4. Before you file or close it: DELETE every HTML comment. They are guidance, not content.-->
# {{report_id}}: {{one_line_summary}}
## Summary
<!-- WHAT One or two sentences: the observable failure, where it happens, and who it affects. This is what a reader sees in a triage queue before deciding whether to open the report. WHY A triage list is read fast. A summary that names the observable failure and its context can be routed without opening it; one that names a suspected cause sends the reader down your hypothesis instead of to the evidence. Deep dive: bug-report_companion.md section 3 (Summary). ASK What actually went wrong, in plain words? Where does it happen (feature, screen, endpoint)? Who does it affect, and roughly how many? Is anything lost or exposed? GOOD "A user who is not entitled to a region can see that region's totals through a shared saved view. Affects any recipient of a shared view whose filter includes data they cannot otherwise access." WEAK "Permissions are broken." (no observable behavior, no location, no affected population - and it is a diagnosis, which may be wrong) TRAP Writing your theory as the summary. If you have a theory, it is welcome, but put it at the end of Expected and Actual Behavior and label it as a guess. -->
{{summary}}
## Steps to Reproduce
<!-- WHAT Numbered steps from a known starting state to the failure, short enough that someone will actually run them. WHY This is the deliverable. The aim of the whole document is to let the reader see the failure themselves, and the research consistently puts steps to reproduce at the top of what developers want. It is also what they most often do not get. Deep dive: bug-report_companion.md section 3 (Steps to Reproduce). ASK What state do you start from - which account, which data, which configuration? What exactly did you do, in order? Which step is the one where it goes wrong? What is the shortest path that still triggers it? Have you tried it twice? GOOD "1. Sign in as an analyst with access to AMER only (test account R). 2. Open dashboard DB-7. 3. Select the shared view 'EMEA weekly'. 4. Read the Total Revenue tile. -> The tile shows the EMEA total, which this account should not be able to see." WEAK "Open a shared view and look at the total." (which account, which entitlements, which dashboard, which view? Two readers will construct two different tests and one of them will not reproduce it) TRAP Starting in the middle. "Open the app" hides the account, the data and the configuration, which are usually the parts that matter. If you cannot reproduce it, say so plainly here and put everything you do know in Environment and Reproducibility - an honest "seen once, could not repeat" is far more useful than invented steps. -->
{{steps_to_reproduce}}
## Expected and Actual Behavior
<!-- WHAT What you expected to happen, what happened instead, and where your expectation comes from. WHY Two reports in three never say what should have happened, and it is the single most-omitted element in real bug reports. It feels obvious to you; the reader often genuinely does not know the intended behavior, especially if they did not build that part. Deep dive: bug-report_companion.md section 3 (Expected and Actual Behavior). ASK What did you expect, precisely? What happened instead, precisely? Where does the expectation come from - an acceptance criterion, documentation, the previous release, or your own assumption? Would a reasonable person expect the same? GOOD "Expected: the Total Revenue tile shows only regions this account is entitled to, so AMER only. (The design says entitlement is re-checked when a recipient opens a shared view.) Actual: the tile shows the combined AMER + EMEA total, and the EMEA rows are correctly hidden from the table below - so the row filter works and the total appears not to." WEAK "It shows the wrong number." (wrong compared to what? The reader cannot tell whether this is a bug or a misunderstanding, and their fastest move is to close it as working-as-designed) TRAP Skipping the expectation because it feels self-evident, or stating a cause instead of an observation. "The aggregate is computed before the filter" is a useful guess - label it as one and keep it separate from what you actually saw. -->
{{expected_and_actual}}
## Environment and Reproducibility
<!-- WHAT Where you saw it (build, environment, account, device, browser, configuration) and how reliably it happens. WHY Environment detail is the defense against "works for me", and reproducibility is a field rather than an adjective: many bugs are intermittent, and how often it happens changes both the diagnosis and the priority. Non-reproducible reports are a large studied category, not a personal failing. Deep dive: bug-report_companion.md section 3 (Environment and Reproducibility). ASK Which build or version? Which environment (production, staging, local)? Which account, and with what permissions? Which browser, device, OS? How many times out of how many attempts did it happen? Did anything else change around the same time? Have you searched for an existing report? GOOD "Staging, build 2.3.1, saved_views flag on. Test account R (AMER entitlement only), Chrome 141 on macOS. Reproduced 5 times out of 5, including after a hard refresh and in a private window. No existing report found for 'shared view total'." WEAK "Latest version, my machine, happens sometimes." (no build, no account, no configuration, and 'sometimes' is not a frequency) TRAP Leaving out the account and its permissions on anything access-related. On permission bugs the account IS the test case, and a report without it cannot be reproduced at all. -->
{{environment_and_reproducibility}}
## Evidence
<!-- WHAT Screenshots, recordings, logs, stack traces, request or response captures, and the identifiers that let someone find them again. WHY Evidence supports the steps rather than replacing them, and it becomes decisive when the failure is intermittent and steps alone will not reproduce it. Stack traces in particular are among the things developers most want and most rarely get. Deep dive: bug-report_companion.md section 3 (Evidence). ASK What artifact shows the failure most directly? Is there a trace, a log line, a request ID or a correlation ID? What should the reader look at in the attachment, and at what timestamp? Is there anything sensitive that needs redacting first? GOOD "Screenshot of the tile showing 1,284,900 with the table below listing only AMER rows (attached: shared-view-total.png). API response for GET /dashboards/DB-7/views/SV-31 attached (response.json); note `total` at line 3 against the empty EMEA rows array. Request ID 7f3c-...-a91, staging logs 2026-07-09 14:22 UTC." WEAK "See attached." (which of the four attachments, and what am I looking for in it?) TRAP Attaching a twenty-minute screen recording with no timestamp. Evidence nobody can navigate is evidence nobody uses. Say where to look. And redact credentials and personal data before attaching. -->
{{evidence}}
## Impact, Severity and Priority
<!-- WHAT What this costs, how severe it is, how urgent it is, and who set each value. WHY Severity and priority are INDEPENDENT axes and conflating them is the classic classification error. Severity is how much damage the defect does; priority is how soon it should be fixed. A crash in a rarely used legacy path is high severity and low priority; a cosmetic error on a launch homepage is low severity and high priority. Deep dive: bug-report_companion.md section 3 (Impact, Severity and Priority). ASK What is the damage: data loss, exposure, wrong numbers, blocked work, annoyance? How many users, and can they work around it? On your team's scale, what severity? How soon must it be fixed, and why that soon? Who assigned each value? PRIORITY Use your team's defined scale and put the level definitions where reporters can see them. There is NO standard severity scale: four, five and six-level scales are all in use, and S1-S4 numbering means different things in different places. Record who set each value, because the certification definitions say what the words mean and specify nobody to assign them. ROW HINT A good row gives the value, the reason in a few words, and the person who set it. A weak row gives a bare label. GOOD | Severity | S1 Critical | Data exposure across an entitlement boundary; not user-recoverable | Anjali Rao (QA) | WEAK | Severity | High | | | TRAP Inflating severity to get attention. Severity is the signal the release gate reads; once it is used as a negotiating position it stops carrying information, and the gate stops meaning anything. -->
| Field | Value | Why | Set by ||---|---|---|---|| Severity | {{severity_value}} | {{severity_reason}} | {{severity_owner}} || Priority | {{priority_value}} | {{priority_reason}} | {{priority_owner}} |
{{impact_narrative}}
## Triage and Ownership
<!-- WHAT What triage decided, who owns the defect now, and which release it is assigned to. WHY Triage is where a report stops being a claim and becomes work: the meeting reviews new reports, validates or corrects severity and priority, and assigns ownership and a release. Recording the outcome is what stops the same argument happening twice, and it is where this document meets the test plan's release gate. Deep dive: bug-report_companion.md section 3 (Triage and Ownership). ASK What did triage decide - fix now, fix later, reject, needs investigation, cannot reproduce? If a value was changed from what the reporter set, why? Who owns it now, by name? Which release is it assigned to? Does it block a gate? GOOD "Triaged 2026-07-09. Severity confirmed S1; priority raised from P2 to P1 because the test plan's exit criteria treat any entitlement failure as a suspension event, not a triage item. Owner: Marcus Bell. Assigned to the 2.3.1 hotfix. Blocks phase 2 of the Saved Views rollout until fixed and re-verified." WEAK "Triaged. Assigned to engineering." (no decision, no named owner, no release, and no record of whether the classification changed) TRAP Silently changing the reporter's severity. If triage disagrees, say so and say why - the reporter learns the scale, and the next report is better classified. -->
{{triage_and_ownership}}
## Resolution and Regression Guard
<!-- WHAT What was actually wrong, what changed, and what now stops it coming back. WHY Root cause belongs here rather than in the reporter's sections, because it is the output of the investigation rather than an input to it. And the last line is the one that matters most: a closed defect with no regression guard is an invitation to fix the same thing twice. Deep dive: bug-report_companion.md section 3 (Resolution and Regression Guard). ASK What was the actual cause, as opposed to the first theory? What changed, and where (commit, PR, release)? How was the fix verified, by whom, and against which steps? What test now guards this, by ID? Was anything else found on the way? GOOD "Cause: the aggregate for the Total tile was computed before the entitlement row filter was applied, so filtered rows still contributed to totals. The row-level check was correct throughout, which is why the table looked right. Fixed in PR 812, released in 2.3.2. Verified by Anjali Rao against the original steps on 2026-07-11. Regression guard: TC-047 step 4, which now asserts the aggregate as well as the rows." WEAK "Fixed." (no cause, no change reference, no verification, and nothing stopping a recurrence) TRAP Reopening this report if the defect recurs later. Open a new one and link it: a reopened ticket loses the record of what was fixed the first time, and you lose the ability to tell a regression from an incomplete fix. -->
{{resolution_and_regression_guard}}---title: "Shared view totals include rows the recipient is not entitled to see"report_id: "DEF-2291"reported_by: "Anjali Rao (QA Lead, Reporting)"reported_on: "2026-07-13"affected_build: "2.3.1 (staging)"severity: "S1 Critical"priority: "P1"assigned_to: "Marcus Bell (Staff Engineer, Reporting)"status: "closed"related: - "../test-case/test-case_example.md (TC-047, the case that found this, at step 4)" - "../test-plan/test-plan_example.md (Saved Views test plan; risk R-05 and the suspension rule)" - "../sdd/sdd_example.md (Saved Views design; the entitlement re-check on recipient read)"doc_type: bug-reportsize: fullsource_template: bug-reportsource_template_version: 0.1.0---
<!--Worked example for the bug-report bundle: a full-variant report closing the chain this family started. Thedefect was found by TC-047 step 4, which was designed from risk R-05 in the test plan, which inherited itfrom the program risk register. Figures marked "illustrative" are made up for the example.
WHY THE FULL VARIANT: this report was filed by QA against a release gate, triage changed one of its values,and the closed record is the evidence that the gate was satisfied. A user-filed report of the same defectwould arrive in the lean variant and grow into this one.
THREE THINGS TO STUDY. First, the expected behavior is stated AND sourced - the element missing from two realreports in three. Second, the reporter and triage disagreed about priority, and the disagreement is recordedrather than silently overwritten, which is what teaches the next reporter how the scale works. Third, theresolution names the regression guard, so the chain closes where it began: a risk produced a plan, the planproduced a case, the case produced this report, and this report produced a test.-->
# DEF-2291: Shared view totals include rows the recipient is not entitled to see
## Summary
A recipient opening a shared saved view sees aggregate values computed over rows they are not entitled to,even though those rows are correctly hidden from the table. On dashboard DB-7, an analyst entitled only toAMER sees the combined AMER and EMEA revenue total. Affects any recipient of any shared view whose filterselects data the recipient cannot otherwise access, which on current usage is an estimated 40 shared viewsacross 12 dashboards (illustrative).
## Steps to Reproduce
1. As an administrator, confirm test account **R** has entitlement to AMER and **not** to EMEA on dashboard **DB-7**. (If R holds EMEA by inheritance the test proves nothing; this is why the check is step 1.)2. As account **O** (entitled to all regions), create a saved view on DB-7 filtered to `region in [EMEA]`, and set its scope to `shared`. This is view **SV-31**.3. As account **R**, request `GET /dashboards/DB-7/views/SV-31`.4. Read the `total_revenue` value in the response, and compare it with the returned rows.
At step 4 the returned rows array is empty of EMEA rows, correctly, while `total_revenue` equals the EMEAtotal. The same is visible in the UI: open DB-7 as R, select SV-31, and read the Total Revenue tile above anempty table.
## Expected and Actual Behavior
**Expected.** The aggregate reflects only rows the recipient is entitled to, so for account R the total shouldbe the AMER-only total, or zero for a view that selects nothing R may see. This follows from the designdocument, which states that on any read of a shared view by another user the service re-checks that user'saccess to the underlying dashboard data, *so a shared view can never widen what a recipient may see*.
**Actual.** The row filter is applied correctly and the aggregate is not. `total_revenue` returns 1,284,900(illustrative), which is the EMEA total that account O would see, while `rows` is empty. The magnitude of dataR may not access is therefore disclosed exactly, in a single number.
**Note on where the expectation comes from, because it matters here.** No acceptance criterion covers this.The agreed criteria for the Saved Views stories describe saving, loading, defaulting and sharing views; noneof them says anything about what a partially entitled recipient sees inside an aggregate. The expectationcomes from the design document's re-check statement and from the program's PII risk appetite, not fromanything the business signed off. This is exactly the territory test design is supposed to reach andacceptance criteria are not.
**Theory, labeled as a theory:** the aggregate looks like it is computed before the entitlement row filter isapplied. Not verified at the time of filing.
## Environment and Reproducibility
Staging, build **2.3.1**, `saved_views` flag enabled. Accounts R (AMER only) and O (all regions) from thestandard permission persona set. Reproduced via the API **5 times out of 5** and in the browser (Chrome 141,macOS) **3 times out of 3**, including after a hard refresh and in a private window.
Not reproducible when the recipient has no entitlement to *any* row the view selects: in that case the totalreturns zero, which is why the fully-unentitled case (TC-048) passed and did not surface this. The defectrequires a **partially** entitled recipient, which is the partition TC-047 was written to cover.
No existing report found. Searched the tracker for "shared view total", "aggregate entitlement" and"saved view permissions".
## Evidence
- `shared-view-total.png` - the Total Revenue tile reading 1,284,900 above an empty table, as account R.- `response.json` - the full API response for step 3. Look at `total_revenue` on line 3 against the empty `rows` array on line 8.- Staging application log, 2026-07-13 14:22 UTC, request ID `7f3c-4a11-a91b` (illustrative). The entitlement re-check is logged as passing, which is correct: the check ran and filtered the rows. Nothing in the log indicates the aggregate path exists, which is itself the useful signal.
No customer data is attached; both accounts are synthetic personas and the revenue figures are seeded testdata.
## Impact, Severity and Priority
| Field | Value | Why | Set by ||---|---|---|---|| Severity | **S1 Critical** | Discloses the magnitude of data across an entitlement boundary. Not user-recoverable and not detectable by the affected user. On Acme's four-level scale (S1 Critical / S2 Major / S3 Minor / S4 Trivial), data exposure is S1 by definition | Anjali Rao (QA Lead) || Priority | **P1**, raised from P2 | Reporter set P2 on the reasoning that this is staging and the feature is behind a flag, so no customer is currently exposed. Triage raised it: the test plan's exit criteria treat any entitlement failure as a **suspension event** rather than a triage item, and phase 2 of the rollout cannot proceed until it is fixed and the full permission matrix is re-run | Priya Nair (PM), at triage |
**Impact if shipped.** Any recipient of a shared view could infer the size of data they are not permitted tosee, without any indication that they were seeing it. The row-level protection working correctly is what makesthis dangerous rather than obvious: the table looks right, so nothing prompts a user or a reviewer toquestion the total.
**The severity and priority disagreement is worth keeping**, not tidying away. Both readings were reasonable.The reporter weighed current exposure, which was genuinely zero; triage weighed the release gate, which thereporter did not own. The record now shows the next reporter how Acme's scale treats a flagged, staging-onlyentitlement failure.
## Triage and Ownership
Triaged **2026-07-13**, same day, by Priya Nair (PM), Marcus Bell (Engineering) and Sam Okafor (Security).
- Severity confirmed at S1.- Priority raised from P2 to P1, for the reason recorded above.- **Phase 2 (sharing) testing suspended immediately**, per the test plan's suspension rule. Anjali Rao made the call at 15:10 UTC and notified Priya Nair, Marcus Bell and Sam Okafor the same afternoon. Phase 1 (private views) testing continued, since it does not exercise the sharing path.- Owner: **Marcus Bell**. Assigned to the **2.3.2** hotfix, not to the next scheduled release.- Sam Okafor confirmed the security review remains blocked until this is closed and the full permission matrix has been re-run from the start.
## Resolution and Regression Guard
**Cause.** The aggregate for dashboard tiles was computed in the query layer *before* the entitlement rowfilter was applied. The re-check itself was implemented correctly and ran on every recipient read, exactly asthe design document specifies; it filtered rows on the way out and never touched the pre-computed aggregates.The reporter's labeled theory turned out to be right, but the investigation confirmed it rather thaninheriting it.
**Why it was not caught earlier.** Every earlier entitlement case asserted on returned rows. A row-levelassertion cannot see this defect: the rows were always correct. TC-047 caught it only because step 4 assertson the aggregate as well, and that step was added at version 1.1 after Sam Okafor's review of the case.Version 1.0 of TC-047 would have passed against this build.
**Fix.** PR 812 (illustrative) moves aggregate computation behind the entitlement filter, so both are derivedfrom the same permitted row set. Released in build **2.3.2** on 2026-07-14.
**Verification.** Re-run by Anjali Rao on 2026-07-15 against the original steps: `total_revenue` returns theAMER-only total for account R, and O's view is unchanged. The **full permission matrix was re-executed fromthe start**, not just this case, per the test plan's resumption rule: an entitlement defect invalidates theassumption behind every passing result in that matrix. All 12 combinations passed. Sam Okafor confirmed thesecurity review unblocked on 2026-07-15 and phase 2 testing resumed the same day.
**Regression guard.** [TC-047](../test-case/test-case_example.md) step 4, which asserts the aggregate as wellas the rows, is now part of the release regression set and runs on every pipeline execution for the releasebranch. A second case, TC-053, was added for the same class of defect on the row-count badge, which shares thepre-filter computation path and was not covered by anything.
**Not reopened.** If this recurs, a new report is opened and linked to this one, so that the record of what2.3.2 actually changed stays intact.Provenance
Section titled “Provenance”The reasoning, the history and every source, in the repository:
- Companion - the long-form argument: why these sections, where the sources disagree, and what the bundle refuses to claim
- History - what changed in this bundle, and when
- Research log - every source consulted, with what each one actually supports
- Catalog metadata - the machine-readable record this page is generated from
Catalog record: 8 sections across 1 format(s).