Skip to content

Backup: report an activity log failure like a backups failure - #53790

Merged
CGastrell merged 3 commits into
trunkfrom
fix/jetpack-3067-backup-activity-error-layout
Oct 19, 2026
Merged

CGastrell merged 3 commits into
trunkfrom
fix/jetpack-3067-backup-activity-error-layout

Conversation

@CGastrell

@CGastrell CGastrell commented Oct 19, 2026 •

Copy link
Copy Markdown
Contributor

Fixes JETPACK-3067

Proposed changes

  • Report an activity log failure the same way as a backups failure when the log has never loaded: one full-width notice with Try again, and no list card or empty detail pane beside it. Before, the notice sat inside the list card and the right pane was blank, on desktop and on phone.
  • After the log has loaded once, a later failure (a refetch, a page change or a sort change) keeps the list layout, with the notice inside the card. The reader keeps the pager, so a failed page 2 no longer strands them: Previous page works. Hiding the rows would have thrown away data they already have.
    • useActivityLog keeps the last answered totals, because DataViews hides its pager when the total drops to zero.
    • "Never loaded" is totalPages === null. No new state.
  • A first-load failure now moves focus to the "Backup activity" group after a successful retry. It used to land on the "Latest backups" heading.

Known and left alone:

  • If both requests fail, two full-width notices stack, one per failure. Each has its own Try again.
  • The pager's buttons are 32px on phone. That is DataViews' own size.
  • Leaving the Overview and coming back counts as a first load again. A failed page then still gets its pager, with the page count taken from the cached first page.
  • While a failed page retries, the list shows no rows. React Query would otherwise fill it with the last page's.
  • The notice's title and props still exist in Overview and ActivityList. One copy needs a new shared component or a bigger refactor.

Related product discussion/links

Does this pull request change what data or activity we track or use?

No.

Screenshots

Local connected site with a Backup plan, 1400px and 390px wide. Only the request under test is failed (HTTP 503) with Playwright route interception. Everything else is real data.

Case Before After
Activity log fails on first load, desktop
Activity log fails on first load, phone
Backups fail on first load (reference), desktop
Backups fail on first load (reference), phone
Activity log fails on first load, on page 2, desktop
Activity log fails on first load, on page 2, phone
A refetch fails while rows are shown, desktop
A refetch fails while rows are shown, phone
A page change fails, desktop
A page change fails, phone
A sort change fails, desktop
A sort change fails, phone
Try again after a first-load failure, desktop
Try again after a first-load failure, phone
Try again after a page-change failure, desktop
Try again after a page-change failure, phone
Backups and activity both fail, desktop
Backups and activity both fail, phone

Testing instructions

Checked: pnpm run test (1047 tests), pnpm run typecheck and lint-changed in projects/packages/backup. All pass. The new page-2 test fails if the hook change is reverted, and if Overview is made to fail on any error.

The screenshots come from the built dashboard on a local connected site. To reproduce a case by hand, open Jetpack › Backup on a site with the modernized dashboard and a Backup plan, block the request /wp-json/jetpack/v4/site/rewindable-activity (or /wp-json/jetpack/v4/backups) in the browser's network tools, and reload. I did not run these exact DevTools steps. The screenshots used the same request blocking through Playwright.

…tice

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Oct 19, 2026 •

Copy link
Copy Markdown
Contributor

Are you an Automattician? Please test your changes on all WordPress.com environments to help mitigate accidental explosions.

  • To test on WoA, go to the Plugins menu on a WoA dev site. Click on the "Upload" button and follow the upgrade flow to be able to upload, install, and activate the Jetpack Beta plugin. Once the plugin is active, go to Jetpack > Jetpack Beta, select your plugin (Jetpack), and enable the fix/jetpack-3067-backup-activity-error-layout branch.
  • To test on Simple, run the following command on your sandbox:
bin/jetpack-downloader test jetpack fix/jetpack-3067-backup-activity-error-layout

