Skip to content

943 seo data optimization error reporting refactoring#23402

Open
pls78 wants to merge 12 commits into
trunkfrom
943-seo-data-optimization-error-reporting-refactoring
Open

943 seo data optimization error reporting refactoring#23402
pls78 wants to merge 12 commits into
trunkfrom
943-seo-data-optimization-error-reporting-refactoring

Conversation

@pls78

@pls78 pls78 commented Jun 24, 2026

Copy link
Copy Markdown
Member

Context

When an unexpected error occurred while building an indexable during SEO data optimization, the failure surfaced as an opaque error: the user had no indication of which object could not be optimized. This PR carries the failing object's identity (type + ID) from the indexable builder all the way through to the WP-CLI output and the REST/React error UI, and gives third parties a hook to react to the failure.

Summary

This PR can be summarized in the following changelog entry:

  • Adds the failing object's type and ID to the SEO data optimization error report when an indexable cannot be built.

Relevant technical choices:

  • A new Indexing_Failed_Exception (extending Indexable_Exception) wraps the underlying throwable and carries the object's object_id, object_type, and object_sub_type.
  • The indexable builder logs the error and fires a new wpseo_indexable_indexing_failed action so third parties can react to a failed indexable build.
  • The JS indexation engine was extracted into a shared AbstractIndexation component so the Tools page and the first-time configuration share a single code path, with the render layer kept in thin subclasses.

Test instructions

Test instructions for the acceptance test before the PR gets merged

This PR changes what the user sees when an indexable cannot be built during SEO data optimization. To test it you need to (a) make sure indexing actually runs, (b) force a build failure, then (c) check the CLI output and the React error UI. Finally, confirm the normal (happy-path) optimization still works once the forced failure is removed.

Setup

  • Use a site with a handful of published posts.
  • Make sure the SEO optimization hasn't been performed (or reset the indexables with the Yoast Test Helper)

Force a build failure (temporary code change)

  • Open src/builders/indexable-post-builder.php and, as the very first line of public function build( $post_id, $indexable ) (around line 115), add:
    throw new \Exception( 'Simulated indexing failure' );
    Every post indexable will now throw an unexpected error, which Indexable_Builder::build() catches, logs, fires wpseo_indexable_indexing_failed for, and re-throws as Indexing_Failed_Exception — exactly the path this PR adds, with object type post and a real post ID.

Check the React error UI (Tools page)

  • Go to Yoast SEO → Tools and open your browser's developer console.
  • Start SEO data optimization ("Optimize SEO data" / "Start SEO data optimization").
  • An error alert appears. Click "Error details" to expand it and confirm the first line reads "Failing object" with the value post #{id} (the ID of the post that failed). The Request URL, Request method, Status code, Error message, and a collapsible "Error stack trace" should also be present.
  • Confirm there are no uncaught JavaScript errors in the console.
  • Repeat the test but this time via the FTC first step.

Check the WP-CLI output

  • Run the indexing command from the CLI: wp yoast index (in this worktree-based Docker setup, run it inside the cli container or via wp-env run cli wp yoast index).
  • Confirm it prints Could not optimize post #{id} while indexing posts: Simulated indexing failure and stops with an error, instead of an opaque failure.

(Optional) Third-party hook + log

  • Drop this into a mu-plugin and confirm it fires once per failed object:
    add_action( 'wpseo_indexable_indexing_failed', function ( $id, $type, $sub_type, $e ) {
        error_log( "Yoast indexing failed: {$type} #{$id}" . $e->getMessage() );
    }, 10, 4 );
    With WP_DEBUG_LOG enabled, the builder's own logger also writes the failure to debug.log.

Confirm the happy path still works (no regression)

  • Remove the temporary throw (and the optional hook).
  • Re-run SEO data optimization on the Tools page and confirm it completes successfully end to end.
  • Run the first-time configuration and confirm its indexing step still completes — it now shares the same engine (AbstractIndexation) as the Tools page.

Note on automated tests: the unit suites (composer test, yarn test), linting, and composer check-branch-cs all pass locally. The WordPress integration suite was run locally from a git worktree; the only failures (5) are environmental — they assert the canonical wordpress-seo plugin folder name but the worktree mounts the plugin under a different folder, so the asserted plugin URL/path differs. None touch the code in this PR. CI is the source of truth for integration tests.

Relevant test scenarios

  • Changes should be tested with the browser console open
  • Changes should be tested on different posts/pages/taxonomies/custom post types/custom taxonomies
  • Changes should be tested on different editors (Default Block/Gutenberg/Classic/Elementor/other)
  • Changes should be tested on different browsers
  • Changes should be tested on multisite
  • Browser console: with the console open, verify the error UI expands "Error details" and renders the "Failing object" line without throwing any JS errors.
  • Different object types: the "Failing object" line reports {object_type} #{object_id} for whichever indexable fails — to see a non-post type, move the temporary throw into the matching builder (e.g. indexable-term-builder.php for term #{id}).

Test instructions for QA when the code is in the RC

  • QA should use the same steps as above.

Forcing a build failure requires a temporary code change, so QA can focus on the happy path plus a visual check of the error UI:

  1. On a site with content, run the normal Yoast SEO → Tools → SEO data optimization flow to completion and confirm it still succeeds end to end (no regression in the happy path).
  2. Run the first-time configuration and confirm its indexing step still runs and completes — it now shares the same engine as the Tools page.
  3. Confirm the wp yoast index WP-CLI command still indexes content normally and reports completion.
  4. If a reproducible indexing failure can be produced (see the developer steps above — add a temporary throw in indexable-post-builder.php), confirm the Tools-page error alert's "Error details" shows a "Failing object" line as post #{id}, and that wp yoast index prints Could not optimize post #{id} while indexing posts: …. Remove the temporary change afterward.

Impact check

This PR affects the following parts of the plugin, which may require extra testing:

  • The indexable building path (Indexable_Builder) — now logs, fires wpseo_indexable_indexing_failed, and re-throws as Indexing_Failed_Exception on unexpected errors.
  • The wp yoast index WP-CLI command and the indexing REST route — both now handle the new exception.
  • The SEO data optimization UI on the Tools page and the first-time configuration indexing step — both refactored onto the shared AbstractIndexation engine, so the happy-path indexing flow should be regression-tested in both places.

Other environments

  • This PR also affects Shopify. I have added a changelog entry starting with [shopify-seo], added test instructions for Shopify and attached the Shopify label to this PR.
  • This PR also affects Yoast SEO for Google Docs. I have added a changelog entry starting with [yoast-doc-extension], added test instructions for Yoast SEO for Google Docs and attached the Google Docs Add-on label to this PR.

Documentation

  • I have written documentation for this change. For example, comments in the Relevant technical choices, comments in the code, documentation on Confluence / shared Google Drive / Yoast developer portal, or other.

Quality assurance

  • I have tested this code to the best of my abilities.
  • During testing, I had activated all plugins that Yoast SEO provides integrations for.
  • I have added unit tests to verify the code works as intended.
  • If any part of the code is behind a feature flag, my test instructions also cover cases where the feature flag is switched off.
  • I have written this PR in accordance with my team's definition of done.
  • I have checked that the base branch is correctly set.
  • I have run grunt build:images and committed the results, if my PR introduces or edits images or SVGs.

Innovation

  • No innovation project is applicable for this PR.
  • This PR falls under an innovation project. I have attached the innovation label.
  • I have added my hours to the WBSO document.

Fixes #943

@pls78 pls78 added the changelog: enhancement Needs to be included in the 'Enhancements' category in the changelog label Jun 24, 2026
@coveralls

coveralls commented Jun 24, 2026

Copy link
Copy Markdown

Coverage Report for CI Build 908

Warning

Build has drifted: This PR's base is out of sync with its target branch, so coverage data may include unrelated changes.
Quick fix: rebase this PR. Learn more →

Warning

No base build found for commit 5c5487d on trunk.
Coverage changes can't be calculated without a base build.
If a base build is processing, this comment will update automatically when it completes.

Coverage: 45.965%

Details

  • Patch coverage: 9 uncovered changes across 3 files (133 of 142 lines covered, 93.66%).

Uncovered Changes

