google / google/site-kit-wp

Build the wp-admin menu count badge + its global count bundle

Open
#13,359 4 comments 0 reactions 0 assignees View on GitHub
P0 Team M Type: Enhancement
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

Open the contributing 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.