From b6e0509ee0add2543c6840c09342980a1b921d9f Mon Sep 17 00:00:00 2001 From: Christian Gastrell Date: Fri, 8 May 2026 15:20:51 -0300 Subject: [PATCH] Search Dashboard: harden plan-info renders against missing API data MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Search dashboard renders `` → `` → `` and `` whenever `isNewPricing && supportsInstantSearch`, and these components reach into the redux `sitePlan` state without fully guarding against pieces being undefined while the data resolves. Three pre-existing landmines: - `getLatestMonthRequests` selector: `state.sitePlan.plan_usage?.num_requests_3m[ 0 ]` had an unguarded `[0]` after the optional chain — when `plan_usage` (or `num_requests_3m`) is undefined, accessing `undefined[0]` throws "Cannot read properties of undefined (reading '0')". Add the missing `?.` between `num_requests_3m` and `[0]`. - `displayPeriodFromAPIData` in plan-summary.jsx: `apiData.latestMonthRequests.start_date` blew up the same way when `latestMonthRequests` was undefined. Bail to `null` if either date is missing; the `` `

` now renders without the date span when data isn't available yet. - `usageInfoFromAPIData` in plan-usage-section.jsx: `apiData?.currentPlan.monthly_search_request_limit` was missing the `?.` on `.currentPlan` (the other three fields above it had it). Same crash class. Add the `?.`. These bugs predate this PR and are independent of the notice migration. They surface intermittently in CI because the JN test site's API population timing varies — the dashboard mounts and the selector evaluates before the API data resolves. Without the fix, the React tree blows up and the dashboard renders blank, which is why Search e2e (`Title should be visible` looking for `

` in `MockedSearchContent`) was failing. Verified locally: with these guards, the dashboard renders cleanly with no console errors even when `plan_usage` is empty. --- .../changelog/fix-search-dashboard-harden-plan-info-renders | 4 ++++ .../dashboard/components/pages/sections/plan-summary.jsx | 6 +++++- .../components/pages/sections/plan-usage-section.jsx | 2 +- .../search/src/dashboard/store/selectors/site-plan.js | 2 +- 4 files changed, 11 insertions(+), 3 deletions(-) create mode 100644 projects/packages/search/changelog/fix-search-dashboard-harden-plan-info-renders diff --git a/projects/packages/search/changelog/fix-search-dashboard-harden-plan-info-renders b/projects/packages/search/changelog/fix-search-dashboard-harden-plan-info-renders new file mode 100644 index 000000000000..7817e26ce17a --- /dev/null +++ b/projects/packages/search/changelog/fix-search-dashboard-harden-plan-info-renders @@ -0,0 +1,4 @@ +Significance: patch +Type: fixed + +Search Dashboard: guard `` and `` against missing wpcom plan fields so the dashboard React tree no longer throws "Cannot read properties of undefined" when `state.sitePlan.plan_usage` or `plan_current` is partially populated. Three landmines: `getLatestMonthRequests` selector's unguarded `[ 0 ]` after the `?.`; `displayPeriodFromAPIData` accessing `latestMonthRequests.start_date` without nullish guard; and the missing `?.` on `currentPlan.monthly_search_request_limit` in `usageInfoFromAPIData`. The dashboard now renders cleanly even when the wpcom plan response hasn't fully resolved. diff --git a/projects/packages/search/src/dashboard/components/pages/sections/plan-summary.jsx b/projects/packages/search/src/dashboard/components/pages/sections/plan-summary.jsx index 12410aa7a1de..f7b28fe83b9a 100644 --- a/projects/packages/search/src/dashboard/components/pages/sections/plan-summary.jsx +++ b/projects/packages/search/src/dashboard/components/pages/sections/plan-summary.jsx @@ -8,6 +8,9 @@ const getPlanName = isFreePlan => { }; const displayPeriodFromAPIData = apiData => { + if ( ! apiData?.latestMonthRequests?.start_date || ! apiData?.latestMonthRequests?.end_date ) { + return null; + } const startDate = new Date( apiData.latestMonthRequests.start_date ); const endDate = new Date( apiData.latestMonthRequests.end_date ); @@ -26,6 +29,7 @@ const displayPeriodFromAPIData = apiData => { }; const PlanSummary = ( { isFreePlan, planInfo } ) => { + const period = displayPeriodFromAPIData( planInfo ); return (

{ @@ -33,7 +37,7 @@ const PlanSummary = ( { isFreePlan, planInfo } ) => { __( 'Your usage', 'jetpack-search-pkg' ) }{ ' ' } - { displayPeriodFromAPIData( planInfo ) } ({ getPlanName( isFreePlan ) }) + { period && `${ period } ` }({ getPlanName( isFreePlan ) })

); diff --git a/projects/packages/search/src/dashboard/components/pages/sections/plan-usage-section.jsx b/projects/packages/search/src/dashboard/components/pages/sections/plan-usage-section.jsx index d6690555e4c3..ac2279194ad8 100644 --- a/projects/packages/search/src/dashboard/components/pages/sections/plan-usage-section.jsx +++ b/projects/packages/search/src/dashboard/components/pages/sections/plan-usage-section.jsx @@ -16,7 +16,7 @@ const usageInfoFromAPIData = apiData => { recordCount: apiData?.currentUsage?.num_records || 0, recordMax: apiData?.currentPlan?.record_limit || 0, requestCount: apiData?.latestMonthRequests?.num_requests || 0, - requestMax: apiData?.currentPlan.monthly_search_request_limit || 0, + requestMax: apiData?.currentPlan?.monthly_search_request_limit || 0, }; }; diff --git a/projects/packages/search/src/dashboard/store/selectors/site-plan.js b/projects/packages/search/src/dashboard/store/selectors/site-plan.js index 0f0401f8a3e0..f7bf283c533f 100644 --- a/projects/packages/search/src/dashboard/store/selectors/site-plan.js +++ b/projects/packages/search/src/dashboard/store/selectors/site-plan.js @@ -11,7 +11,7 @@ const sitePlanSelectors = { state.sitePlan.supports_instant_search || state.sitePlan.supports_only_classic_search, getTierMaximumRecords: state => state.sitePlan.tier_maximum_records, isFreePlan: state => state.sitePlan.effective_subscription?.product_slug === productSlugFree, - getLatestMonthRequests: state => state.sitePlan.plan_usage?.num_requests_3m[ 0 ], + getLatestMonthRequests: state => state.sitePlan.plan_usage?.num_requests_3m?.[ 0 ], getCurrentPlan: state => state.sitePlan.plan_current, getCurrentUsage: state => state.sitePlan.plan_usage, };