Interested in more tips and information?

  • In your local development environment, use the jetpack rsync command to sync your changes to a WoA dev blog.
  • Read more about our development workflow here: PCYsg-eg0-p2
  • Figure out when your changes will be shipped to customers here: PCYsg-eg5-p2

@github-actions

Copy link
Copy Markdown
Contributor

Thank you for your PR!

When contributing to Jetpack, we have a few suggestions that can help us test and review your patch:

  • ✅ Include a description of your PR changes.
  • ✅ Add a "[Status]" label (In Progress, Needs Review, ...).
  • ✅ Add testing instructions.
  • ✅ Specify whether this PR includes any changes to data or privacy.
  • ✅ Add changelog entries to affected projects

This comment will be updated as you work on your PR and make changes. If you think that some of those checks are not needed for your PR, please explain why you think so. Thanks for cooperation 🤖


Follow this PR Review Process:

  1. Ensure all required checks appearing at the bottom of this PR are passing.
  2. Make sure to test your changes on all platforms that it applies to. You're responsible for the quality of the code you ship.
  3. You can use GitHub's Reviewers functionality to request a review.
  4. When it's reviewed and merged, you will be pinged in Slack to deploy the changes to WordPress.com simple once the build is done.

If you have questions about anything, reach out in #jetpack-developers for guidance!

@jp-launch-control

jp-launch-control Bot commented Oct 19, 2026 •

Copy link
Copy Markdown

Code Coverage Summary

Coverage changed in 2 files.

File Coverage Δ% Δ Uncovered
projects/packages/backup/src/dashboard/screens/overview.tsx 72/73 (98.63%) -1.37% 1 ❤️‍🩹
projects/packages/backup/src/dashboard/hooks/use-activity-log.ts 66/66 (100.00%) 0.00% 0 💚

Full summary · PHP report · JS report

…ows while it retries

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@CGastrell
CGastrell marked this pull request as ready for review October 19, 2026 18:50

@dhasilva dhasilva left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The approach holds up. TanStack applies placeholderData only while a query is pending, and retrying an errored query that holds no data puts it back to pending, so the isPlaceholderData && error !== null check in the second commit is the right signal. The DataViews 20 footer hides on ! totalItems, so keeping the last totals is what keeps the pager.

No blockers. There are two test changes I'd ask for and two optional ones, all inline.

I mutation-tested the new tests at e7a3b31 to check that each one fails without the change it covers:

Mutation Result
showsPlaceholder = false "does not fill a failed page…" fails (2 "Backup complete" vs 1)
activityFailed = false 3 tests fail
lastAnswer.current = answer (no keeping) 2 tests fail, as the description says
in-card returnFocusTo={ undefined } all 1048 tests still pass (see inline)

Two notes outside the code:

  • Changelog: the empty entry is right while the dashboard is behind its flag. If #53443 (the flip) merges first, this becomes a fix users can see and needs a real entry.
  • Labels: the PR has both [Status] In Progress and [Status] Needs Review.

Generated by Claude.

Comment on lines +133 to +136
const lastAnswer = useRef( answer );
lastAnswer.current = answer ?? lastAnswer.current;
const totalItems = lastAnswer.current?.totalItems ?? 0;
const totalPages = lastAnswer.current?.totalPages ?? null;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Optional: the kept totals belong to this hook instance, not to the query, which shows up two ways:

  • A failed per-page change keeps totalPages counted at the old size. DataViews resets to page 1 with the new perPage. With 30 rows at 10 per page, switching to 20 per page shows "1 of 3", and page 3 is past the end.
  • A return trip from Download or Restore after gcTime (5 min) to a remembered page 2 or later starts with an empty ref. If that page fails, Overview shows the full-width notice with no pager. Try again is then the only way off the page until a reload resets lastView. The description lists "counts as a first load again" as known, but not that it strands the reader.

