← all branches

feat/discovery

needs attentionviewing older commit
7eff611 · incrementalPR #305reviewed 2026-07-18 00:41 UTC0H · 1M · 2L · 5I
The branch
Purpose
Add a feature discovery system to the platform — guided spotlights, a Descubre hub menu, and a first-run tour — so new users organically find key features (CFE setup, contracts, API keys, RPU monitoring).
Goal
Feature Discovery v1: ambient spotlights, hub-driven pulls, first-run guided tour, full FCIS backend, analytics events
Sub-goals
  • SG-1: FCIS discovery domain (discovery-state entity, decisions, shells, queries)
  • SG-2: API handler + contract (getSpotlight, reportSpotlight, getHub, getTour)
  • SG-3: FeatureSpotlight coachmark UI (ambient + pull + revisit modes)
  • SG-4: DescubreMenu hub component
  • SG-5: GuidedTour + TourOverlay + RpuSubTour components
  • SG-6: Analytics events + E2E anchor tests
  • SG-current: Refactor pull to be param-driven (fixes pull→pull dead surface)
The changes (whole branch)
What
Removes the `origin:'pull'|'ambient'` field from `SpotlightPick`, removes `shouldConsumePullParam` and its `useEffect`, and makes pulls render directly from the `?descubre` URL param (early-return branch) rather than routing through the ambient latch. The param is now the single source of truth: kept until the user dismisses/acts (where `stripSummonParam` removes it), never stripped on load. Fixes the pull→pull dead surface: a param swap is now a clean React re-render with no racing strip effect.
Why
The old design coupled pulls to the latch via `origin:'pull'`, which raced a `router.replace` strip effect against `keepPreviousData` placeholder responses. Switching from one hub item to another could blank both cards and jam the surface. The new design eliminates all coupling by separating the concerns: the latch owns ambient picks, the URL param owns pulls.
Areas
apps/platform+231652domains/cross-domain+16040domains/core+4980packages/api + packages/analytics + packages/database+3331e2e/platform+610
Blast
60 files, +4995/-65 across the branch. Platform-only (no CDK, no DB migrations). Core risk area is discovery UI state machine in FeatureSpotlight.tsx and the cross-domain feature-discovery decisions/shells.
no-migration ui-only-refactor-in-this-commit pull-path-unit-tests-missing
typecheck· not checked in this runci· GitHub GraphQL statusCheckRollup not accessible via this tokencoderabbit· no .coderabbit.yaml

Findings · 8

correctness3

low

Comment slightly understates `eligibleKeys` behavior on stale-cache first render

apps/platform/src/components/discovery/FeatureSpotlight.tsx

Comment says 'During the very first load eligibleKeys is empty, so the card waits one tick for the response.' With `keepPreviousData`, the prior query result (a different key) could populate `eligibleKeys` with stale data before the fresh response for the new `requested` key arrives. A key eligible under the stale context could render briefly. Not a regression over the old design (same TanStack Query behavior), but the comment is a slight overstatement. Low impact in practice.

info

`CoachmarkProps` allows `revisit` and `pulled` simultaneously (type doesn't enforce mutual exclusion)

apps/platform/src/components/discovery/FeatureSpotlight.tsx

No call site passes both today, and the semantics are clearly distinct. Documented as a potential footgun if a future caller passes both: `stripSummonParam` would delete both params, which may or may not be intended. Low risk, no action needed now.

info

Pull card React `key` doesn't include `orgId` — potential stale instance on org switch

apps/platform/src/components/discovery/FeatureSpotlight.tsx

key={`pull:${page}:${pulledKey}`} — omits `orgId`. If `orgId` changes without a pathname change (same page, org context update), the pull coachmark won't remount. In practice org switches trigger full navigation, so this is a theoretical edge case only.

security1

info

Double-gate on URL param value is correct and sufficient

apps/platform/src/components/discovery/FeatureSpotlight.tsx

The `requested in SPOTLIGHT_CTA` catalog membership check plus `eligibleKeys.includes(pulledKey)` server validation means the param value never reaches JSX as a raw string and is never used in href/src/dangerouslySetInnerHTML. No XSS or injection vector. `router.replace()` in Next.js App Router cannot navigate to external origins. Clean.

conventions1

info

Render-time `setPick` is valid React derived-state pattern

apps/platform/src/components/discovery/FeatureSpotlight.tsx

Calling `setPick` during render (not in useEffect/handler) with a guard (`nextPick.mountId !== pick.mountId || nextPick.key !== pick.key`) is the documented React pattern for synchronizing derived state. React re-renders synchronously before painting, avoiding tearing. The guard prevents infinite loops. Acceptable.

tests2

medium

New param-driven pull path has no unit test coverage

apps/platform/src/components/discovery/FeatureSpotlight.tsx

The old pull mechanism had dedicated tests: `shouldConsumePullParam` (4 cases) + origin-latch tests in `nextSpotlightPick`. All are correctly deleted because that code is gone. But the new pull path — 'param present → eligible → render; stripped only on dismiss' — has zero unit coverage. The key invariant (never strip on load, only on dismiss) is unguarded. Because `FeatureSpotlight` is a React component, coverage likely needs RTL render tests. The logic is simple, but the prior blink-bug shows this area bites in subtle ways.

low

Deleted `shouldConsumePullParam` tests: risk is eliminated by design, not just untested

apps/platform/src/components/discovery/__tests__/spotlight-latch.test.ts

The tests lens flagged these deletions as leaving the blink-bug unguarded. Clarification: the new design makes the blink-bug structurally impossible — the param is NEVER stripped on load (no `useEffect`, no `router.replace` on mount). The strip only happens in `stripSummonParam` on user dismiss. So the risk isn't moved to a guard-free path; it's designed out. Deletions are correct. Flagging for visibility only.

improvement1

low

`stripSummonParam` merges two orthogonal summon-param concerns

apps/platform/src/components/discovery/FeatureSpotlight.tsx

The callback handles both `?revisit` and `?descubre` deletion. For two cases this is readable. If a third summon mechanism is added, this will become a multi-branch conditional. Two narrow callbacks (`stripRevisitParam`, `stripPullParam`) would be more self-documenting and independently testable. Not a blocker now.

History · 36 commits

  1. c1fe7b7needs attentionincremental3H · 4M · 3L2026-07-27 06:02
  2. 414db56safeincremental0H · 0M · 2L2026-07-23 00:52
  3. ea68968needs attentionincremental0H · 4M · 7L2026-07-22 23:01
  4. 4bdb2c9needs attentionincremental1H · 3M · 5L2026-07-21 23:22
  5. 6e3f255safeincremental0H · 0M · 0L2026-07-20 16:46
  6. cc41079blockedincremental5H · 5M · 3L2026-07-20 16:25
  7. 2451ee0needs attentionincremental0H · 5M · 6L2026-07-20 16:09
  8. ab7c103needs attentionincremental0H · 1M · 6L2026-07-20 15:52
  9. 7eff611needs attentionincremental0H · 1M · 2L2026-07-18 00:41current
  10. 246c67csafeincremental0H · 0M · 0L2026-07-17 23:58
  11. 7521b17safeincremental0H · 0M · 0L2026-07-17 23:35
  12. 1847ec8needs attentionincremental0H · 2M · 4L2026-07-17 23:28
  13. 5d3175cneeds attentionincremental3H · 6M · 4L2026-07-17 19:31
  14. d3f875dneeds attentionincremental1H · 2M · 4L2026-07-17 18:01
  15. 47d25faneeds attentionincremental1H · 1M · 2L2026-07-17 00:46
  16. 440d838needs attentionincremental4H · 9M · 9L2026-07-17 00:27
  17. 7466f97needs attentionincremental0H · 1M · 5L2026-07-16 23:20
  18. b686d58needs attentionincremental0H · 2M · 2L2026-07-16 14:24
  19. 9ea1446blockedincremental2H · 7M · 10L2026-07-16 13:44
  20. 6f39bb9needs attentionincremental0H · 2M · 7L2026-07-14 21:53
  21. 2eadf2aneeds attentionincremental0H · 1M · 2L2026-07-14 20:12
  22. 97bd08fneeds attentionincremental0H · 2M · 5L2026-07-14 19:40
  23. a48e3ffneeds attentionincremental5H · 8M · 6L2026-07-14 18:22
  24. 69473b7needs attentionincremental2H · 5M · 3L2026-07-14 01:34
  25. 92e4235needs attentionincremental2H · 1M · 3L2026-07-14 01:04
  26. 6be8018safeincremental0H · 0M · 3L2026-07-14 00:15
  27. 090b4daneeds attentionincremental1H · 3M · 5L2026-07-13 23:50
  28. fa7683bneeds attentionincremental1H · 4M · 3L2026-07-13 18:55
  29. d64cd72needs attentionincremental2H · 3M · 2L2026-07-13 16:27
  30. c1f337bneeds attentionincremental3H · 7M · 14L2026-07-13 13:30
  31. 46e32b2safeincremental0H · 0M · 3L2026-07-11 02:54
  32. 3b30949needs attentionincremental0H · 2M · 3L2026-07-11 02:39
  33. 9f8dbdfneeds attentionincremental3H · 5M · 5L2026-07-11 02:32
  34. 23191eeneeds attentionincremental5H · 12M · 7L2026-07-11 02:18
  35. fa9b88fneeds attentionincremental0H · 1M · 2L2026-07-11 01:30
  36. 6a8bf42blockedfull8H · 11M · 8L2026-07-11 01:08