Repository navigation
Implement the shared setup-flow mechanism + the return to the hub #13338
Description
Feature Description
Most of the features the hub lists are set up in a flow Site Kit already has: the user leaves the hub for the service's own setup screens, and often for Google's, and comes back when it's done. The hub adds no setup UI of its own for these — clicking its call to action should start exactly the flow the user would get from Settings' Connect more services tab today.
Two things make that more than a link. The first is the handoff itself: the service has to be activated before the user can be sent into its setup. The second is the trip back. Every one of these flows finishes by sending the user to the dashboard, which is the right place when they started there — but a user who came from the hub to set up one feature has more they want to do, and dropping them on the dashboard makes them find their way back. So the hub records that it launched the setup, and a flow that finds such a record completes to the hub instead, on the tab the user started from.
This issue builds that shared path — the handoff, the record, and the return for every module that completes through Site Kit's common module-setup completion. It covers the features that are a plain service activation: Analytics, Ads, AdSense, PageSpeed Insights, Sign in with Google, and Reader Revenue Manager's full setup. The flows that finish somewhere of their own, and so have to consult the record themselves, are covered by #13340; the Reader Revenue Manager newsletter feature and Key Metrics have their own issues, #13345 and #13348. An activation that fails before the user leaves the hub is reported by #13339.
Everywhere else that starts one of these setups — a dashboard banner, Settings, anywhere that isn't the hub — is unaffected: the hub is an override, never a new default.
For reference, see the setup-flow — navigate away to a Site Kit setup flow and Returning to the hub sections in the design doc.
Do not alter or remove anything below. The following sections will be managed by moderators only.
Acceptance criteria
- Clicking a feature's call to action (Build
FeatureCTAwith gating rules #13322) where that feature is set up in an existing setup flow activates the feature's Google service and takes the user into that service's setup, exactly as Settings' Connect more services tab does today — for Analytics, Ads, AdSense, Sign in with Google, and Reader Revenue Manager (full setup). - PageSpeed Insights is activated immediately, with nothing to configure, so the user stays on the hub. The confirmation they see is covered by Implement in-place success detection & notice #13346.
- Before the user is taken away, the hub records which feature they set out to set up and which tab they were on. The record belongs to the browser tab they left from, survives signing in to Google, and expires after an hour.
- Completing the setup returns the user to the hub, on the tab they started from, instead of the dashboard it goes to today. This covers every module that finishes through Site Kit's shared module-setup completion: AdSense, Sign in with Google, Reader Revenue Manager, and Ads where it doesn't use the PAX flow.
- Otherwise every flow keeps the destination it has today: where there's no record, where it's expired, or where it belongs to some other setup the user left unfinished rather than the one that just completed, the flow completes exactly where it does now.
- Landing back on the hub uses the record up, so the user is routed once: reloading the hub, or coming back to it later, doesn't route them again. A user who abandoned the setup and opens the hub afterwards simply gets it in its normal state.
- Where the service can't be activated, so the user never leaves the hub, the failure is reported on the feature's card — covered by Implement the shared setup-failure notice #13339.
- Where the browser has no storage for the record, setup still works: the feature is set up and the user completes to the dashboard as today.
- The flows that finish on a screen of their own — Analytics' Key Metrics step, the Ads PAX flow, the Ad Blocking Recovery screen, the Key Metrics questionnaire, and Reader Revenue Manager's express route — are untouched here, and covered by Return the user to the hub from bespoke completion handlers #13340 and their own features' issues, Implement setup for the Key Metrics feature #13348 and Implement setup for the RRM newsletter sign-up feature #13345.
- The success confirmation shown on return is covered by Implement the top-of-list success notice on return #13356: this issue lands the user on the right tab and shows nothing there.
Implementation Brief
-
In
assets/js/googlesitekit/feature-discovery/, createpending-setup.ts:- Define a cache key constant,
FEATURE_DISCOVERY_SETUP_CACHE_KEY = 'feature_discovery_setup'— the hub's hash-route segments (the tab components themselves land in#13321/#13328). - Export
getCurrentFeatureDiscoveryTab(): parsesglobal.location.hash(#/whats-new,#/all-services) and falls back toAll Servicestab for anything else. - Export
setPendingSetup( featureSlug: string, returnTab: string ), wrappingsetItem( FEATURE_DISCOVERY_SETUP_CACHE_KEY, { featureSlug, returnTab } )from@/js/googlesitekit/api/cache. Use the helper's default TTL (HOUR_IN_SECONDS) rather than passing one. - Export
consumePendingSetup(): Promise<{ featureSlug: string; returnTab: string } | null>: reads viagetItem()then deletes viadeleteItem(), returningvalueon a cache hit ornullotherwise. This read-and-delete is what makes sure the record can only ever be consumed once — called from exactly one place,FeatureDiscoveryApp's mount effect described below. - Export
getPendingSetupReturnURL( select: Select, featureSlug: string ): Promise<string | undefined>— read the record viagetItem()only (nodeleteItem(), so it's left in place forconsumePendingSetup()to pick up once the hub actually mounts), and return the plain hub admin URL,select( CORE_SITE ).getAdminURL( 'googlesitekit-features' ), only whererecord.featureSlug === featureSlug; otherwise returnundefined. Matching directly on the feature slug — rather than resolving it through the catalog — is what lets this be called with whatever feature slug the caller already has in hand, so#13340's bespoke completion handlers can use it as-is. Stopping a stale record left by some other, abandoned hub setup from hijacking this one falls out of the same comparison. The URL carries no tab hash — the tab is restored from the consumed record onceFeatureDiscoveryAppmounts. - Model this on the existing
module_setupcache item — written byassets/js/components/settings/SetupModule.jsand cleared byassets/js/components/setup/hooks/useFinishSetup.ts— but keep it in its own file/key since it carries a feature slug and a tab, not a module slug.
- Define a cache key constant,
-
In
assets/js/googlesitekit/datastore/feature-discovery/features.ts, implement thesetupFeature( slug )generator action (started as a placeholder in#13322):- Resolve the feature via
getFeature( slug )and return{}unlessfeature.setup.type === FEATURE_SETUP_TYPES.SETUP_FLOW— thebackground-toggleandin-place-paneltypes are built by their own issues. - Where
feature.setup.moduleSlugis set (Analytics, Ads, AdSense, PageSpeed Insights, Sign in with Google, Reader Revenue Manager full setup):- Dispatch
CORE_MODULESactivateModule( moduleSlug ). - On
{ error }, return it as-is and stop — surfacing it on the card is#13339's job, out of scope here. - On success, read
select( CORE_MODULES ).getModule( moduleSlug )?.SetupComponent. If it's falsy (e.g. PageSpeed Insights — no setup screen registered), then stop: the module is already connected, the user stays on the hub, and nothing is written or navigated to. - Otherwise, call
setPendingSetup( slug, getCurrentFeatureDiscoveryTabPath() ), then dispatchCORE_LOCATIONnavigateTo( response.moduleReauthURL ). Note that the pending-setup cache item should be set before navigating tomoduleReauthURL, — this is essential for the record to survive an OAuth round trip.
- Dispatch
- Import
CORE_MODULESfrom@/js/googlesitekit/modules/datastore/constantsandCORE_LOCATIONfrom@/js/googlesitekit/datastore/location/constants; use thecommonActions.getRegistry()/commonActions.await()pattern already used throughout the datastores (e.g.assets/js/googlesitekit/modules/datastore/modules.js). - Add the session-only state the hub reads once it mounts.
- A
receivePendingSetup( pendingSetup: { featureSlug: string; returnTab: string } | null )action and reducer case storing it understate.pendingSetup(initial statenull), plus agetPendingSetup( state )selector returning it.
- A
- Resolve the feature via
-
In
assets/js/components/feature-discovery/FeatureDiscoveryApp.tsx, once on mount:- Call
consumePendingSetup()and dispatch its result (a record ornull) toreceivePendingSetup()— this is the single place the record is ever consumed, so reloading or revisiting the hub afterwards can't route the user again.
- Call
-
In
assets/js/components/feature-discovery/FeatureDiscoveryContent.tsx:- Read
select( CORE_FEATURE_DISCOVERY ).getPendingSetup(). Where it names areturnTab, use that in place of the existing new-vs-returning-user calculation when computingdefaultPath, so the component's existing<Redirect to={ defaultPath } />fallback carries the user to the tab they set out from.
- Read
-
In
assets/js/components/feature-discovery/constants.ts- Add a small
moduleSlug→ feature slug lookup, covering just the plain module-activation features this issue handles:MODULE_SLUG_ANALYTICS_4→'analytics',MODULE_SLUG_ADS→'ads',MODULE_SLUG_ADSENSE→'adsense',MODULE_SLUG_PAGESPEED_INSIGHTS→'pagespeed-insights',MODULE_SLUG_SIGN_IN_WITH_GOOGLE→'sign-in-with-google',MODULE_SLUG_READER_REVENUE_MANAGER→'reader-revenue-manager' - As per this discussion, add the descriptive comment in the code that this mapping is a deliberate choice.
- Add a small
-
In
assets/js/components/setup/hooks/useFinishSetup.ts:- In the branch that runs when
finishSetup()is called with noredirectURL, before building the dashboardadminURL, look up this hook'smoduleSlugin above mapping table and, where it maps to a feature slug, callawait getPendingSetupReturnURL( registry.select, featureSlug ). - Where it resolves a URL,
navigateTo()it and return, skipping the dashboardadminURLbuild. - Where it resolves
undefined— no record, an expired one, or one belonging to a different feature — or wheremoduleSlugisn't in the lookup, fall through to today's dashboard destination unchanged. - Where browser storage isn't available,
getItem()'s existing cache-miss behaviour meansgetPendingSetupReturnURL()resolvesundefinedthe same way, so setup still completes — to the dashboard.
- In the branch that runs when
Test Coverage
- Add unit tests for
pending-setup.ts:setPendingSetup/consumePendingSetupround-tripping (including the read-and-delete/single-consumption behaviour), andgetPendingSetupReturnURL()matching, not matching, and finding no record — confirming it never deletes the item it reads. - Add/extend
features.test.tsforsetupFeature()(themoduleSlugactivation success, error, and no-SetupComponentpaths) and forreceivePendingSetup()/getPendingSetup(). - Add unit tests for the new mount hook, confirming
consumePendingSetup()is called exactly once per mount and its result reachesgetPendingSetup(). - Extend
FeatureDiscoveryContent's test coverage to confirm it redirects to the consumed record'sreturnTabin place of the computed default when one is present. - Extend
useFinishSetup.test.tsto cover: themoduleSlug→ feature slug lookup for each of the six mapped modules, returning to the hub on a matching pending-setup record, falling through to the dashboard for an unmappedmoduleSlugor a mismatched/absent/expired record, and confirming the existingredirectURLbranch is unaffected.
QA Brief
-
Prerequisites: the
featureDiscoveryHubfeature flag is enabled (Tester Settings → General → Enabled feature flags), Site Kit is connected, and at least one of AdSense or Sign in with Google is not yet set up. Use whichever you can complete: AdSense needs an AdSense account on the connected Google account, and Sign in with Google needs a Client ID (it opens Google in a new tab to create one; paste the ID back into the Site Kit tab). -
Go to Site Kit → Add Features and open the All services and features tab.
-
Click the CTA for a feature that isn't set up yet (for example Set up AdSense). Verify you're taken to that module's setup screen, and that its URL is
…page=googlesitekit-dashboard&slug=adsense&reAuth=true. -
Complete the setup. Verify you land back on the hub on the All services and features tab (URL ends
#/all-services), not on the dashboard. -
Reload the hub, and verify nothing redirects you again and you stay where you are.
-
Repeat from the What's new? tab: set Tester Settings → General → "Force initial Site Kit plugin version" to something old (for example
1.150.0) so features show as new, click the CTA from that tab, and complete the setup. Verify you return to the What's new? tab. Clear the forced version afterwards. -
If you can use an account with a publication, repeat with Set up Reader Revenue Manager (full setup). Verify the same return to the hub.
-
With PageSpeed Insights disconnected (Settings → Connected Services), click Set up PageSpeed Insights in the hub. Verify it's activated immediately, with nothing to configure, and you stay on the hub.
-
Verify setups started elsewhere still complete to the dashboard as before: Settings → Connect More Services → set up a module → complete → you land on the dashboard.
-
Verify the record belongs to the browser tab: start a setup from the hub, copy the setup screen's URL into a new tab, and complete the setup there. Verify you land on the dashboard.
-
Verify the record expires: start a setup from the hub, and before completing it run the snippet below in the console, then complete the setup. Verify you land on the dashboard.
const key = Object.keys( sessionStorage ).find( ( k ) => k.includes( 'feature_discovery_setup' ) ); const value = JSON.parse( sessionStorage[ key ] ); value.timestamp -= 3700; sessionStorage[ key ] = JSON.stringify( value );
-
The next two steps simulate damaged browser storage, which shouldn't happen in normal use. Verify a corrupted record doesn't stop the hub from loading: start a setup from the hub so you're on the module's setup screen, then run the snippet below in the console. Then go to Site Kit → Add Features. Verify the hub loads normally, on its usual default tab, with no blank area.
-
Verify a corrupted record doesn't stop a setup from completing: start a setup from the hub again, and on the setup screen run the same snippet. Then complete the setup. Verify you land on the dashboard, not on a blank page and not stuck on the setup screen.
const key = Object.keys( sessionStorage ).find( ( k ) => k.includes( 'feature_discovery_setup' ) ); sessionStorage[ key ] = '{';
-
If you can, verify the Google sign-in round trip: disconnect the module, then disconnect and reconnect your Google account in Site Kit (profile menu → Disconnect) so the module isn't active when you reconnect. Click the module's CTA in the hub. Verify you go to Google's permissions screen in the same tab, are returned to the module's setup screen, and on completing it land back on the hub on the tab you started from.
Notes for the tester
- Known limitation, not a defect for this issue: if you start a setup from the hub, abandon it (for example with Cancel), and then set up the same feature from Settings or the dashboard in the same tab within the hour, that setup returns to the hub instead of the dashboard. A follow-up issue has been created in A leftover hub record sends a later setup of the same feature, started elsewhere, back to the hub #13806.
Changelog entry
- Enable setting up the Analytics, Ads, AdSense, Sign in with Google, and Reader Revenue Manager features on the Feature Discovery Hub.
Activity
techanvil commentedon Sep 24, 2026
Hi @ankitrox, thanks for drafting this IB. A few points:
FEATURE_DISCOVERY_TABSandDEFAULT_TAB_PATHinfeature-discovery/constants.ts, I think we can use these instead of defining a new constant.getSetupURL()handling to be implemented in that issue, as the related features are out of scope here. As you've written the IB for that issue I've assigned it back to you - please move this aspect to its IB.module_setupcache item before navigating tomoduleReauthURL, as we do elsewhere when setting up a module (needed for the setup to survive an OAuth retry).22 remaining items
kelvinballoo commentedon Oct 12, 2026
QA Update⚠️
Tested this and I have a couple of feedback:
ITEM 1:
Whenever, I set up a module:
ITEM 2:
For the PSI module, I set it up from the hub.
As the QAB states, it isactivated immediately, with nothing to configure, and you stay on the hub.
However, there is no success message and the PSI feature remains intact in the list of features, even after successfully set up. By right, it should disappear.
The UX isn't good here. The user might not know if PSI has been activated.
Details
13338.-.04.-.PSI.-.ok.but.no.msg.and.the.PSI.banner.remains.NOT.ok.mov
ncoetzer commentedon Oct 13, 2026
@kelvinballoo Thanks for the review and feedback.
ITEM 1
The spinner is there, but it's white against a white card background, so you can't see it. The spinner was introduced in #13322, but we'll need to account for lighter backgrounds. I'll raise a separate bug issue for that.
I believe this will be taken care of in #13356
ITEM 2: This will be handled in #13346. Also mentioned in the AC above 🙂
kelvinballoo commentedon Oct 14, 2026
QA Update ✅
Thanks for clarifying @ncoetzer
We should be good moving this to approval.
Tested as follows:
Adsense setup from the hub was fine and we are redirected to the hub after the setup. Reloading the page at the hub maintained the same page.
Details
https://github.com/user-attachments/assets/4cff9364-3406-41fa-ab89-9f35564c7a09
https://github.com/user-attachments/assets/05361167-fda7-4d5c-bcb7-122120bebdd7
Modules starting from a specific hub page will reroute back to the same hub page after setup Tested with RRM and Ads and SiwG.
Details
13338.-.02.-.Ads.setup.from.what.s.new.mov
13338.-.03.-.RRM.setup.ok.mov
13338.-.09.-SiwG.ok.mov
13338.-.09.-SiwG.ok.2.mov
PSI is activated immediately and user remains on the same page.
Details
13338.-.04.-.PSI.-.ok.but.no.msg.and.the.PSI.banner.remains.NOT.ok.mov
Modules that are set up from the connect services page will route to the main dashboard after setup.
Details
13338.-.05.-.module.setup.from.settings.ok.mov
Tested a scenario where we copy out the link to a new tab. After setup, we are redirected to the main dashboard as expected.
Details
13338.-.06.-.RRM.-.start.hub.copy.new.tab.return.to.dashboard.ok.mov
Tested the first and 2nd snippet and in both cases, we are redirected to the main dashboard after the setup, as expected.
Details
13338.-.07.-.First.snippet.return.to.tdesktop.ok.mov
13338.-.08.-second.snippet.mov