Repository navigation
fix(postmortem): a postmortem published while its incident is hidden is sent when the incident is made visible - #4462
Merged
Conversation
… is never announced Reproduces the bug #4429 found: the send job settles the notification of a postmortem published on a hidden incident as Skipped, and making the incident visible later queues nothing, so subscribers are never told. Fails until the fix that follows. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…is sent when the incident is shown The status page shows a postmortem only on an incident it shows, so the send job skips the notification of one published while its incident is hidden. Nothing queued it again when the incident was made visible, so subscribers were never told (found in #4429). Now the update that switches Visible on Status Page on for a hidden incident queues that notification - only a skip because the incident was hidden, in this release's words or the earlier ones, never one that was sent, failed or skipped for another reason - once, under a compare-and-set on the status and its message. The incident's visibility rides on the one stored read of onBeforeUpdate (recordStoredValuesBeforeUpdate), the rule is IncidentPostmortemPublication's (isShownByUpdate, isHiddenIncidentSkip, QueueIfSkippedAsHidden), and the claim race is closed both ways: the update looks again once written, and the job looks again after its hidden skip (requeueIfShownSinceRead). The job's skip now says the postmortem waits for the incident. The Postmortem page labels it 'Not sent yet: incident hidden from status pages', and the Visible on Status Page switch says turning it on sends it, in all 17 locales. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…d to end; docs Tests for the rule (isIncidentShownBy, isShownByUpdate, isHiddenIncidentSkip, isWaitingForIncidentToShow, QueueIfSkippedAsHidden), the real update hooks (one stored read, the guarded queue, the race both ways, bulk updates, private and unpublished cases), PUT /api/incident, the send job's hidden skip and its look again, the real hooks with the real job in memory, the Postmortem page's label, the Settings switch's description, the one-rule source guard, and the strings in all 17 locales. The subscriber docs (en, fa) say a postmortem published while its incident is hidden is sent when the incident is made visible, once; the incident page and the declaring docs point there. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…isible-later Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…; no note in the stored read Code review fixes for a postmortem that waits for its hidden incident. - A private incident is hidden from every status page, whatever its switch says, so its postmortem waits too (isIncidentShown). The send job skips it, an update that leaves it private sends nothing, and making it not private - with Visible on Status Page on - sends it once. The Settings switch says it sends the postmortem only when switching it on alone would (isSentBySwitchingVisibilityOn). - The earlier wording is no longer recognised in code. The migration MarkPostmortemsWaitingForHiddenIncidents1799100000000 gives skips in the earlier words the new ones when the incident is still hidden (switch off, never set, or private), and leaves those whose incident was shown since, so hiding and showing such an incident never emails an old postmortem. It changes only the message; it sends nothing. - The stored read before the write no longer loads the postmortem note unless the update writes the postmortem. Once an update that shows the incident is written, one read decides (isDueOnceShown) and the guarded write expects the hidden skip's status and words. - The Settings card no longer loads the note. - One rule decides what is compared (isComparedBy), and the job's two look-agains are one helper (requeueIfChangedSinceRead). - The waiting badge draws a clock instead of the crossed-out circle. Tests: the migration (SQL contract, texts, registration, and an opt-in Postgres suite wired into the Common Test workflow), the badge's clock, private incidents through the rule, the hooks, PUT /api/incident, the job, the in-memory end-to-end harness and both pages, and the earlier wording everywhere. Docs (en, fa) say private incidents are hidden and what happens to skips from earlier releases. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…isible-later Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… for the postmortem The incident's Settings form sends both switches with every save, so a save that keeps a hidden incident hidden - Visible on Status Page off, Private Incident off - asked for the stored read as if it might show the incident. An update that writes Visible on Status Page as off (or null) leaves the incident hidden whatever else it writes, so mayShowIncident says no, and the created-notification suite's "reads nothing" holds again. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Contributor
Author
✅ Snyk checks have passed. No issues have been found so far.
💻 Catch issues earlier using the plugins for VS Code, JetBrains IDEs, Visual Studio, and Eclipse. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The problem
Found in #4429: a postmortem published while its incident is hidden from status pages was never announced.
The status page shows a postmortem only on an incident it shows. So the send job settled the notification of a postmortem published on a hidden incident as Skipped ("Incident is not visible on status page. Skipping notifications to subscribers."). When someone later turned Visible on Status Page on, the postmortem appeared on the status page for the first time, but nothing queued its notification again. Only the API's
Pendingwould ever send it.What changes
Making the incident visible sends the postmortem that was waiting for it, once. This covers every update that makes a hidden incident visible: the incident's Settings form, the API, Terraform and workflows. They all go through
IncidentService.A private incident counts as hidden. It is hidden from every status page, whatever its switch says, so its postmortem waits too. Making it visible means switching Visible on Status Page on and Private Incident off. That can happen in one save, or in two saves in either order. The job, the update and the Dashboard all use the same check (
isIncidentShown). This matches howIncidentSubscriberAudienceBuilderalready treats private incidents.The rule stays in one place.
IncidentPostmortemPublicationis the only place that decides. It gains:isIncidentShown,mayShowIncident,isComparedByandisShownByUpdate;isHiddenIncidentSkip,isWaitingForIncidentToShow,isSentBySwitchingVisibilityOnandisDueOnceShown;getNotificationAction,QueueIfSkippedAsHidden.The server, the send job and the Dashboard all ask it.
No extra read before the write, and no postmortem body in it. The update reads
isVisibleOnStatusPage,isPrivateand the notification's status and message inrecordStoredValuesBeforeUpdate, the existing stored read inonBeforeUpdate. It reads them only when the update may show the incident: it switches visibility on, or writes Private Incident as off without switching visibility off. Saving a hidden incident's Settings reads nothing for this.VeryLongText) is not part of that read unless the update writes the postmortem itself.findOneByIdreads the note and the switches. This happens only when the notification stands skipped as hidden.The notification is queued once, even when two updates race. After the write, the hook queues the notification with a hook-free compare-and-set that expects both the status and the message (
Skippedplus the hidden-incident message). Two updates that both show the incident cannot queue it twice, and neither can an update racing a send that settled in the meantime.The claim race is closed both ways, as Subscribers hear about a postmortem once, when it is published #4429 did for publishing.
requeueIfChangedSinceRead. It only moves the notification back while it still stands at the exact skip that run wrote.The skip says what happens next. The job's message is now "Incident is hidden from status pages. Subscribers will be sent the postmortem when the incident is made visible on status pages."
The Dashboard explains it.
Docs (English and Persian). Status pages → Subscribers → The postmortem gets a bullet for this case: private incidents, what happens to skips from earlier releases, and an API sentence naming
isVisibleOnStatusPageandisPrivate. The incident page's Postmortem item and "Declaring hidden and publishing later" point to that section.What changes for existing customers
MarkPostmortemsWaitingForHiddenIncidents1799100000000gives them the new skip message. Making the incident visible then sends them, once.PUTthat makes such an incident visible now movessubscriberNotificationStatusOnPostmortemPublishedtoPendingwith its own guarded write. That meansisVisibleOnStatusPageset totrue, plusisPrivateset tofalsefor a private incident. Nothing changes when the request sets that status itself.Decisions
IncidentCreatedRenotify.isHiddenFromStatusPagesSkip, which already works this way.statusPagesNotifiedOnCreation, so sending it again would reach the earlier pages twice.Left out
StatusPageAPIfilters incidents onisVisibleOnStatusPageonly, never onisPrivate. Making an incident private switches its visibility off (IncidentService.onBeforeUpdate). But an API write ofisVisibleOnStatusPage: truealone on an incident that is already private stores both as on, and the status page then shows it. Subscriber notifications, including this postmortem, treat it as hidden. This is pre-existing and worth its own fix.Tests
Regression test, which fails on master:
App/Tests/Workers/Jobs/Incident/PostmortemPublishedNotifiesOnce.test.tsruns the real update hooks and the real send job against an in-memory incident. It covers:"true";Unit and service tests:
Common/Tests/Types/StatusPage/IncidentPostmortemPublication.test.tscovers the rule, private incidents included.Common/Tests/Server/Services/PostmortemNotifyOnPublish.test.tsruns through the realonBeforeUpdateandonUpdateSuccess. It covers:API and job tests:
Common/Tests/Server/API/PostmortemNotifiesOnceThroughTheApi.test.tsexercisesPUT /api/incident/:idwithisVisibleOnStatusPageandisPrivate. It covers a viewer being refused, hiding and showing again, and an earlier release's skip. The stand-in database now keeps what each write sets.App/Tests/Workers/Jobs/Incident/SendPostmortemNotificationToSubscribers.test.tscovers the job's hidden skip (private incidents included), which had no test before, and its look again.Migration:
Common/Tests/Server/Infrastructure/Postgres/MarkPostmortemsWaitingForHiddenIncidentsMigration.test.tschecks the SQL contract, the texts and the registration. It also has an opt-in Postgres suite (RUN_POSTGRES_POSTMORTEM_WAITING_MIGRATION_TESTS) that covers:down().The flag is wired into the Common Test workflow, and
Tests/Ops/PostgresSuitesRunInCipasses.Dashboard:
Common/Tests/App/Dashboard/IncidentPostmortemPage.test.tsxchecks the label: hidden, private, earlier wording, visible, another reason, queued.Common/Tests/App/Dashboard/SubscriberNotificationStatusWaiting.test.tsxchecks the clock icon.Common/Tests/App/Dashboard/IncidentSettingsRenotifyOnPublish.test.tsxchecks the switch's description: when it appears and when it does not, private included. It also checks that the card does not load the note, and renders the description in the realModelForm.Guards and docs:
App/Tests/Dashboard/PostmortemWaitsForIncidentI18n.test.tschecks the strings in all 17 locales.App/Tests/PostmortemPublicationOneRule.test.tsis the source guard.App/Tests/FeatureSet/Docs/PostmortemNotifiesOnceDocs.test.tscovers the English and Persian docs.Two existing tests were updated because showing an incident now also reads it once for its postmortem:
IncidentCreatedRenotifyOnPublishandIncidentStatusPageScopecount only the created notification's own reads now.Verification
I ran the following locally, on the branch with the newest master merged in.
The regression test fails before the fix and passes after. On the unfixed code,
PostmortemPublishedNotifiesOncereported "is announced once, when the incident is made visible" as failing, because nothing was sent. It passes now.The new tests catch regressions. I broke the code-review fixes one at a time and re-ran the postmortem suites. Every break made tests fail:
The migration on real Postgres. The opt-in suite passed, 24 tests, against a postgres:15 container of my own (
postmortem-visible-later-pg), before and after the merge.Tests/Ops/PostgresSuitesRunInCipasses (65 tests) with the new flag wired into the Common Test workflow.Type checks:
npm run compileexits 0 inpackages/Commonand inpackages/App. App's compile also type-checksApp/Tests, and it ran again after the merge and after the last commit.npm run compile-testsexits 0 inpackages/Common(all three parts).tsc --noEmitexits 0 in the Dashboard.jest, App:
Tests/Dashboardin full: 618 suites, 32,859 tests passed.Tests/FeatureSet/Docsin full: 146 suites, 6,368 tests passed.Tests/Workers/Jobs/Incident,Tests/Workers/Jobs/StatusPageSubscriber,Tests/Notificationand the guards near this change: 73 suites, 3,191 tests passed.jest, Common:
Tests/UIin full: 821 suites passed.IncidentService, plus the Postmortem and Settings page suites, the badge, the migration suite andTests/Types/StatusPage. With CI's billing settings (BILLING_ENABLED=trueand theSUBSCRIPTION_PLAN_*lines fromtest-setup.sh): 201 suites, 11,353 tests passed. With billing off: 198 suites, 11,272 tests passed. The 5 skipped suites are opt-in Postgres suites.jest, Home: 38 suites, 2,257 tests passed.
i18n and lint:
npm run i18n:extractadds nothing.npm run i18n:checkpasses ("Every locale file is sound and no translation was lost").eslint --fixandprettier --checkare clean on every changed file. I linted Common, App and Dashboard in separate runs.I did not run the live stack, the Playwright E2E or offline suites, or the whole repository's tests. No offline fixture page sends a new request: the Postmortem and Settings cards only select more columns of the same incident read.
🤖 Generated with Claude Code