File Changed Covered %
packages/js/src/components/AbstractIndexation.js 55 48 87.27%
packages/js/src/first-time-configuration/tailwind-components/steps/indexation/indexation.js 1 0 0.0%
src/commands/index-command.php 15 14 93.33%
Total (8 files) 142 133 93.66%

Coverage Regressions

Requires a base build to compare against. How to fix this →


Coverage Stats

Coverage Status
Relevant Lines: 61458
Covered Lines: 29617
Line Coverage: 48.19%
Relevant Branches: 11943
Covered Branches: 4122
Branch Coverage: 34.51%
Branches in Coverage %: Yes
Coverage Strength: 7.12 hits per line

💛 - Coveralls

@pls78 pls78 force-pushed the 943-seo-data-optimization-error-reporting-refactoring branch from 600e5a2 to 3ecba73 Compare June 29, 2026 14:09
@pls78 pls78 marked this pull request as ready for review June 29, 2026 14:36
@github-actions

github-actions Bot commented Jul 3, 2026

Copy link
Copy Markdown

A merge conflict has been detected for the proposed code changes in this PR. Please resolve the conflict by either rebasing the PR or merging in changes from the base branch.

Copilot AI 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.

Pull request overview

This PR improves SEO data optimization/indexing failure reporting by propagating the failing object’s identity (type + ID) from the PHP indexable build layer through to WP-CLI output and REST/React error UI, and refactors the JS indexation engine into a shared base component.

Changes:

  • Introduces Indexing_Failed_Exception to wrap unexpected build failures while carrying failing object metadata, and emits a new wpseo_indexable_indexing_failed action.
  • Extends REST (Indexing_Route) and WP-CLI (Index_Command) error handling to surface the failing object (type + ID) in responses/output.
  • Refactors JS indexation into AbstractIndexation and centralizes error-details rendering in IndexingErrorContent, with updated unit/Jest coverage.

Reviewed changes

Copilot reviewed 22 out of 22 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
tests/Unit/Routes/Indexing_Route_Test.php Adds unit coverage for turning Indexing_Failed_Exception into a REST WP_Error.
tests/Unit/Exceptions/Indexable/Indexing_Failed_Exception_Test.php Adds unit coverage for the new exception’s stored metadata and message.
tests/Unit/Commands/Index_Command_Test.php Adds unit coverage for WP-CLI output including failing object details.
tests/Unit/Builders/Indexable_Builder/Save_Indexable_Test.php Updates builder tests for the new constructor dependency (logger) and improves phpdoc typing.
tests/Unit/Builders/Indexable_Builder/Maybe_Build_Author_Indexable_Test.php Updates builder tests for the new constructor dependency (logger).
tests/Unit/Builders/Indexable_Builder/Is_Type_With_No_Id_Test.php Updates builder tests for the new constructor dependency (logger) and corrects provider docblock.
tests/Unit/Builders/Indexable_Builder/Ensure_Indexable_Test.php Updates builder tests for the new constructor dependency (logger).
tests/Unit/Builders/Indexable_Builder/Build_Test.php Adds coverage for logging + action firing + wrapped exception rethrow on unexpected build errors.
tests/Unit/Builders/Indexable_Builder/Abstract_Indexable_Builder_TestCase.php Adds a logger mock to the shared builder test setup; improves phpdoc typing.
src/routes/indexing-route.php Converts Indexing_Failed_Exception into WP_Error with stack trace and failing object metadata.
src/exceptions/indexable/indexing-failed-exception.php Adds the new exception class to carry failing object identity and wrap the underlying throwable.
src/commands/index-command.php Enhances WP-CLI indexing to halt with a clearer error including failing object info.
src/builders/indexable-builder.php Logs unexpected build failures, fires wpseo_indexable_indexing_failed, and rethrows as Indexing_Failed_Exception.
packages/js/tests/indexation.test.js Adds Jest coverage for showing the failing object in the error details UI.
packages/js/src/first-time-configuration/tailwind-components/steps/indexation/indexing-error.js Reuses shared IndexingErrorContent for consistent error-details rendering.
packages/js/src/first-time-configuration/tailwind-components/steps/indexation/indexation.js Refactors FTC indexation UI to extend shared AbstractIndexation.
packages/js/src/errors/RequestError.js Extends RequestError to carry failing object metadata.
packages/js/src/components/IndexingErrorContent.js Introduces shared error-details renderer, including a “Failing object” line.
packages/js/src/components/IndexingError.js Refactors to use shared IndexingErrorContent.
packages/js/src/components/Indexation.js Refactors Tools-page indexation UI to extend AbstractIndexation.
packages/js/src/components/AbstractIndexation.js Introduces shared indexation request/state engine and shared propTypes/defaultProps.
composer.json Updates YoastCS error threshold value.