One change fixes both: keep only totalItems, work out totalPages from the current pageSize, and when nothing is kept, fall back to the page-1 query's totalItems. useHasRestorePoints keeps that query mounted in Overview, so it's there whenever page 1 has loaded.

Generated by Claude.

code: 'activity_log_fetch_failed',
message: 'Service unavailable',
} )
: new Promise( () => {} );

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nothing checks where focus goes after a retry of the in-card notice succeeds. On trunk, error-persists-during-retry.test.tsx asserted that it lands on the "Latest backups" heading. This PR points that test at the first-load path instead. With returnFocusTo={ undefined } on the in-card QueryError (activity-list/index.tsx L312), all 1048 tests still pass.

The bug it would catch: the heading loses its ref, and focus drops to <body> after a successful retry. That is the regression the old assertion was there for.

The cheapest fix is here. Replace this never-settling promise with a deferred one, resolve it after the "same count" assertion, then await waitFor( () => expect( screen.getByRole( 'heading', { name: 'Latest backups' } ) ).toHaveFocus() ). One test then covers both behaviours.

Generated by Claude.

// The detail pane keeps its heading; only the list rows are in question.
const before = screen.getAllByText( 'Backup complete' ).length;
failPage2 = false;
await userEvent.click( screen.getAllByRole( 'button', { name: 'Try again' } ).at( -1 ) );

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

.at( -1 ) is choosing between two notices. The catch-all Promise.resolve( {} ) also answers /jetpack/v4/backups, so "We couldn't check your site's backup status." is on screen for the whole test (confirmed by dumping .jpb-query-error mid-test). It finds the right button only because of DOM order. The test above has the same setup.

The mockActivityLog helper this PR adds to error-persists-during-retry.test.tsx answers that route with [] for exactly this reason. I'd suggest one helper for the two copies of the page-2 routing in this file that does the same. This line can then be a plain getByRole( 'button', { name: 'Try again' } ).

Generated by Claude.

Comment on lines +303 to +304
// the wrong answer used to be. A log that never answered has no list to
// keep, so Overview reports that one instead.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This reason is also in overview.tsx L250 ("Only a log that never answered has no list to keep"). Overview makes the decision, so it can keep the reason and this comment can just point there:

Suggested change
// the wrong answer used to be. A log that never answered has no list to
// keep, so Overview reports that one instead.
// the wrong answer used to be. Overview reports a log that never answered.

Generated by Claude.

screen.findByText( "We couldn't load your site's activity." )
).resolves.toBeInTheDocument();
expect( screen.getByRole( 'heading', { name: 'Latest backups' } ) ).toBeInTheDocument();
// The pager is what gets the reader back; DataViews hides it when the page has no rows.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This repeats the hook's comment on lastAnswer, and has drifted from it: the DataViews footer hides on ! totalItems, not on the page's rows. The hook owns the reason:

Suggested change
// The pager is what gets the reader back; DataViews hides it when the page has no rows.
// The pager is what gets the reader back.

Generated by Claude.

expect( screen.getByText( 'Service unavailable' ) ).toBeInTheDocument();
// The regression: a 5xx telling the reader their site has no activity.
expect( screen.queryByText( 'No results' ) ).not.toBeInTheDocument();
// The notice stands in for the list card instead of sitting inside a narrow one.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"Instead of sitting inside a narrow one" describes the layout this PR removes, which a reader won't find after merge:

Suggested change
// The notice stands in for the list card instead of sitting inside a narrow one.
// The notice replaces the list card.

Generated by Claude.

…failure tests

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@CGastrell
CGastrell merged commit d7ffe66 into trunk Oct 19, 2026
91 checks passed
@CGastrell
CGastrell deleted the fix/jetpack-3067-backup-activity-error-layout branch October 19, 2026 20:48
@github-actions github-actions Bot removed the [Status] Needs Review This PR is ready for review. label Oct 19, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants