Build the wp-admin menu count badge + its global count bundle
- Dominant language
- JavaScript
- Stars
- 1.4k
- Forks
- 383
- Avg merge
- 4d 14h
- Merged PRs (30d)
- 77
Description
## Feature Description
WordPress has a long-established way of telling an admin there's something waiting for them: a small count bubble on the menu item, as plugin updates and pending comments use. The hub's **Add Features** menu item (#13244) uses the same convention — a count of the features that are new to this user — and, crucially, shows it wherever the user happens to be in wp-admin, not only on Site Kit's own pages. That's the point of it: it reaches a user who isn't looking at Site Kit at all.
What makes this more than a menu tweak is where the number comes from. What counts as new is per user, and whether a feature is already set up is a live check against the user's services (#13246, #13247), so the count can only be worked out where Site Kit's data layer is running. So Site Kit works it out on the pages where it already runs — the hub, both dashboards, and the Site Kit widget on the WordPress dashboard, which is the page WordPress lands an admin on when they sign in — remembers it in the browser, and a very small script on every admin screen renders what was remembered. One place decides the number, one place draws it, and no rule about newness is duplicated anywhere.
The trade-off is deliberate: the count is as fresh as the last time the user was on a Site Kit page. When something has happened that could have changed it — Site Kit updating and bringing new features with it, or another administrator connecting a service — the remembered number can no longer be trusted, and the menu item shows a plain dot with no number until it's worked out again. A dot still says *there's something new here*, without asserting a number that might be wrong.
For reference, see [The global count bundle](https://docs.google.com/document/d/1sLWcimi6eZqbK4YXVCZtO0ZrfrDV7Ub218Hu_vpwmyI/edit?tab=t.0#heading=h.ovzfhdo03hja) and [Entry points & new-feature indicators](https://docs.google.com/document/d/1sLWcimi6eZqbK4YXVCZtO0ZrfrDV7Ub218Hu_vpwmyI/edit?tab=t.0#heading=h.iwgxt6wulwk) in the design doc, and the [menu badge](https://www.figma.com/design/7gBBIQhrIvLicLinAt9vta/Feature-Discovery-Hub?node-id=756-19764) in Figma.
---------------
_Do not alter or remove anything below. The following sections will be managed by moderators only._
## Acceptance criteria
- The **Add Features** item in the Site Kit menu (#13244) carries a count bubble showing how many features are new to this user and not yet seen (#13246) — the same number behind the dashboard header pill's dot (#13358).
- The bubble uses WordPress's own menu count styling, as plugin updates and pending comments do, and its meaning is available to screen readers.
- The bubble is shown on every wp-admin screen, not only Site Kit's own pages.
- When the count is zero, no bubble is shown.
- The count is worked out and remembered whenever the user loads a page where Site Kit derives it: the hub, the main dashboard, the entity dashboard, and the Site Kit widget on the WordPress dashboard.
- The remembered count is used only while it still matches the plugin version and the site's service connection state it was worked out against. If either has changed since — Site Kit was updated, or another administrator connected a service — the item shows a plain dot with no number, until the count is next worked out on one of those pages.
- Visiting **What's new?** (#13321) clears the count, and any other wp-admin tabs the user has open update to match without needing to be reloaded.
- Logging out clears the remembered count, so the next person to use that browser doesn't see it.
- The count is remembered per browser, so a user signing in on a second browser sees no bubble there until they load a Site Kit page.
- The badge is loaded only for administrators, and only when the `featureDiscoveryHub` feature flag is enabled — so only users who have an **Add Features** item to badge carry any of this.
- What's added to every admin screen is deliberately minimal: it reads the remembered count and renders the bubble. It loads no Site Kit data stores, no React, and makes no API requests.
- If the browser can't store the count, no bubble is shown and nothing else is affected.
## Implementation Brief
- [x] In `includes/Core/Admin/Screens.php`:
- For the `googlesitekit-features` screen, add the following to the `menu_title`:
```html
```
- Use `__( 'Add Features %s', 'google-site-kit' )` and `sprintf()` for the appending, for consistency with [core's example](https://github.com/WordPress/WordPress/blob/c1953621997242eedcafff0eae1002ac44a98192/wp-admin/menu.php#L395).
- [x] Create `assets/js/util/features-badge.ts`:
- Define const `FEATURE_COUNT_CACHE_KEY` – `googlesitekit::feature-count`.
- Define const `FEATURE_COUNT_CHANNEL_NAME` – `googlesitekit::feature-count`.
- Define const `FEATURE_COUNT_CHANNEL_MESSAGES.UPDATED` – `'updated'`
- Define interface `FeatureCountFingerprint` – `{ connectedModules: string[], pluginVersion: string, userID: number }`.
- Define interface `FeatureCountCache` – `extends FeatureCountFingerprint { count: number }`.
- Export function `isFeatureCountCache( value: unknown ): value is FeatureCountCache`:
- Check `value` is an object with properties consistent with a `FeatureCountCache`.
- `value is FeatureCountCache` is a type predicate, which helps TypeScript recognise that any `value` that passes the check is a `FeatureCountCache`.
- Define function `getFeatureCountCache(): FeatureCountCache | null`:
- Get cache from local storage with `FEATURE_COUNT_CACHE_KEY`.
- Parse JSON and check `isFeatureCountCache()`.
- Return result or `null` on parsing error or check failure.
- Export function `clearFeatureCountCache()`:
- Clear cache from local storage with `FEATURE_COUNT_CACHE_KEY`.
- Post `FEATURE_COUNT_CHANNEL_MESSAGES.UPDATED` as a message on a `BroadcastChannel` for `FEATURE_COUNT_CHANNEL_NAME`.
- Export function `setFeatureCountCache( featureCountCache: FeatureCountCache )`:
- Check `isFeatureCountCache()` and stringify.
- Return on error or check failure.
- Set cache in local storage with `FEATURE_COUNT_CACHE_KEY`.
- Post `FEATURE_COUNT_CHANNEL_MESSAGES.UPDATED` as a message on `FEATURE_COUNT_CHANNEL_NAME`.
- Export function `renderFeaturesBadge( count: number, showCount: boolean )`:
- Find `.googlesitekit-features-badge` element.
- Return if it doesn't exist.
- Set text content of the `.count` element to `count` if `showCount`, otherwise an empty string.
- Replace the existing `count-*` class with `count-${count}`.
- Set text content of the `screen-reader-text` element with:
- If `showCount` – `_n( '%d new feature', '%d new features', count, 'google-site-kit' )`
- Otherwise `__( 'new features', 'google-site-kit' )`.
- Export function `renderFeaturesBadgeFromCache( fingerprint: FeatureCountFingerprint )`:
- Get the cache with `getFeatureCountCache()`.
- If one is found:
- Call `clearFeatureCountCache()` and return if `fingerprint.userID` doesn't match the cache's `userID`.
- Call `renderFeaturesBadge()` with the cache count, and pass `showCount` as `true` if the fingerprint version and connected modules match the cache's.
- Otherwise call `renderFeaturesBadge( 0, false )`.
- Make sure to `try`/`catch` with `localStorage` and `BroadcastChannel`, failing silently.
- [ ] Create `assets/js/components/feature-discovery/useFeatureCountCache.ts`:
- Export hook `useFeatureCountCache()`:
- Keep track of whether `getModules` has finished resolution.
- Get `connectedModules` by selecting `CORE_MODULES.getModules()` and using `CORE_MODULES.isModuleConnected()`.
- Get `count` from `CORE_FEATURE_DISCOVERY.getNewFeatureCount()`.
- Get `pluginVersion` from `global.GOOGLESITEKIT_VERSION`.
- Get `userID` from `CORE_USER.getID()`.
- Call `setFeatureCountCache()` with those values in an effect if selectors are resolved and `count` is not `undefined`.
- [x] In each of the following components, import and call `useFeatureCountCache()`:
- `assets/js/components/DashboardMainApp.js`
- `assets/js/components/DashboardEntityApp.js`
- `assets/js/components/feature-discovery/FeatureDiscoveryApp.tsx`
- `assets/js/components/wp-dashboard/WPDashboardApp.js`
- [x] Create `assets/js/googlesitekit-features-badge.ts`:
- Import `clearFeatureCountCache`, `FEATURE_COUNT_CHANNEL_NAME`, `renderFeaturesBadgeFromCache`.
- Get `connectedModules`, `pluginVersion`, `resetSession`, `userID` from `_googlesitekitFeaturesBadgeData`.
- Define `featureCountChannel` – `new BroadcastChannel( FEATURE_COUNT_CHANNEL_NAME )`.
- Define function `onMessage( event: MessageEvent )`:
- If the message is `FEATURE_COUNT_CHANNEL_MESSAGES.UPDATED`, call `renderFeaturesBadgeFromCache({ connectedModules, pluginVersion, userID })`.
- Define function `setupFeaturesBadge()`:
- Bind `onMessage` to the `message` event on a `BroadcastChannel` for `FEATURE_COUNT_CHANNEL_NAME`.
- Call `clearFeatureCountCache()` if `resetSession`.
- Call `renderFeaturesBadgeFromCache({ connectedModules, pluginVersion, userID })`.
- Call `setupFeaturesBadge()`.
- [x] In `includes/Core/Modules/Modules.php`:
- In `register()` add a filter to `googlesitekit_connected_modules` that returns `$this->get_connected_modules()`.
- [x] In `includes/Core/Assets/Assets.php`:
- Add a `get_inline_features_badge_data()` method returning:
- `connectedModules` – Slugs of modules from `Modules::get_connected_modules()` by applying the `googlesitekit_connected_modules` filter added above to an empty array.
- `pluginVersion` – `GOOGLESITEKIT_VERSION`.
- `resetSession` – Whether the `googlesitekit_reset_session` URL parameter is true.
- `userID` – `get_current_user_id()`.
- In `get_assets()`, if the user has `Permissions::MANAGE_OPTIONS` and `featureDiscoveryHub` is enabled:
- Add `googlesitekit-features-badge-data` as a `Script_Data`:
- `global` – `_googlesitekitFeaturesBadgeData`.
- `data_callback` – Return `get_inline_features_badge_data()`.
- Add `assets/js/googlesitekit-features-badge.ts` as a `Script`:
- `dependencies` – `googlesitekit-i18n` and `googlesitekit-features-badge-data`.
- `load_contexts` – `Asset::CONTEXT_ADMIN_GLOBAL`.
- In `register()`:
- Add an action to `admin_enqueue_scripts` that enqueues scripts with `Asset::CONTEXT_ADMIN_GLOBAL`, like the existing `enqueue_block_assets` and `enqueue_block_editor_assets` examples.
- [x] In `assets/webpack/basicModules.config.js`
- Add an entry for `googlesitekit-features-badge`.
- [x] In `assets/js/types/globals.d.ts`
- Type `_googlesitekitFeaturesBadgeData`.
### Test Coverage
- Add unit test coverage for:
- `features-badge.ts`, checking cache validation, rendering results and fingerprint comparisons.
- `useFeatureCountCache.ts`, checking the cache updates when expected.
- `googlesitekit-features-badge.ts`, checking event handling.
- Update unit test coverage for `Core\Assets` to check `get_inline_features_badge_data()` returns the expected data.
## QA Brief
- Set up a new Site Kit site and enable the `featureDiscoveryHub` feature flag.
- Connect a second admin account to Site Kit. Log in as the original account before proceeding.
- In the Site Kit admin sub-menu an _Add Features_ item should be visible, without any count or badge.
- With the tester plugin set _Force initial Site Kit plugin version_ to `1.84.0`.
- Navigate to _Pages > All Pages_. Hover over the _Site Kit_ menu item, no badge should appear.
- Navigate to _Site Kit > Dashboard_. A "5" badge should appear next to _Add _Features_.
- Navigate back to _Pages > All Pages_. The "5" badge should persist.
- Keep that page open and in an incognito window, or another browser, log in and connect Analytics. Once Analytics is fully connected the badge should read "8".
- Return to the original browser and refresh. Hover over the _Site Kit_ admin menu item. In the sub menu a dot should be shown next to _Add Features_ with no number.
- Navigate to _Posts > All Posts_. The numberless dot should persist.
- Navigate to _Dashboard > Home_, the WordPress dashboard. The number should update to "8".
- Open a new a tab in the same window. In that window disconnect Analytics. The badge should become a numberless dot. Navigate to _Site Kit > Dashboard_ and it should return to "5".
- Return to the original tab and hover over the Site Kit admin menu. The badge should have changed to a numberless dot. Refresh and it should update to "5".
- Navigate to _Site Kit > Dashboard_, open a new tab and re-connect Analytics, when returning to the dashboard the number should update to "8". Return to the original tab and the number should also have updated to "8".
- Log out of WordPress and attempt to visit `/wp-admin/edit.php` (to ensure a login redirect to a non-Site Kit page). Log in and hover over the Site Kit admin menu and the badge should read "8".
- Log out and and attempt to visit `/wp-admin/edit.php` again, but log in as the second admin user. The badge should not appear.
- Navigate to _Site Kit > Dashboard_ and the number should update to "8".
- Open a second tab to _Posts > All Posts_. Return to the tab on the Site Kit dashboard.
- In the browser console, run `googlesitekit.data.dispatch( 'core/feature-discovery' ).markFeaturesSeen( [ 'email-reports' ] );`. The number should update to "7" in both tabs.
- Log in as the original admin and return to the Site Kit dashboard. The number should be "8".
## Changelog entry
-
Contributor guide
Research direction
Start with the unchecked useFeatureCountCache.ts brief and compare the existing feature discovery components, then review the feature-count tests and Core\Assets coverage. Implement the remaining hook and tests, ensuring the cache updates only after module selectors resolve; done means the listed unit coverage passes and the global badge behavior meets the acceptance criteria.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- javascript, php, typescript, wordpress
- Domain
- backend, frontend, testing
- Issue type
- Feature
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Clearly specified
- Newbie friendliness
- 70/100