Comment on lines +49 to +59
parent::__construct(
\sprintf(
/* translators: 1: indexable object type; 2: object ID; 3: underlying error message. */
'Yoast SEO could not build the %1$s indexable for object %2$d: %3$s',
$object_type,
$object_id,
$previous->getMessage(),
),
0,
$previous,
);

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 is valid feedback

Comment on lines +519 to +537
/**
* Tests that a failing object reported through an Indexing_Failed_Exception is turned into a
* WP_Error carrying that object's id and type.
*
* @covers ::run_indexation_action
*
* @return void
*/
public function test_index_posts_when_indexing_failed_exception_occurs() {
$exception = new Indexing_Failed_Exception( 123, 'post', 'post', new Exception( 'The underlying error.' ) );

$this->post_indexation_action->expects( 'index' )->once()->andThrow( $exception );

$this->indexing_helper->expects( 'indexing_failed' )->once()->withNoArgs();

Mockery::mock( WP_Error::class );

$this->assertInstanceOf( WP_Error::class, $this->instance->index_posts() );
}

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.

I'll leave this to @pls78 whether he wants to deal with that or not

Comment thread src/builders/indexable-builder.php
Comment on lines 141 to 143
}

Indexation.propTypes = {
indexingActions: PropTypes.object,
preIndexingActions: PropTypes.object,
indexingStateCallback: PropTypes.func,
};

Indexation.defaultProps = {
indexingActions: {},
preIndexingActions: {},
indexingStateCallback: () => {},
};

export default Indexation;

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.

I'll leave this to @pls78 whether he wants to deal with that or not

@leonidasmi leonidasmi 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.

CR: 🏗️

A couple of remarks aside from the inline ones:

  • object_id can be null for id-less types. For home-page, date-archive, post-type-archive, and system-page indexables object_id is null,
    • so the exception message renders "for object 0" and the CLI prints "Could not optimize system-page #0 while indexing general objects". I think we should improve that, for better UX (maybe the message could fall back to object_sub_type for those types?)
    • it also means that we should update the $object_id docblocks (in exception constructor and the action doc) to be int|null
  • There are cases where building an indexable means building an additional indexable during the initial build (eg. when building a post indexable, we also do $this->maybe_build_author_indexable( $indexable->author_id ).
    • Which means that if there's something wrong with building the second/inner indexable, we will initiate the improvements of this PR twice. Specifically:
      • a second log entry, a second action fire (now with the post's identity and the already-wrapped exception as $exception), and a message like "could not build the post indexable for object 5: Yoast SEO could not build the user indexable for object 3: …".
    • There's an easy fix where we can add a catch ( Indexing_Failed_Exception $exception ) before catch ( Throwable $exception ), in the builder
    • if we decide to do that, let's also add test instructions for that case

Also, let's add test instructions for the Logger addition, since support might use this PR for future reference and I suspect that they are gonna use our Logger for troubleshooting :)

Comment on lines +442 to +458
/**
* Fires when an indexable could not be built because of an unexpected error.
*
* This action lets third parties observe build failures themselves.
*
* @param int $object_id The object ID of the indexable that failed to build.
* @param string $object_type The object type of the indexable that failed to build.
* @param string|null $object_sub_type The object sub type of the indexable that failed to build.
* @param Throwable $exception The error that caused the failure.
*/
\do_action(
'wpseo_indexable_indexing_failed',
$indexable->object_id,
$indexable->object_type,
$indexable->object_sub_type,
$exception,
);

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.

Nice addition!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

changelog: enhancement Needs to be included in the 'Enhancements' category in the changelog

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants